Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,40 @@ Conventional Commits by release-please.
now warns however the address is written. An OpenRouter address in capitals is also
recognised as OpenRouter.

- Security: a program that took your server's place while it was stopped could keep a
request Kotiko sent it and play it to your server once it was back, and get your word
list, when the clock of the computer Kotiko runs on was ahead of the server's. Your
server now makes a new random id each time it starts, gives it with its proof, and
accepts only requests signed with the current one; after a restart Kotiko asks for a
new proof and sends the request again by itself. Update the server and the extension
together: each turns the other's older version away and says so.

- Release process: the check that holds store uploads until the security review is closed
still missed "Gate: open" written as a list item, a table row, code, HTML, an entity or
math, a fake "Appendix" heading inside a comment or code block, a closing line hidden in
a comment, and "the review is still open" written as prose. It now reads all of these as
open, counts the closing line and the heading only where a reader sees them, and says
which line keeps the gate open.

- Self-hosted server: the two addresses that answer without your access key, `/health` and
`/api/v1/proof`, also answered when written another way, such as `/health/` or
`//api/v1/proof`. Now only the exact addresses do; any other spelling needs the key.

- Self-hosted server: a key written in the query of `LLM_URL` or `TRANSCRIBE_URL` (some
providers take `?key=...`) was written to the log at start. The log now shows the address
without its query (`https://host/v1?…`), and the query's values are taken out of every
log line. A query such as `?api-version=...` now also reaches the provider on every
request, after the path.

- Self-hosted server behind a reverse proxy: everyone the proxy passes on was counted as
one, so a stranger sending ten wrong keys a minute could keep your other devices out
for as long as they kept at it. Set the new `TRUSTED_PROXY_HEADER` to the header your
proxy puts each visitor's address in (`x-forwarded-for`, `x-real-ip`,
`cf-connecting-ip` or `forwarded`), and each visitor is counted on their own. The server
also recognises more of the headers proxies add (`Via`, `X-Client-IP`,
`Fastly-Client-IP` and others), so visitors through such a proxy are no longer taken for
your own computer, which is never limited.

- Security: a Kotiko server set to a model of its own (`LLM_MODEL`), for example on a paid
provider, sent no limit on how long an answer may be, so a model that kept writing could
cost you up to the provider's own limit for one word. Every lookup now asks for at most
Expand Down
41 changes: 38 additions & 3 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ behind a reverse proxy). Used only in the pairing string from
- 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@...`).

### `TRUSTED_PROXY_HEADER`

Behind a reverse proxy on this computer (Caddy, nginx, `tailscale serve`, a Cloudflare
tunnel): the header that proxy sets to the address of the client it forwards. The
wrong-token lockout and the proof limit then count each client on its own, so a stranger
who sends wrong tokens locks out only themselves, not your other devices.

- Default: none. Every request a proxy on this computer forwards is then counted together
under the proxy's address (see [`API_TOKEN`](#api_token)): ten wrong tokens a minute
from anyone keep everyone who comes through the proxy out, your other devices included.
- Allowed: `x-forwarded-for` (the rightmost address in the header, the one the proxy
added), `x-real-ip`, `cf-connecting-ip` (its one address), or `forwarded` (the last
element's `for=`), in any case. Wrong: anything else stops the server.
- It is read only for requests from this computer that carry a forwarding header. The
address is counted like any other: an IPv6 address by its /64, an IPv4-mapped one as
IPv4. A request without a usable address in that header (none, two `X-Real-IP` lines,
`unknown`, an obfuscated name) is counted with the proxy's shared count.
- Set it only when the proxy sets or overwrites that header on every request. Otherwise a
client can write any address there and gets a new count with each one. Caddy sets
`X-Forwarded-For`, and nginx appends to it with
`proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`: either way its rightmost
address is the client the proxy saw. Behind Cloudflare, use `cf-connecting-ip`, since
the rightmost `X-Forwarded-For` address is then Cloudflare's.

### `API_TOKEN`

The token that opens the server. A secret. The extension never sends it: it signs each
Expand All @@ -118,9 +142,14 @@ send it as `Authorization: Bearer <token>` (see the [HTTP API](http-api.md#reque
page shares, so none of them can lock the extension out;
[`POST /api/v1/proof`](http-api.md#post-apiv1proof) isn't limited for them either.
Requests a reverse proxy on this computer forwards (they carry a header such as
`X-Forwarded-For` or `Forwarded`) are limited, all together: a stranger's lockout
refuses everyone coming through the proxy for the rest of that minute, never the
extension. A token the server made (256 random bits) can't be guessed, so this costs
`X-Forwarded-For`, `Forwarded` or `Via`; the [HTTP API](http-api.md#requests) lists them)
are limited, all together: a stranger's lockout refuses everyone coming through the
proxy for the rest of that minute, including the extension on your other devices that
reach the server through it. Only the extension on this computer, which sends no
forwarding header, is never locked out. Set
[`TRUSTED_PROXY_HEADER`](#trusted_proxy_header) to count each client behind the proxy
on its own. A proxy that adds none of the listed headers makes its clients look like
this computer's own: they are never limited. A token the server made (256 random bits) can't be guessed, so this costs
nothing; a weak token you chose can be guessed by any program on this computer without
limit, and the proof lets anyone who can reach the server test guesses offline. Keep
the generated token.
Expand Down Expand Up @@ -152,6 +181,11 @@ The model API: any OpenAI-compatible API's base URL.
- 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.
- A query (`?api-version=2024-10-21`, or `?key=...` for a provider that takes its key
there) is kept on every request, after the path (`/chat/completions?api-version=...`).
It is never logged: the start-up summary shows the address as `https://host/v1?…`, and
the query's values are taken out of every log line like the other secrets. A key that
the provider accepts in a header still belongs in `LLM_API_KEY`.
- Your typed text goes to this address (README, "A different model").

### `LLM_MODEL`
Expand Down Expand Up @@ -196,6 +230,7 @@ upload.
- 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.
- A query is kept on the request and never logged, as for [`LLM_URL`](#llm_url).

### `TRANSCRIBE_MODEL`

Expand Down
Loading