diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8052c..503cd6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,55 @@ Conventional Commits by release-please. ## Unreleased +- Self-hosted server: after moving your words from the folder used before the rename to + Kotiko, the old copy (`~/.local/share/slovo/slovo.db`) and the old access key could stay + readable by other accounts. The server now makes them private after the move, and at + every start for installs moved earlier. `install-service.sh --uninstall --delete-data` + now deletes that old copy too. + +- Self-hosted server: someone who could write in your data folder could choose your access + key (by leaving an `api-token` there) or send your words to a file of theirs (by putting + a link where `kotiko.db` goes). The server now refuses to start, saying what it found + and how to fix it, when the data folder or one of Kotiko's files belongs to another + account, when one of Kotiko's files is a link, or when others can write in a data folder + that also holds other files. A folder others can write in that holds only Kotiko's files + is made private first, with a warning. To keep your words on another disk, point + `KOTIKO_DATA_DIR` there instead of linking `kotiko.db`. + +- Self-hosted server: with `LOG_LEVEL=debug`, words the model suggested and the server + refused were written to the log even with `LOG_LOOKUPS=false`. Now only the reasons are, + unless you turn `LOG_LOOKUPS` on. + +- Self-hosted server: when your model or transcription key would go over plain `http://` + to another machine (not this computer, not Tailscale), the server now warns at start, + because anyone on the network in between could read it. + +- Self-hosted server: a user name and password written inside `LLM_URL` or + `TRANSCRIBE_URL` (`https://user:password@host/...`) went into the log at start. The + server now refuses to start with such an address, without showing it; put the key in + `LLM_API_KEY` or `TRANSCRIBE_API_KEY` instead. The same goes for `PUBLIC_URL`. + +- Self-hosted server: an access key you chose yourself that looks easy to guess (few + different characters, a repeated piece, mostly one character) now gets a warning at + every start. Delete `API_TOKEN` to let the server make a random one. + +- Self-hosted server: after 10 wrong access keys from one address within a minute, the + server refuses that address for the rest of the minute (`429`) and logs it once. Other + addresses, including yours, keep working. New route `POST /api/v1/proof`: Kotiko can + check that an address really is your server, which proves it holds your access key + without sending it, before Kotiko sends the key there. + +- Self-hosted server: another account on the same computer could receive your access key + by listening on `[::1]:4747`, the address browsers try first for `localhost`. The server + now listens on both `127.0.0.1` and `::1` by default (and with `BIND=localhost`), so + nobody else can take either; it refuses to start, saying which address, if another + program already holds one. In Kotiko, enter `http://127.0.0.1:4747` as your server's + address. + +- Self-hosted server: requests for many different host names can no longer fill the log. + The server logs each refused name at most once an hour, and at most 1,000 names an hour; + past that it logs one line saying so, and how many it left out. + - Security: a web page that managed to run code inside Kotiko's page script could change Kotiko's settings, because browsers let that script write Kotiko's storage. It could turn Kotiko off, pause sites, hide languages, change the words pages show, queue a word to be diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e41e56c..e35329b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Kotiko architecture -Last reviewed: 2026-10-05. +Last reviewed: 2026-10-06. This page says what runs where, what each part does, where words are kept, what leaves your machine, and where to look next. It describes the code on `main`. Plans for later @@ -120,10 +120,15 @@ order: the log filter that hides secrets, settings, the data folder, the API tok database migrations (with a backup first), a startup summary, then the supervision tree. - **HTTP**: Bandit serves [`Kotiko.Router`](../server/lib/kotiko/router.ex), a Plug - router. Every request passes `Kotiko.Plug.HostCheck` + router, on the address in `BIND` and, for a loopback one, on the other loopback address + too ([`Kotiko.Listener`](../server/lib/kotiko/listener.ex)), so no other account can + listen on `[::1]` where browsers send `localhost`. Every request passes + `Kotiko.Plug.HostCheck` ([`plug/host_check.ex`](../server/lib/kotiko/plug/host_check.ex), refuses unknown host names against DNS rebinding), then the token check (deny by default; only `GET` and - `HEAD /health` are open), then body parsing (64 KB, 1 MB for the batch route). + `HEAD /health` and `POST /api/v1/proof` are open; an address that sent 10 wrong tokens + in a minute gets `429`, [`Kotiko.AuthThrottle`](../server/lib/kotiko/auth_throttle.ex)), + then body parsing (64 KB, 1 MB for the batch route, 1 KB for the proof). `/api/v1` is forwarded to [`Kotiko.RouterV1`](../server/lib/kotiko/router_v1.ex). The routes are listed in [`reference/http-api.md`](reference/http-api.md). - **Settings**: [`Kotiko.Config`](../server/lib/kotiko/config.ex) parses the environment @@ -245,7 +250,8 @@ Kotiko treats these as untrusted and checks what crosses them: HTTP only if you set `BIND` to a network address, with a warning at startup); - the model's answers, which are checked against the spec before anything is saved; - Telegram updates (allowlisted user IDs only); -- the files in the data folder, protected by your operating system's file permissions. +- the files in the data folder, protected by your operating system's file permissions + (the server refuses to start when another account could have put files there). The full threat model, every input and how it is checked, and the evidence are in the [assurance case](security/assurance-case.md). diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 4df295f..8eb22f3 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -53,8 +53,18 @@ The TCP port the server listens on. The address the server listens on. - Default: `127.0.0.1`, this computer only. -- Allowed: an IPv4 or IPv6 address (`100.101.102.103`, `0.0.0.0`, `::1`) or a host name - that resolves to one (`localhost`). +- Allowed: an IPv4 or IPv6 address (`100.101.102.103`, `0.0.0.0`, `::`, `::1`) or a host + name that resolves to one. `localhost` is always `127.0.0.1`, whatever the hosts file says. +- Browsers look up `localhost` themselves and try the IPv6 address `::1` first. So that no + other program or account on this computer can listen on the address the browser tries + and receive the token, the server listens on both loopback addresses: with `127.0.0.1` + (the default) or `localhost` it also listens on `::1`, with `::1` also on `127.0.0.1`, + and with `0.0.0.0` also on `::1`. `::` covers IPv4 too (dual-stack). On a computer + without IPv6 the server starts on `127.0.0.1` alone and logs a line saying so. If another + program already listens on one of these addresses at `PORT`, the server doesn't start + (status 1) and says which address; stop that program or choose another `PORT`. +- In the extension, use `http://127.0.0.1:4747` (not `localhost`) for a server on the same + computer. - Example: your Tailscale address, to use the extension on another computer. - At start, the server logs who can reach it. For a local network address, `0.0.0.0` or a public address, that line is a warning: the server speaks plain HTTP, so anyone on the way @@ -82,7 +92,8 @@ behind a reverse proxy). Used only in the pairing string from [`mix kotiko.token`](#mix-kotikotoken). - Default: none. Allowed: an `http://` or `https://` address; a trailing `/` is dropped. -- Wrong: anything else stops the server ("isn't a web address"). +- Wrong: anything else stops the server ("isn't a web address"), and so does a user name + or password in the address (`https://user:password@...`). ### `API_TOKEN` @@ -94,6 +105,16 @@ The token the extension sends as `Authorization: Bearer ` (see the random source, written as 43 URL-safe characters, in a file only you can read (mode 0600). - Allowed: at least 24 characters. Example: the output of `openssl rand -hex 24`. - Wrong: a shorter token stops the server (the message gives its length, not its value). +- A token that looks easy to guess gets a warning at every start (never showing the + token): one with fewer than 10 different characters, one that repeats a shorter piece + (`passwordpasswordpassword`), or one with under 64 bits by its characters' frequencies + (its length times the Shannon entropy of the character counts, low when most characters + are the same). This is a simple check that catches typed tokens, not a guarantee. The + same check runs on a token you put in `api-token` yourself. Ten wrong tokens from one + address within a minute lock that address out for the rest of the minute + ([HTTP API](http-api.md#requests)), but [`POST /api/v1/proof`](http-api.md#post-apiv1proof) + lets anyone who can reach the server test guesses offline, so only a random token is + safe on a network. - To replace the saved token: `mix kotiko.token --rotate`, then restart. With `API_TOKEN` set, edit `.env` instead; with [`API_TOKEN_FILE`](#api_token_file), replace the file's contents. @@ -107,7 +128,9 @@ The key for the model API that works out which word you mean. A secret. and adds answer `503 lookup_not_set_up` until you set it. A local model such as Ollama needs no key. - Example: a key from . -- Sent only to `LLM_URL`. +- Sent only to `LLM_URL`. When `LLM_URL` is a plain `http://` address on another machine + (not this computer, not a Tailscale address or `*.ts.net` name), the server warns at + start: anyone on the network in between can read the key. - Or from a file: [`LLM_API_KEY_FILE`](#llm_api_key_file). ### `LLM_URL` @@ -117,6 +140,9 @@ The model API: any OpenAI-compatible API's base URL. - Default: `https://openrouter.ai/api/v1`. - Example: `http://localhost:11434/v1` for a local Ollama. - Allowed: an `http://` or `https://` address. Wrong: anything else stops the server. +- A user name or password in the address (`https://user:password@host/v1`) stops the + server too, without showing it: the address is written to the log at start. Put the key + in [`LLM_API_KEY`](#llm_api_key) (or [`LLM_API_KEY_FILE`](#llm_api_key_file)) instead. - Your typed text goes to this address (README, "A different model"). ### `LLM_MODEL` @@ -158,7 +184,9 @@ A speech-to-text endpoint for Telegram voice notes: a local whisper.cpp server upload. - Default: none; voice notes aren't transcribed (typing still works). -- Allowed: an `http://` or `https://` address. Wrong: anything else stops the server. +- Allowed: an `http://` or `https://` address. Wrong: anything else stops the server, and + so does a user name or password in the address: put the key in + [`TRANSCRIBE_API_KEY`](#transcribe_api_key) instead. ### `TRANSCRIBE_MODEL` @@ -171,6 +199,8 @@ The model name sent with each voice note. Sent as `Authorization: Bearer ` to `TRANSCRIBE_URL`, for hosted services. A secret. - Default: none (a local whisper.cpp server needs none). +- With a plain `http://` `TRANSCRIBE_URL` on another machine (not this computer or + Tailscale), the server warns at start, as for `LLM_API_KEY`. - Or from a file: [`TRANSCRIBE_API_KEY_FILE`](#transcribe_api_key_file). ### `KOTIKO_DATA_DIR` @@ -199,7 +229,9 @@ How much the server logs: `debug`, `info`, `warning` or `error` (`warn` works to ### `LOG_LOOKUPS` `true` to log what you look up and the model's answer, at debug level (so also set -`LOG_LEVEL=debug`). For troubleshooting a model. +`LOG_LEVEL=debug`). For troubleshooting a model. Without it, even `LOG_LEVEL=debug` logs +no words: not what you typed, not the words the model answered or the server refused +(only the reasons), not the words a database upgrade flags. - Default: `false`. Allowed: `true`, `yes`, `on`, `1`, `false`, `no`, `off`, `0`. - Wrong: anything else stops the server. @@ -296,8 +328,21 @@ check. | `models-cache.json` | OpenRouter's model list as last read, used when the server starts offline | `0600` | | `backups/kotiko-pre--