At-rest encryption¶
At-rest encryption optionally protects local Synapse storage when an operator's disk, backup target, or support workflow requires a stronger confidentiality boundary than file permissions alone.
The default product remains local-first and low-dependency. Encryption is an opt-in profile that protects data when files are copied, backed up, or read offline. It does not protect data while the hub is running, does not replace filesystem permissions, and does not solve multi-tenant isolation.
Implemented runtime profile¶
The encryption primitive, key-file management, and operator runtime profile are implemented in
:mod:synapse_channel.core.at_rest:
AtRestCipheris an AES-256-GCM envelope over a 32-byte key, with a versioned header bound as additional authenticated data, fresh per-message nonces, and atomicencrypt_file/decrypt_filehelpers. Keys come from a raw key file or from a passphrase via the memory-hard scrypt KDF.synapse encrypt-key generate <path>writes a fresh owner-only 32-byte key;synapse encrypt-key check <path>verifies a key file's ownership, mode, and length before an encrypted workflow trusts it.synapse encrypt-key profile --key <path> ... --require-encryptedinspects the selected runtime surfaces and fails closed if any existing file is plaintext, unreadable, or cannot be authenticated with the key.synapse encrypt-key migrate --key <path> ...encrypts existing plaintext profile files in place. It covers SQLite event-store files plus-waland-shmsidecars, relay logs, A2A state files, cursor files, and archive outputs. Optional--backup-dircopies owner-only plaintext originals before replacement.synapse encrypt-key rekey --old-key <path> --new-key <path> ...rotates already encrypted profile files and refuses missed plaintext files so rotation cannot hide an incomplete migration.synapse encrypt-key backup --key <path> --backup-dir <dir> ...writes an owner-only recovery bundle manifest that copies encrypted bytes exactly as stored. Key material is deliberately excluded.synapse encrypt-key restore --key <path> --manifest <manifest.json>restores a bundle after authenticating every encrypted file with the supplied key.- The AES-GCM primitive comes from the optional
cryptographydependency (pip install synapse-channel[encryption]); the package still imports without it, and an encryption attempt without it raises a clear install hint.
The implemented profile is whole-file operator encryption. It is intended for cold migration, rekey, backup, restore, and fail-safe startup checks around the real files Synapse already writes. Transparent live database opening is not implemented for this whole-file profile because SQLite must keep a database file open while it writes pages and WAL frames.
Live hub event store (SQLCipher)¶
The live synapse hub --db store can use SQLCipher page encryption so the
main database, WAL, and indexes stay ciphertext on disk while the hub holds the
file open:
pip install 'synapse-channel[sqlcipher]' # or sqlcipher3-binary==0.6.0
synapse encrypt-key generate ~/synapse/hub.key
# new store:
synapse hub --db ~/synapse/hub.db --db-key-file ~/synapse/hub.key
# migrate an existing plaintext store (hub stopped; destination must not exist):
synapse encrypt-key migrate-sqlcipher \
--key ~/synapse/hub.key \
--source ~/synapse/hub-plain.db \
--destination ~/synapse/hub.db
Rotate an existing encrypted store (hub stopped) using SQLCipher
PRAGMA rekey:
synapse encrypt-key generate ~/synapse/hub.key.new
# Preferred top-level form:
synapse sqlcipher rekey \
--db ~/synapse/hub.db \
--old-key ~/synapse/hub.key \
--new-key ~/synapse/hub.key.new
# Equivalent legacy form: synapse encrypt-key rekey-sqlcipher ...
mv ~/synapse/hub.key.new ~/synapse/hub.key
synapse hub --db ~/synapse/hub.db --db-key-file ~/synapse/hub.key
Read the same store without decrypting offline copies:
synapse ingest ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse event-query ~/synapse/hub.db --db-key-file ~/synapse/hub.key 'task T timeline'
synapse postmortem ~/synapse/hub.db --db-key-file ~/synapse/hub.key T
synapse merkle root ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse causality causes ~/synapse/hub.db --db-key-file ~/synapse/hub.key 1
synapse accounting report ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse memory-recall ~/synapse/hub.db --db-key-file ~/synapse/hub.key 'probe'
synapse debug ~/synapse/hub.db --db-key-file ~/synapse/hub.key --fork-at 1
synapse reproduce ~/synapse/hub.db --db-key-file ~/synapse/hub.key T
synapse approval status ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse trust-graph ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse ttl-advice ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse cross-repo ~/repos --db ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse participant costs ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse route-task T --event-store ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse multihub observe --peer-db ~/peer/hub.db --db-key-file ~/peer/hub.key
synapse reliability ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse workflow contention wf.json ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse causality contention ~/synapse/hub.db --db-key-file ~/synapse/hub.key
# MCP: synapse_route_task(..., event_store=..., event_store_key_file=...)
# MCP: synapse_memory_recall(event_store=..., event_store_key_file=..., query=...)
synapse sandbox run tool.wasm --manifest m.json --approve \
--attest ~/synapse/hub.db --db-key-file ~/synapse/hub.key
synapse dashboard --feeds-db ~/synapse/hub.db --feeds-db-key-file ~/synapse/hub.key
Doctor can verify the key opens the store:
synapse doctor --db-path ~/synapse/hub.db --db-key-file ~/synapse/hub.key
Without [sqlcipher] the stock install stays dependency-free and --db-key-file
fails closed with an install hint. Whole-file AES-GCM envelopes still apply to
relay logs, A2A state, cursors, and archives — they are complementary, not a
substitute for page encryption of a live open database. SQLCipher protects
offline copies of the event store; it does not protect a running hub's RAM and
does not replace filesystem permissions.
Storage scope¶
The first design must cover every local surface that can contain coordination content:
- SQLite event store files created by
synapse hub --db. - WAL and SHM sidecars that SQLite writes next to the event store.
- Relay logs written by
--relay-logor relay workflows. - A2A state files created by
synapse a2a-serve --state-file. - Cursor files used by relay, ingest, or other replay consumers.
- Archive reports written by compaction or postmortem workflows.
- Temporary files used during atomic writes, bridge state updates, report generation, and backup staging.
- Backups produced by SQLite online backup, filesystem copy, or operator archive jobs.
Generated documentation, public examples, and ordinary source checkouts are out of scope unless they embed private event-log material.
Key model¶
The design should support three operator profiles:
- Passphrase: a human-supplied passphrase unlocks the local store. Key derivation must use a memory-hard KDF when a dependency is available, with parameters recorded in metadata.
- Platform keyring: desktop/server key storage delegates secret wrapping to the OS keyring or an operator-approved secret manager.
- File key: automation reads a key file protected by owner-only file permissions, suitable for systemd user services where interactive prompts are impossible.
- Wrapped key (envelope encryption): a random data key does the bulk AES-GCM,
and a key-encryption key (KEK) wraps it with RFC 3394 AES-KW. The wrapped data key
is stored on disk and unwrapped at startup. Because only the KEK changes when it
rotates (or moves into hardware), no encrypted data is re-written — the data key
underneath is unchanged. The wrapped-key file records which
backendproduced it, and backends are pluggable: synapse encrypt-key generate-wrappedderives the KEK from a passphrase in software;synapse encrypt-key rewraprotates that passphrase.synapse encrypt-key generate-wrapped-pkcs11holds the KEK on a PKCS#11 token (YubiKey PIV, cloud/network HSM, or SoftHSM), wrapping and unwrapping on the device so the key never leaves it — the optionalpython-pkcs11dependency (synapse-channel[pkcs11]) and a module path via--pkcs11-module/PKCS11_MODULE.synapse encrypt-key generate-wrapped-tpm2roots the KEK in a TPM 2.0 device: a decrypt-only RSA-2048 key is derived from the TPM storage seed and wraps the data key with RSA-OAEP, so the RSA private key never leaves the chip. The optionaltpm2-pytssdependency (synapse-channel[tpm2]) and a TPM interface via--tcti/TPM2_TCTI(default the in-kernel resource managerdevice:/dev/tpmrm0).synapse encrypt-key generate-wrapped-cloud-hsmwraps under a cloud HSM / cloud KMS key-encryption key. Providers:local-aes-kw— AES-KW under an owner-only 32-byte master key file (offline tests, air-gapped drills, CI).aws-kms— optionalboto3(synapse-channel[cloud-hsm]) calling KMS Encrypt/Decrypt so the customer master key never leaves AWS (--kms-key-id, optional--region). Network HSMs that expose PKCS#11 continue to usegenerate-wrapped-pkcs11.
Key escrow and recovery¶
Lost-key recovery is impossible unless the operator planned for it. Threshold (Shamir)
escrow over GF(2^8) splits a raw 32-byte data key into n shares with threshold k:
synapse encrypt-key escrow-split \
--key ~/.config/synapse/at-rest.key \
--threshold 3 --shares 5 \
--out-dir ~/synapse/escrow-shares/
# Store share-01.json … share-05.json in separate custody.
synapse encrypt-key escrow-recover \
--share ~/custody-a/share-01.json \
--share ~/custody-b/share-03.json \
--share ~/custody-c/share-05.json \
--out ~/.config/synapse/at-rest-recovered.key
Any k distinct shares reconstruct the key; fewer than k reveal nothing. Recovery
writes a new raw key file — it never bypasses encryption. The live hub never reads
share files automatically.
Hardware attestation¶
Operators can require a fresh attestation of platform PCR digests before trusting a host to unwrap keys. The attestation gate is fail-closed and orthogonal to the KEK backend:
synapse encrypt-key attest-policy-create \
--policy-id seat-a \
--pcr 0=<sha256-hex> --pcr 7=<sha256-hex> \
~/.config/synapse/attest-policy.json
synapse encrypt-key attest-create \
--policy ~/.config/synapse/attest-policy.json \
~/.config/synapse/attest-evidence.json
synapse encrypt-key attest-verify \
--policy ~/.config/synapse/attest-policy.json \
--evidence ~/.config/synapse/attest-evidence.json
The software path uses HMAC-SHA256 with a 32-byte policy key (no extra dependency).
The tpm2-quote algorithm tag supports a software-assisted trust key for CI and the
same statement format for future AK public-area verification. Combine with the TPM2
KEK backend when the long-term secret must stay inside the chip and the host must
prove measured boot state.
Key storage must be explicit. Synapse should never silently write an encryption key next to the encrypted database with broad permissions. A doctor check should verify key-file ownership and mode before an encrypted hub starts.
Rotation and metadata¶
Key rotation should create a new encrypted copy, verify it, then swap atomically:
- Verify the old key file with
synapse encrypt-key check. - Generate and verify the new key with
synapse encrypt-key generateandsynapse encrypt-key check. - Stop writers for whole-file surfaces before migration or rotation. For
SQLite stores, include the main database plus any
-waland-shmsidecars. - Run
synapse encrypt-key profile --key OLD --require-encrypted ...to prove the selected existing files are encrypted before rotation. - Run
synapse encrypt-key rekey --old-key OLD --new-key NEW --backup-dir ./rekey-backup .... The command authenticates old envelopes, writes new envelopes atomically, and keeps owner-only old-envelope copies when--backup-diris provided. - Run
synapse encrypt-key profile --key NEW --require-encrypted ...before restarting the runtime.
The current backup manifest records the manifest schema, storage role, original source path, and copied encrypted file path without storing raw key material. Future platform-keyring profiles can add key ids and KDF metadata without changing the envelope bytes.
AES-GCM message limit¶
AES-GCM with random 96-bit nonces carries a per-key safety bound: after roughly
2**32 messages the probability of a nonce repeating climbs past the 2**-32
threshold, which would weaken confidentiality. AtRestCipher counts the messages
it seals (exposed as encrypted_count), logs a one-time warning once it passes
fifteen-sixteenths of that limit, and raises AtRestKeyExhausted rather than
encrypt beyond it. The correct response is a key rotation (rekey, above).
By default the count lives in memory (InMemoryMessageCounter) and resets when
the process restarts or the key is reloaded, so it guards a single long-running
hub rather than a key's cumulative lifetime. For a key whose count must survive
restarts, pass a PersistentMessageCounter: it persists the count to a sidecar
file and is crash-safe by reserving a batch ahead of use, so a crash resumes at
or above the true count and never under-counts into a nonce collision. That
counter is single-writer — it holds no inter-process lock, so exactly one
live hub process may own a given key and its sidecar; a genuine multi-writer
deployment would need inter-process file locking, which is out of scope here.
Backup recovery¶
Backup recovery must be boring and documented:
- A backup bundle includes encrypted store files and
manifest.json. - The key material is backed up separately from the encrypted data; the manifest never contains the raw key.
synapse encrypt-key restore --key KEY --manifest manifest.jsonauthenticates every encrypted file before replacing the recorded source path.- Restored files are written owner-only.
- For SQLite event stores, recovery replay should be checked after restore by
opening the event store with the normal
synapse hub --dbor read-side CLI after the operator has restored/decrypted the cold envelope into the expected runtime file. - Lost-key recovery is impossible by design unless the operator has an escrowed
passphrase, key-file backup, platform-keyring recovery path, or threshold
escrow shares (
synapse encrypt-key escrow-split/escrow-recover).
The command-line UX should state that lost-key recovery cannot decrypt the data. It can help identify which key id is needed, but it must not imply a bypass.
Runtime examples¶
Generate and verify a file key:
synapse encrypt-key generate ~/.config/synapse/at-rest.key
synapse encrypt-key check ~/.config/synapse/at-rest.key
Encrypt the full local profile after stopping writers:
synapse encrypt-key migrate \
--key ~/.config/synapse/at-rest.key \
--sqlite-db ~/synapse/hub.db \
--relay-log ~/synapse/feed.ndjson \
--a2a-state-file ~/synapse/a2a-state.json \
--cursor ~/synapse/feed.cursor \
--archive-report ~/synapse/compact-report.html \
--backup-dir ~/synapse/at-rest-migration-backup
Fail closed before startup:
synapse encrypt-key profile \
--key ~/.config/synapse/at-rest.key \
--sqlite-db ~/synapse/hub.db \
--relay-log ~/synapse/feed.ndjson \
--a2a-state-file ~/synapse/a2a-state.json \
--cursor ~/synapse/feed.cursor \
--archive-report ~/synapse/compact-report.html \
--require-encrypted
Create and restore a recovery bundle:
synapse encrypt-key backup \
--key ~/.config/synapse/at-rest.key \
--backup-dir ~/synapse/at-rest-backup \
--sqlite-db ~/synapse/hub.db \
--relay-log ~/synapse/feed.ndjson
synapse encrypt-key restore \
--key ~/.config/synapse/at-rest.key \
--manifest ~/synapse/at-rest-backup/manifest.json
Local-first tradeoff¶
At-rest encryption adds operational weight: key prompts, service unlock order, rotation procedures, backup discipline, and failure modes. The local-first tradeoff is that single-owner default installs should stay simple, while operators with stronger storage requirements can opt in and accept the extra runbook.
The first implementation should prefer a small, auditable encryption boundary over broad dependency sprawl. If strong authenticated encryption cannot be provided without a mature dependency, the feature should remain design-only rather than ship a custom cipher or home-grown key schedule.
Boundaries¶
This profile can encrypt current event-store files as cold whole-file envelopes,
including WAL and SHM sidecars when they exist. It does not provide transparent
SQLCipher-style live encryption for the standard sqlite3 event store. It does
not protect data while the hub is running and has decrypted state in memory. It
does not replace filesystem permissions, process isolation, or host-disk
encryption. It does not solve multi-tenant isolation, per-agent secrecy, signed
events, or network transport security.
At-rest encryption should integrate with paranoid mode as one reported hook: paranoid mode can require encrypted storage only after the encryption feature exists, has migration tests, and has recovery documentation. It remains separate from end-to-end encrypted channels, which hide selected payload bodies from the hub while preserving visible routing metadata.