Owner: Security + SRE. Cadence: quarterly. Required evidence for SOC 2 CC7.5 + A1.3.
This is the procedure for a Managed-tier restore drill — proving that the
encrypted off-cluster backups recorded by
src/control-plane/services/backup-mirror.js
can actually be turned back into a queryable database within the published
RTO. It pairs with the incident response plan (which
covers the escalation path when a drill fails) and the
audit-trail architecture (which covers the
forensic evidence captured along the way).
docs/compliance/dr-drills/<YYYY-Qn>.md.In scope for the drill:
key_source: "kms"), AES-256-GCM authenticate + decrypt, untar into a
scratch PVC, read-only Postgres bring-up, ledger sanity check, tear-down.Out of scope:
The first four phases are automatable via the npm run dr:drill helper
(see scripts/dr-drill.js). The remaining phases
require a real Kubernetes namespace.
Pick a tenant. From the operator workstation, list tenants with a
configured backup_target. Pick one at random whose most recent backup
is < 30 days old. Record the slug + plan in the drill log.
Phase A — manifest fetch. GET /cp/backups (owner session). Confirm
the most recent row exists and that key_fingerprint matches what the
org currently has configured. Capture created_at → this is the
observed RPO for the drill.
Phase B — restore-token mint. POST /cp/backups/:id/restore-token.
Confirm the token expires_at is in the future. Start the RTO clock here.
Phase C — cipher-byte download. Pull the cipher bytes from the configured transport (s3 / gcs / azure / stub). Time + record bytes transferred. Failure here typically means the bucket policy drifted or the transport credentials rolled.
Phase D — decrypt.
key_source: "kms" — call the configured KMS provider's Decrypt
on the wrapped DEK. Failure here is the realistic failure mode in
production: customer-side KMS IAM drift or MeterBox IAM role drift.
Treat any KMS Decrypt failure as Sev-1 — see the incident response
plan — because a live restore would also fail.key_source: "static" — confirm the in-cluster secret matches the
stored key_fingerprint.Phase E — untar. Untar the plaintext into a scratch PVC mounted in
dr-drill-<yyyymmdd> namespace. Confirm the file tree shape matches the
bundled-Postgres layout (base/, pg_wal/, postgresql.conf, …).
Phase F — read-only Postgres + ledger sanity. Start a postgres:16
pod with the scratch PVC mounted at /var/lib/postgresql/data. Once
ready, pick a recent ledger_entries row from the live cluster, query
the same (customer_id, timestamp) in the restored database, confirm
the cost + token totals match. Stop the RTO clock when the row
matches. This is the measured RTO.
Tear down. Delete the namespace + PVC. Confirm no residual snapshot bytes remain outside the customer's bucket.
Log the drill under docs/compliance/dr-drills/<YYYY-Qn>.md using
the template in template.md. The structured
output from npm run dr:drill covers phases A–D verbatim; phases E–H
are filled by the operator running the destination side.
A drill is passing when all of the following hold:
key_source: "kms").Sev-1 follow-up was opened during the drill.A drill is failing when any of the above does not hold. A failed drill opens an incident under incident-response.md at the severity matched to what would happen if this were a real outage — usually Sev-1.
tested RPO/RTO restore drill) and A1.3 (restore drill with measured RPO/RTO).backup-mirror.* audit events surface in the org's append-only log.