Data at rest, backups and erasure#
Status: decision record, 2026-11-05.
Decision: volume encryption, not SQLCipher#
Groundnote requires the operator to store data/ on an encrypted volume (LUKS/dm-crypt on
Linux, FileVault on macOS, an encrypted block volume at a cloud provider). Groundnote itself does
not encrypt SQLite pages.
Options considered:
| Option | Protects against | Cost | Verdict |
|---|---|---|---|
| SQLCipher (page encryption) | Copy of the file without the key | A native fork of SQLite instead of stock SQLite; key must be on the same host to run the service, so a host compromise yields both; Litestream replicates pages and would need the same build; sqlite3 .backup, the restore probe and every operator tool need the SQLCipher build too; FTS5 and sqlite-vec must be compiled into the same fork |
rejected for now |
| Volume encryption | Stolen disk, VM image, decommissioned hardware | Operator setup, no code | required |
| Application-level field encryption | Leaked file and leaked backup, if keys live elsewhere | Breaks FTS over encrypted fields | only for erasure (crypto-shredding option below) |
Reasoning: the realistic threats for a single-operator service are a lost or copied disk and a leaked backup. Volume encryption covers the first at zero code cost. SQLCipher would add a second SQLite build to every path that touches the file (service, backup, restore probe, export) while the key would still sit on the running host. The backup threat is handled separately below, because neither SQLCipher-with-key-on-host nor volume encryption protects a bucket.
File permissions (built, F2)#
- Data directory
0700, files0600, owned by the service user. The server setsumask 077before opening or creating any file. - At start the server checks the data directory,
scopes/and every SQLite file and refuses to run if one is group- or world-accessible (data-dir-permissions, no silentchmod; commit25231ea). - The container image runs as a non-root user; the volume is mounted only into this container.
Backups#
backup.kind |
Encryption | Recommendation |
|---|---|---|
copy + restic/borg |
Client-side, by the backup tool (restic encrypts every repository) | Recommended for scopes with personal data |
litestream |
None client-side. Litestream 0.5.x rejects the age encryption block at start (since 0.5.1; configuration reference, retrieved 2026-11-05). |
Only with a bucket that enforces provider-side encryption, a bucket-scoped key with no list permission on other buckets, object lock or versioning against deletion, and a lifecycle rule matching the erasure deadline |
The restore probe (daily) and the manual probe (quarterly, on another machine) are part of the contract; a backup without a passed restore does not count.
Erasure despite append-only#
A person named in a mail or meeting may demand erasure (GDPR Art. 17). The scope file forbids
UPDATE and DELETE on history by trigger. The design that replaces the earlier proposal (an ops
command groundnote redact with a free-text reason) is ../redaction.md: a
tombstone with a category from an enum, one narrow trigger exemption, redaction only from the
approval channel by a person, secure delete, and per-scope age keys for backups and exports
(crypto-shredding). It is not built; the order of its slices is in
architecture.md section 15.2. Volume encryption stays mandatory, SQLCipher is
not adopted (redaction.md section 10).
Secrets and logs#
Secrets only from the environment; never in groundnote.json, never logged, never in error
messages. Service bearers stored as SHA-256. Logs carry no request bodies, quotes or excerpts
(see threat model, "Logging").