Skip to content

DL-328

The gateway_credentials value payload is encrypted at rest application-side (AES-256-GCM envelope; value_ciphertext+value_nonce+key_version columns, no plaintext column ever; AAD binds the stable row identity so ciphertexts are not row-portable), covering api_key and OAuth rows on ONE seal/open path (D7). A 256-bit master key is auto-provisioned zero-human-step into an operator-chosen WRITABLE SecretSpec provider (self-hosted default age:// — encrypted-at-rest, headless; a cloud store or Vault/OpenBao where present) under a Postgres advisory lock with every-boot read-back verify (+ a key-fingerprint tripwire). The master key and the existing server secrets (primary + reviewer App PEM, webhook, and the three Linear secrets) live in a NEW physically separate server_secrets store (mechanism C1: names-only table, bucket-A infrastructure — no tenant_id, RLS off, its own GRANT to compass_app/compass_system; no delivery/kind columns; a SECOND SpecResolver instance over it on the shared profile; an admin-gated SetServerSecret/DeleteServerSecret RPC) so server secrets are STRUCTURALLY undeliverable to agent containers — the container resolver’s manifest never contains their names. C1 splits the DECLARATION registries, not (by default) the shared provider keyspace, so F1 is a STRUCTURAL reserved-prefix partition enforced on BOTH the admin RPC AND the authenticatedOpen user SetSecret/DeleteSecret path: every server-secret name carries a reserved prefix (SERVER_ for the six forge secrets, GATEWAY_CREDENTIALS_ for the master-key family), the admin door REQUIRES one and the user door REJECTS one, so the two keyspaces are disjoint by name and no name declared through either guarded door is ever live in both tables in either order (against the wiped pre-production baseline — the guards are prospective, and the DB is wiped and re-declared, so no legacy reserved-prefixed row pre-exists) — a pure string check, no cross-table membership SELECT and so no cross-tenant RLS-visibility question. The table split alone is necessary but not sufficient for the structural claim; the prefix partition is what makes it sufficient. No proto ENUM change and no FetchSecrets filter — the admin RPC adds two additive SecretsService methods (supersedes the earlier SERVER_ONLY-delivery-kind fold). The six forge secret NAMES are declared into server_secrets at boot from the resolved forge config (on the ordinary compass_app path — server_secrets is bucket-A, no RLS, so no reconcile and no BYPASSRLS), gated on the same predicates that enable the forge lanes; their VALUES live in the operator-chosen writable provider (F2), populated by the operator directly (e.g. the age:// file) or through the compass server-secret CLI / admin SetServerSecret RPC for rotation. Because the provider is populated BEFORE the server runs, the six values are present at first boot with NO running server required to bootstrap them (Matt-ruled — the server boots with the secrets set through the provider/config, not via a running-server RPC); a configured App whose forge value is absent from the provider is a clean STATIC deploy-time startup error (validateForgeSecret), fixed by populating the provider and rebooting. The pre-production deployment is wiped and the six values re-provisioned under prefixed names (name-keyed provider, so a provider write), not migrated in place, closing the pre-existing inject-all exposure. The age:// default requires bumping BOTH the secretspec-go SDK (read path) and the secretspec CLI binary (write path, shelled by resolver.Set) to >= 0.17 with the age build feature (repo pins v0.15.0), the CLI pinned via WithCLI into the Server closure — a gating prerequisite PR for the self-hosted age:// path only; a cloud/Vault deployment needs no bump. Managed-plane KMS is a provider-URI deployment config, not a code fork; rotation is deferred (single live key, key_version landed at v1, bounded key-loss blast radius — credentials are re-obtainable); per-tenant at-rest isolation + gateway-topology exposure is a tracked follow-up

Status: Superseded by DL-355 (Matt, 2026-09-11)

Record: ../../server/compass-gateway-credentials-at-rest-encryption.md#d1–application-layer-aes-256-gcm-envelope-encryption