Self-hostable, zero-knowledge sync server for the Keyfount browser extension (and future mobile clients).
The extension is a deterministic password manager: passwords are derived
on demand from master + domain + email and never stored. The only data
worth syncing is the user's account index — which (domain, username)
pairs they have registered, and the per-site generation profile attached to
each one.
This server is that sync layer, with two strong properties:
- Zero-knowledge. The server never sees the master password, an email address, a domain, or a username. It only stores opaque ciphertexts and ordering metadata.
- No offline brute-force after a server compromise. Authentication uses OPAQUE (an asymmetric PAKE). Even a complete database dump leaks nothing that an attacker can use to crack the master offline.
🚧 Work in progress. M1 (scaffold + /health + CI + container
publishing) is in place; auth, sync, and snapshots land in subsequent
milestones.
Multi-arch images (linux/amd64, linux/arm64) are published to GHCR on
every push to main and on every v*.*.* tag:
ghcr.io/keyfount/server:latest # rolling main
ghcr.io/keyfount/server:v0.1.0 # pinned semver
ghcr.io/keyfount/server:sha-abcdef0 # exact commit
A 32-byte HMAC key is required. Generate it once and keep it stable — rotating it invalidates all stored email/IP hashes:
openssl rand -base64 32- Stacks → Add stack → name it
keyfount. - Paste the contents of
docker-compose.ymlinto the editor. - Under Environment variables, set at minimum:
SERVER_HMAC_KEY(the base64 string above)HOST_PORT(e.g.8080, change if taken)
- Click Deploy the stack. Expose
HOST_PORTbehind your existing reverse proxy (Cloudflare Tunnel, Traefik, nginx…).
- Container Manager → Project → Create.
- Source: Create docker-compose.yml. Paste
docker-compose.yml. - Add a sibling
.envfile withSERVER_HMAC_KEY=…andHOST_PORT=8080. - Build & start. Wire it through DSM Control Panel → Login Portal → Advanced → Reverse Proxy to expose it on HTTPS via your DSM cert.
If ports 80/443 are free and you want Caddy to handle Let's Encrypt:
git clone https://github.com/Keyfount/server.git
cd server
cp .env.example .env
# set SERVER_HMAC_KEY, DOMAIN=sync.example.com, ACME_EMAIL=you@example.com
docker compose -f docker-compose.standalone.yml up -dCaddy is configured inline in that compose file (no extra Caddyfile to manage).
git clone https://github.com/Keyfount/server.git
cd server
cp .env.example .env
echo "SERVER_HMAC_KEY=$(openssl rand -base64 32)" >> .env
docker compose up -d| Variable | Default | Description |
|---|---|---|
SERVER_HMAC_KEY |
required | Base64-encoded 32+ bytes. Hashes email/IP at rest. Never rotate. |
HOST_PORT |
8080 |
Host port for the bare compose. |
LOG_LEVEL |
info |
fatal|error|warn|info|debug|trace |
TRUST_PROXY |
true |
Read X-Forwarded-* (set false if exposed directly). |
CORS_ORIGINS |
(empty) | Comma-separated. E.g. chrome-extension://abc… |
DOMAIN |
(standalone only) | Public hostname for Caddy / Let's Encrypt. |
ACME_EMAIL |
(standalone only) | Contact for Let's Encrypt. |
curl http://localhost:8080/health
# {"status":"ok"}
See the self-hosting runbook: docs/self-host.md.
TL;DR for backups: stop the stack, tar the data volume, restart. Keep
SERVER_HMAC_KEY backed up separately — without it the database is
unusable.
If you discover a security issue, please do not open a public issue; see SECURITY.md. The full threat model is in docs/threat-model.md and the wire protocol is fully documented in docs/protocol.md.
npm install
cp .env.example .env # then set SERVER_HMAC_KEY
npm run dev
npm test
npm run typecheck