Disaster Recovery — Restore Drill

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).

Definitions

Scope

In scope for the drill:

Out of scope:

Procedure

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Phase D — decrypt.

  6. 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, …).

  7. 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.

  8. Tear down. Delete the namespace + PVC. Confirm no residual snapshot bytes remain outside the customer's bucket.

  9. 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.

Success criteria

A drill is passing when all of the following hold:

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.

Cadence + evidence

Related