Security Policy
This document is the threat model and security posture for the Kryptos sync server (kryptos-api) —
the only internet-facing component. Kryptos Mobile is a local-only, end-to-end-encrypted vault; when the
optional self-hosted sync is used, the server stores and routes ciphertext it cannot read. This file
states exactly what that means, what the server can and cannot observe, and how it defends itself.
It reflects the posture as of kryptos-api 0.28.0. It is versioned with the code and updated as the posture changes.
Supported versions
Security fixes target the latest released api-v* minor. Older minors are not maintained; upgrade to
the latest release.
What the server can and cannot see
The vault is encrypted on the device with a key derived from the user’s secret; that key never leaves the device. Sync ships already-encrypted records and blobs. Therefore:
The server CANNOT see:
- Device plaintext — the decrypted vault contents.
- The data-encryption key (DEK) — it is derived and held client-side only; the server never receives it.
- Record and blob contents — records and blobs are stored and transferred as ciphertext only. The server cannot decrypt them and has no key with which to try.
The server CAN see (and an operator should treat as observable metadata):
- Routing metadata — which enrolled device syncs, and when (connection timing, frame counts, byte counts of ciphertext moved).
- Coarse aggregates —
stats()-level totals: record counts by type, blob count and bytes, storage used/quota/free. These are counts and sizes, never contents. - Device key fingerprints — a short fingerprint of each enrolled device’s public key (used in the admin UI and activity feed); not the plaintext identity of the human.
- Client IP addresses — used as the rate-limit key (from the socket peer, or a trusted proxy’s
X-Forwarded-ForwhenTRUST_PROXYis set). - Activity events — coarse, identity-free events (
enrolled/authenticated/rejected/sync) carrying a fingerprint or a generic reason, never payloads, keys, or refs.
The design intent is that compromising the server yields metadata, not vault contents — the end-to-end encryption is the backstop that holds even if the server is fully popped.
Pairing & transport trust model
- Handshake: device ⇄ server pairing/sync runs over a Noise-IK handshake on the
/pairWebSocket. The device knows the server’s static public key ahead of time (it’s in the pairing payload), so it authenticates the server and pins that key; subsequent reconnects re-authenticate by the pinned key with no re-pairing. - Enrollment: a device enrolls with a single-use, short-TTL (~5 min) pairing token minted by the operator. At most one live code exists (each mint revokes the prior); a token is consumed exactly once.
- Transport: a real mobile device pairs only over
wss://(TLS) — iOS/Android block cleartext. TLS is terminated by a reverse proxy (Caddy) in front; the app itself speaks plain HTTP/WS behind it.
Abuse & authentication posture
- Rate limiting (fail-closed before crypto): the
/pairsurface is per-source rate-limited with a token bucket; an over-budget source is closed with a transient1013before any Noise responder is allocated, so a junk/brute-forced handshake is cheap to reject. A failed handshake debits an extra penalty (a token brute-forcer throttles itself out); a successful re-auth is never penalized. The admin mint and the admin login are likewise throttled. Always-on with generous defaults (PAIR_MAX_*). - Admin authentication (opt-in): setting
ADMIN_TOKENputs a session gate over the whole admin console — a login verified in constant time, a sliding-window session in anHttpOnly; SameSite=Strictcookie, and a per-source login throttle. Unset preserves the pre-auth, loopback-only behavior. - CSRF: authenticated state-changing admin POSTs require a session-derived HMAC CSRF token
(
_csrffield orX-CSRF-Tokenheader); it needs noOrigincheck, so it works behind a TLS proxy. /metrics: authenticated by its own bearer token (METRICS_TOKEN), independent of the operator session and strictly read-only (least privilege) — a cookie-less scraper works, but it can only read gauges.- Exposure model: the admin console defaults to loopback (reach it via
ssh -L). Exposing it remotely is an explicit opt-in (ADMIN_ALLOW_REMOTE) that fails fast unless bothADMIN_TOKENandMETRICS_TOKENare set and marks the session cookieSecure— remote admin runs over authenticated TLS.
At-rest integrity & backup
- Integrity: the record DB (SQLite, WAL) is bounded by a periodic maintenance tick (WAL truncate) and
can be scrubbed on demand (SQLite
integrity_check+ blob checksum verification + free-disk). Schema/layout migrations run through an ordered ladder that takes a pre-migrate snapshot before each step. - Backup: a hot-snapshot command takes a consistent copy to a fresh directory; restore by replacing
/data. Storage intake is bounded (quota + free-disk floor) so sync traffic can’t fill the host disk. - At-rest scope: the vault data at rest is ciphertext (the E2E backstop). The server’s identity
private key and the enrolled-device registry live in
/dataas operational secrets — protect the/datavolume and the host (filesystem permissions, disk encryption) accordingly. The server does not add its own at-rest encryption layer over/data.
Caveats & non-goals
- Single-tenant: one shared operator secret; no multi-user/RBAC/SSO.
- In-memory security state: sessions, rate-limit buckets, and the CSRF signing key are in memory and reset on restart (a deploy logs the operator out and clears counters). There is no persistent admin audit trail.
- Not volumetric-DDoS protection: the rate limiter bounds per-source application work; absorbing a network flood is a WAF/fail2ban/cloud-scrubbing layer, out of scope.
- TLS is proxy-terminated (Caddy), not issued in-app.
- Trusted-proxy IP is opt-in:
X-Forwarded-Foris trusted only whenTRUST_PROXYis set (a directly exposed server must not trust it — it’s client-spoofable).
Reporting a Vulnerability
Please report security issues privately — do not disclose them publicly before a fix is available.
- Email security@microcode.io with the details.
Include a description, affected version, reproduction steps, and impact. Expect an acknowledgment within a
few days; fixes are prioritized by severity and released as a new api-v* version, credited to the reporter
unless anonymity is requested. Please allow a reasonable disclosure window before publicizing.