From 6688f2533fb5e94c46ceacbcbea20b4a25b43ec8 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Tue, 6 Oct 2026 18:30:08 -0400 Subject: [PATCH 1/4] fix(server): keep the word list private from other accounts The database, its -wal and -shm files, the model list cache and a new data folder were created with the process umask, and the systemd unit set no UMask, so on a shared computer other accounts could read the word list (SCR-450). The API token and backups were already private. Now every file the server keeps in the data folder is readable only by its own account. The BEAM can't give a mode when it creates a file, so files are created empty, made 0600, and only then filled: the database before SQLite first opens it (SQLite gives the -wal, -shm and -journal files the database's mode; a test checks this), each backup and the copy from the old name before VACUUM INTO, and the token and model cache through a private temporary file. A data folder the server makes is 0700. For existing installs, every start checks the folder before the database opens: Kotiko's own files that others can open are made 0600 again (with one warning naming them), and the folder 0700 when it holds nothing but Kotiko's files. A folder with other files in it, or a link, may be shared on purpose, so it is left as it is with a warning that gives the chmod to run. Nothing here stops the start. run.sh now sets umask 077 and the unit from install-service.sh UMask=0077, so files are private from the moment they exist. Docs: the configuration reference, security requirements and assurance case (the umask gap leaves the residual risks), the site's server page, the changelog and slice 29. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ayla Croft --- CHANGELOG.md | 9 ++ docs/reference/configuration.md | 24 +++- docs/security/assurance-case.md | 18 +-- docs/security/requirements.md | 14 ++- server/install-service.sh | 2 + server/lib/kotiko/application.ex | 2 +- server/lib/kotiko/config.ex | 4 +- server/lib/kotiko/data_dir.ex | 157 +++++++++++++++++++++--- server/lib/kotiko/llm/catalog.ex | 11 +- server/lib/kotiko/migrations.ex | 7 ++ server/lib/kotiko/private.ex | 117 ++++++++++++++++++ server/lib/kotiko/token.ex | 15 +-- server/run.sh | 4 + server/test/kotiko/boot_test.exs | 16 ++- server/test/kotiko/config_test.exs | 13 +- server/test/kotiko/data_dir_test.exs | 134 ++++++++++++++++++++ server/test/kotiko/llm/catalog_test.exs | 1 + server/test/kotiko/private_test.exs | 157 ++++++++++++++++++++++++ server/test/kotiko/scripts_test.exs | 16 +++ server/test/kotiko/token_test.exs | 7 ++ site/src/content/docs/server/index.md | 5 + slices/29-server-ops-hardening/SPEC.md | 8 +- 22 files changed, 682 insertions(+), 59 deletions(-) create mode 100644 server/lib/kotiko/private.ex create mode 100644 server/test/kotiko/private_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9f146..6527336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ Conventional Commits by release-please. ## Unreleased +- Self-hosted server: other accounts on the same computer can no longer read your words. + The database (with the `-wal` and `-shm` files next to it), the model list cache and a + new data folder are now readable only by you, like the API token and the backups + already were. At start, the server makes these files private again if an older version + left them readable, and logs what it changed. It leaves a data folder that also holds + other files as it is and warns with the command to fix it. `run.sh` and the service + from `install-service.sh` now start the server with umask `077`; run + `install-service.sh` again to update the service. + - Self-hosted server: your keys can now live in their own files instead of `.env`. Set `LLM_API_KEY_FILE=/path/to/file` in place of `LLM_API_KEY=...`, and the same for `API_TOKEN`, `TELEGRAM_BOT_TOKEN` and `TRANSCRIBE_API_KEY`; this is how Docker secrets diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 966624f..4df295f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -180,7 +180,7 @@ The folder that holds your words and the server's files (see - Default: `$XDG_DATA_HOME/kotiko`, or `~/.local/share/kotiko` when `XDG_DATA_HOME` isn't set (or isn't an absolute path, which the XDG Base Directory spec says to ignore). The - server creates it if it's missing. + server creates it if it's missing, readable only by you (`0700`). - Words already in `~/.local/share/kotiko` stay there: while `$XDG_DATA_HOME/kotiko` has no `kotiko.db` and `~/.local/share/kotiko` has one, the server keeps using `~/.local/share/kotiko`. To move them, stop the server and move the folder. @@ -291,11 +291,29 @@ check. | File | What it is | Permissions | |---|---|---| -| `kotiko.db` (with `kotiko.db-wal` and `kotiko.db-shm` while running) | Your words, kept responses for repeated adds (24 hours), cached lookups (30 days), and the background jobs' state. SQLite | Created with your umask: usually readable by other users of the computer unless your umask is `077`. The default folder is inside your home folder. To keep it private: `chmod 700` the data folder (usually `~/.local/share/kotiko`) | +| `kotiko.db` (with `kotiko.db-wal` and `kotiko.db-shm` while running) | Your words, kept responses for repeated adds (24 hours), cached lookups (30 days), and the background jobs' state. SQLite | `0600`. The server creates it empty and private before SQLite opens it, and SQLite gives `-wal` and `-shm` the database's permissions | | `api-token` | The generated API token, when `API_TOKEN` isn't set | `0600`, written so no other user can read it at any moment | -| `models-cache.json` | OpenRouter's model list as last read, used when the server starts offline | Your umask | +| `models-cache.json` | OpenRouter's model list as last read, used when the server starts offline | `0600` | | `backups/kotiko-pre--