Documentation
¶
Overview ¶
Package cek is cloud's ONE encryption-at-rest gate for its per-subsystem SQLite stores. Every store opens its database through cek.Open — the single seam where a plaintext file is transparently migrated to SQLCipher and a keyed *sql.DB is returned — so "encrypted at rest" is a property of the open path, not something each of the ~30 stores must remember to do.
THREAT MODEL. The DO block volume under /var/lib/cloud is provider-encrypted, so the residual exposure is a COPIED PV snapshot/backup or an in-cluster exec/PV read seeing plaintext customer PII (crm), the ledger (treasury), org wallet maps (wallets), the audit log, team/entitlements. cek removes that exposure: each file's pages are SQLCipher-encrypted under a per-database key that never leaves the process in the clear and is itself wrapped by the KMS-injected master key; the pre-migration plaintext copy is shredded once the encrypted store is proven readable. A lifted file is useless without the key.
SCOPE / NON-GOALS. cek provides CONFIDENTIALITY at rest against the read-only exposure above (no master key ⇒ no plaintext). It is NOT integrity, authenticity, or anti-rollback against a PV-WRITE (node-compromise) adversary who can modify the volume: the per-file id lives in the (unauthenticated) .dek sidecar, so such an adversary could still replay an old snapshot of a store in place. Swapping two stores ACROSS tenants no longer works — the owner is in the KEK and the AAD, so a pair carried into another org's directory fails to unwrap (Principal). Rollback of a store onto itself remains out of model; it needs an epoch, which the sidecar does not carry.
ENVELOPE (the primitives live in github.com/hanzoai/sqlite/cek.go and are reused verbatim — one crypto implementation, KAT-gated there):
Each database has its OWN random 256-bit DEK (the SQLCipher page key), minted once at first touch and NEVER changed, so ciphertext pages are never rewritten.
Each database also gets a random 128-bit FILE ID, stored in the clear at the head of its <db>.dek sidecar. The KEK derives from the OWNER and that id, never from the file path:
KEK = HKDF-SHA256(masterKey, lp(type) || lp(owner || "/" || hex(fileID)))
for a tenant store, and lp("global") || lp(hex(fileID)) for the platform partition, which has no owner. Because the path is absent, moving the data dir or changing CLOUD_DATA_DIR can never change a KEK or brick a store; because the owner is present, a {db,.dek} pair carried into another tenant's directory fails to unwrap rather than opening. RFC-5869 HKDF via x/crypto/hkdf — NOT luxfi/crypto/kdf (a QZMQ KeySchedule, not generic HKDF; using it would brick every store).
The DEK is wrapped AES-256-GCM under the KEK, bound to the same id as AAD. Sidecar = fileID(16) || wrapped-DEK. The raw DEK is never written.
Master-key ROTATION rewraps only the sidecar: the DEK and fileID are unchanged, so no page is rewritten and no file can be bricked.
FAIL-SECURE. On an encryption-CAPABLE build (production is CGO + libsqlcipher) a missing master key is FATAL — the data plane refuses to open unencrypted, the same posture the KMS store takes. A key set on a NON-encrypting build is likewise fatal. An encrypted file whose sidecar is missing is refused. A migration whose encrypted copy does not reproduce the source's schema AND per-table content (a rowid-independent multiset hash, not just a row count) leaves the plaintext untouched and errors — the caller (MountAll) fails closed, so cloud never serves a half-migrated data plane.
rewrap.go — the one-time migration "a store's key names its owner" needed and did not ship with.
That change (cek: a store's key names its owner) started deriving a store's KEK from (principal, fileID) instead of fileID alone. Every sidecar written before it was wrapped under Global, so every pre-existing ORG store stopped opening the moment the new derivation landed — reported as
cek: unwrap DEK (wrong master key or corrupt sidecar)
which is true and misleading: the master key is right and the sidecar is intact. Only the identity the KEK derives from moved.
Rewrap moves ONE sidecar forward: unwrap the DEK under the legacy principal, re-wrap the SAME DEK under the store's real owner, write it atomically. The DEK never changes, so the database's pages are untouched — this rewrites the wrapper and nothing else.
It is NOT a compatibility shim. There is exactly one derivation — the owner-bound one — and this walks the old world into it once. A store that already opens under its owner is left alone; a store that opens under NEITHER identity is reported and skipped, never "repaired" by minting a fresh DEK, because a new DEK would answer every future open with plausible garbage instead of an error.
Index ¶
- Variables
- func Encrypting() bool
- func EnsureDevKey() bool
- func Exists(path string) bool
- func Master() []byte
- func Open(p Principal, path string) (*sql.DB, error)
- func Rebind(from, to Principal, path string) error
- func RebindOrgs(dataDir, platformSlug string) (bound, skipped int, errs []error)
- func SetMasterKey(k []byte)
- type Principal
- type Replica
- type RewrapResult
Constants ¶
This section is empty.
Variables ¶
var ErrNotBound = fmt.Errorf("cek: store not bound to the source principal")
ErrNotBound reports that a store did not unwrap under the principal it was asked to move FROM — most often because it is already bound to the target, which makes a re-run a no-op rather than a failure.
var Global = Principal{/* contains filtered or unexported fields */}
Global is the cross-org platform partition: certs, providers, the audit trail, the gateway's own store. Its id is fixed, so a platform store's key derives from the file id alone exactly as it always has — the platform partition is not a tenant and has no owner to bind to.
Functions ¶
func Encrypting ¶
func Encrypting() bool
Encrypting reports whether cek will encrypt at rest (a valid master key is configured). cloud calls this once at boot for the posture log; a false result means resolveMaster errored (no or invalid key) and the first store Open will fail closed rather than write plaintext.
func EnsureDevKey ¶ added in v1.801.231
func EnsureDevKey() bool
EnsureDevKey installs a deterministic development master key when NO key is configured AND the live codec is not linked — i.e. a pure-Go dev/CI build. It lets that build run through the SAME encrypted path as production (per-db DEK, SQLCipher-format envelope) with zero configuration, instead of a divergent plaintext path. It is a NO-OP when a key is already configured (production uses it) or the live codec is linked (a production binary, which MUST supply the real KMS key and fails closed without it). Call once at boot, before the first Open. Returns true when a dev key was installed. Not safe against a concurrent Open — resolveMaster caches on first use, so this must run first.
func Exists ¶ added in v1.801.231
Exists reports whether a store lives at path — the question "has this org/ subsystem been created yet?", asked by every caller that discovers stores by walking the data directory.
os.Stat on the database file alone does NOT answer it. The pure-Go codec holds the database in its envelope and materializes the file on close, so a store that is OPEN right now has only its sidecar on disk; the live codec writes the database in place and has both. A caller that stats only the database therefore sees a store on one build and not the other, and on the pure-Go build skips precisely the stores that are in use. cek mints the sidecar eagerly, before the first byte of database is written, on both codecs — so "database or sidecar" is the marker that holds on every build and at every point in a store's life.
The layout stays cek's own: callers ask this rather than knowing the suffix.
func Master ¶ added in v1.801.299
func Master() []byte
Master returns the resolved data-plane key, or nil when none is configured.
It exists for ONE caller — the credential broker, which hands the same key to every child process in the deployment. That is not a convenience: the children open the SAME encrypted files, so a child that resolved a key of its own would write a store no sibling could read. The broker therefore has to be able to state which key this deployment settled on, including the dev key a keyless build installs for itself.
It resolves (and so memoizes) exactly like Encrypting, and must be called after any SetMasterKey/EnsureDevKey — the same ordering every caller here has.
func Open ¶
Open returns a *sql.DB for the SQLite database at path, encrypted at rest and bound to p. It is the single way a cloud store opens its file.
The principal comes first because it is the question a caller must answer, not one it may forget: pass cek.Global for a platform store, cek.Org(slug) for a tenant's. Passing the wrong one is not a silent mistake — the store will not unwrap.
func Rebind ¶ added in v1.801.307
Rebind moves one store from one owner's key to another's. It exists because the principal became part of the derivation AFTER stores were already on disk: a tenant store written when every file keyed under the platform tag cannot be opened by its owner until its sidecar is rewrapped.
It is an OPERATION, deliberately not a fallback inside Open. A store has one key and Open derives it one way; a second derivation tried on failure would mean every open silently accepts two answers forever, which is the thing that made the old binding unenforceable in the first place. Running this once is a migration. Leaving it in the open path would be a permanent ambiguity.
Only the sidecar changes. The DEK and the fileID are read out under `from` and written straight back under `to`, so not one database page is rewritten and the file itself is never opened — which is why this is safe on a store too large to copy and why a failure cannot corrupt data. The write is atomic (temp + rename), so an interrupted rebind leaves either the old sidecar or the new one, never a torn file.
It is idempotent in the way that matters: a store already bound to `to` fails to unwrap under `from` and returns ErrNotBound, so re-running a completed migration reports "already done" rather than damaging anything.
func RebindOrgs ¶ added in v1.801.307
RebindOrgs binds every per-org store under dataDir to the org that owns it, which is the one migration the principal change requires. It walks {dataDir}/orgs/<slug>/, including the nested projects/<slug>/ level, and rebinds each store from Global to Org(slug).
The reserved platform partition is skipped: it is not a tenant and keys as Global both before and after, so there is nothing to move.
It never stops on one store's failure. A run over a live volume will meet stores that are already bound (a re-run, or a store created after the change) and those are counted as skipped, not errors — the point of the walk is to leave every store bound, and that is a state to converge on rather than a transaction.
func SetMasterKey ¶
func SetMasterKey(k []byte)
SetMasterKey injects the 32-byte master key explicitly (cloud's boot resolves it once from cfg and hands it here), taking precedence over the environment. Call before the first Open. A wrong-length key is ignored so the env path can still apply.
Types ¶
type Principal ¶ added in v1.801.307
type Principal struct {
// contains filtered or unexported fields
}
Principal is WHOSE store this is. It is the first argument to Open because it is part of a store's identity, not a property of its location: the same bytes at the same path belong to exactly one org, and the key says so.
It binds the derivation. Before, every store — platform and per-org alike — derived under the single tag "global", so a store's key knew nothing about its owner and a {db,.dek} pair lifted into another org's directory opened there perfectly well. Confidentiality between orgs still held (each file has its own random DEK under its own KEK), but nothing tied a file to the tenant it belonged to. Now the owner is in the HKDF info and in the GCM AAD, so a store carried across a tenant boundary fails to unwrap instead of opening.
func Org ¶ added in v1.801.307
Org binds a store to one tenant. slug MUST be the value SanitizeOrg produced (the injective slugger OrgDB already folds every org through), so two distinct orgs can never derive the same key.
type Replica ¶ added in v1.801.350
type Replica interface {
// Has reports whether a replica exists for this store. It is what makes
// hydrate-on-open safe to call unconditionally: a first-ever boot finds
// nothing and proceeds to create an empty store, which is correct and is
// what `-if-replica-exists` was approximating from outside.
Has(path string) (bool, error)
// Hydrate materializes the store at path from its replica. It runs only
// when Exists(path) is false, so it can never overwrite live data — the
// property the initContainer got right and which must not be lost.
Hydrate(path string) error
// Follow streams subsequent commits out. It returns once following has
// started, not when the replica is caught up: a store that refuses to open
// until its backup is current is a store that will not open during an S3
// incident, which trades a durability risk for an availability one.
Follow(path string) (stop func() error, err error)
}
replication.go — where SQLite→S3 replication belongs, and why it is not a sidecar.
DESIGN ONLY. Nothing here is wired yet; this file marks the seam and records the decision so the next change lands in the right place instead of adding a sixth object to every stateful pod.
What the sidecar costs ¶
Today each replicated service carries four objects and a key: a `replicate` container, a generated ConfigMap, a restore initContainer, and its own age keypair. All of it exists for one reason — `replicate` is a separate binary watching a file it does not own, so the file has to be described to it.
On 2026-07-29 that arrangement produced four independent outages in one day: a misindented age stanza replicate refused outright, an age/plaintext mismatch between config and bucket, a service whose data directory was not mounted at all, and a restore path that had never once run successfully. The last is the instructive one: restore only executes `-if-db-not-exists`, so while the local file happened to exist it was never exercised. The backups were configured, not current, and not restorable — and nothing said so until a volume was lost.
Why it belongs here ¶
Replication is a property of the STORE, not of a process watching it. Open is already "the single way a cloud store opens its file" and Exists already answers "is there a store here" — which is the entire question the initContainer was shelling out to ask.
Native, the lifecycle collapses into Open:
if !Exists(path) && replica.Has(path) { replica.Hydrate(path) }
db := open(path)
replica.Follow(db) // every commit streams out
Restore stops being a lifecycle stage and becomes what Open does. The ConfigMap, the initContainer, the second container and the ordering between them all disappear — there is nothing left to describe to a peer, because there is no peer.
One key, not two ¶
This is the sharpest argument and the reason the age keypair should not be migrated to KMS but DELETED. cek already holds a master key (CLOUD_KMS_MASTER_KEY_REF) and resolveMaster is explicit that there is no plaintext-at-rest mode: a store is keyed or it does not open. The age identity is a SECOND key system encrypting the SAME data — with its own per-service secret, its own failure modes, and no rotation story at all, because an age identity cannot be rotated after the fact. Lose it and every replica under it is unreadable.
A replica written by this layer is encrypted under the key the process is already holding. One store, one key, one encryption path.
Transport ¶
S3 over ZAP on the internal plane, like every other cross-app call — see cloud/rpc.go. That also retires ghcr.io/hanzoai/replicate as a shipped image.
The interface this wants ¶
Small on purpose: three verbs, all about bytes at a path, none about containers or lifecycle.
type RewrapResult ¶ added in v1.801.350
type RewrapResult struct {
Path string // the database path whose sidecar was examined
Owner string // the principal it now derives from
Rewrapped bool // true when this call moved it forward
Already bool // true when it already opened under its owner
Err error // non-nil when it opened under neither identity
}
RewrapResult is the honest per-store outcome.
func InspectOrgs ¶ added in v1.801.350
func InspectOrgs(root string) ([]RewrapResult, error)
InspectOrgs is RewrapOrgs' read-only twin: it reports what each store would do without writing a byte. The dry run and the real run therefore share one walk and one decision — a dry run that used different logic would be a different program telling you about this one.
func Rewrap ¶ added in v1.801.350
func Rewrap(owner Principal, dbPath string) RewrapResult
Rewrap migrates one sidecar from the legacy (Global) derivation to owner-bound.
Order matters and is deliberate: try the OWNER first. A store already migrated must not be touched, and trying the legacy identity first on an already-correct store would fail and look like corruption.
func RewrapOrgs ¶ added in v1.801.350
func RewrapOrgs(root string) ([]RewrapResult, error)
RewrapOrgs walks every per-org store under root and migrates each to its owner. The subdirectory NAME is the slug OrgDB folded through on the way in, so the owner is read from the layout rather than guessed.