Keep server data in an encrypted database, and harden every part

Storage. The server keeps its data in SQLite (a file in the data
directory, the default) or PostgreSQL (database, KEEPORG_DATABASE_URL or
-database). Every row, the KDBX versions included, is sealed with
AES-256-GCM under a key derived from the master key and named by an HMAC.
The master key is required to start and a different one is refused. A data
directory from 0.10 or earlier is imported on first start. Several servers
may share one PostgreSQL database. Go goes to 1.26 for the pure-Go
drivers (modernc.org/sqlite, jackc/pgx).

Hardening, after an audit of the server, both clients and the deployment:
- Single sign-on approves a device only after an explicit Approve in the
  browser that started it, bound by a cookie and PKCE; identities are never
  linked by username, by verified email only when allowed, never to admins.
- Sign-in attempts are limited per address and per account, passwords are
  checked outside the store lock with uniform timing, one-time codes work
  once, and starting two-factor needs the password.
- Merge vaults bound KDF parameters, payload size and XML shape, check
  pushes before storing them, and run at most two merges at once. A version
  is written in the same transaction as its metadata.
- Tokens in URLs only on the event stream, which ends on revocation; CSP,
  frame-ancestors, no-store and HSTS; sign-in CSRF; 404 for databases
  without access; new trusted_proxies.
- Clients open only the server's approval page, keep case-variant and
  retired file names apart, never overwrite a save made during a pull,
  require https except locally, send the stream token in a header, bound
  their reads and escape notifications and terminal output.
- The .deb creates the master key, systemd is sandboxed further, nginx sets
  modern TLS and HSTS, and CI deploys from main only.

Server 0.12.0, desktop 0.6.0, Android 0.4.0 (versionCode 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Reactorcoremeltdown
2026-11-02 15:38:32 +01:00
co-authored by Claude Opus 5.5
parent f8f634fbf7
commit 4ffc434b1b
77 changed files with 4192 additions and 776 deletions
+15 -10
View File
@@ -25,11 +25,12 @@ keep their key off the server entirely, exactly as before.
- **Android client**: the same, published to every file picker as a storage
provider.
No FUSE, and the server has no third-party dependencies and no cgo. The
KDBX 4 codec, including ChaCha20, BLAKE2b and Argon2, is part of the
repository and is tested against both the relevant RFCs and a real
KeePassXC. The desktop client's one dependency is `fyne.io/systray`, for the
tray icon.
No FUSE and no cgo. The server keeps everything in SQLite or PostgreSQL,
every row sealed under a master key the database never sees, and its only
third-party code is the two pure-Go database drivers. The KDBX 4 codec,
including ChaCha20, BLAKE2b and Argon2, is part of the repository and is
tested against both the relevant RFCs and a real KeePassXC. The desktop
client's one dependency is `fyne.io/systray`, for the tray icon.
## Quick start
@@ -97,7 +98,7 @@ Or open `http://127.0.0.1:8765/web` and create the vault from the browser.
```sh
make deb # Debian packages for the client and the server
make docker # server container image, scratch-based, about 9 MB
make docker # server container image, scratch-based, about 25 MB
make dmg # macOS disk image with an /Applications symlink (on macOS)
make windows # Windows zip: keeporg-tray.exe, keeporg.exe and a Task Scheduler job
make android # unsigned release APK (needs the Android SDK)
@@ -117,9 +118,11 @@ podman run -d -p 8765:8765 \
keeporgd:latest -addr :8765 -data /var/lib/keeporg -url https://keeporg.example.com
```
Keep that master key: it unwraps the vault keys, and without it the server
can no longer merge. `KEEPORG_MASTER_KEY` takes the base64 key directly if
your secret store injects variables instead of files. See
Keep that master key: everything in the database is encrypted under it,
and the server does not start without it. `KEEPORG_MASTER_KEY` takes the
base64 key directly if your secret store injects variables instead of
files. The data goes to a SQLite file on the volume; to use PostgreSQL
instead, add `-e KEEPORG_DATABASE_URL=postgres://keeporg:…@db/keeporg`. See
[packaging/docker/compose.yaml](packaging/docker/compose.yaml).
Server environment variables:
@@ -127,8 +130,10 @@ Server environment variables:
| Variable | Meaning |
| --- | --- |
| `KEEPORG_MASTER_KEY` | the master key, base64; overrides the other two sources |
| `KEEPORG_MASTER_KEY_FILE` | path to a file holding the master key (base64 or 32 raw bytes); never created, and if it is missing the server runs but cannot create or merge vaults |
| `KEEPORG_MASTER_KEY_FILE` | path to a file holding the master key (base64 or 32 raw bytes); never created, and the server does not start without it |
| `KEEPORG_EXTERNAL_URL` | the public base URL (`external_url`); `-url` overrides it |
| `KEEPORG_DATABASE_URL` | `postgres://…` or `sqlite:<path>` (`database`); default a SQLite file in the data directory; `-database` overrides it |
| `KEEPORG_TRUSTED_PROXIES` | reverse proxies whose `X-Forwarded-For` is believed, for per-client rate limits (`trusted_proxies`) |
| `KEEPORG_ADMIN_PASSWORD` | the first `admin` password, read only on an empty server and by `-reset-admin` |
Everything else is configured in `server.json` or by flag. The details,