Case Study · Disaster Recovery · AWS · PostgreSQL

DR for a 2,000-Engineer SaaS on AWS

A practical, step-by-step disaster-recovery plan for NimbusPay — a fintech running Aurora + RDS PostgreSQL, with a locked-down account (2 admins among 2,000 engineers), an immutable cross-account backup vault, and runbooks for every disaster that actually happens.

A worked application of the PostgreSQL Disaster Recovery playbook — swap in your own numbers

0The company & the premise

Meet NimbusPay: a payments SaaS, ~2,000 engineers, running on AWS in us-east-1 with us-west-2 as the DR region. The AWS org is tightly held — only 2 DevOps/platform admins have elevated access; the other ~1,998 engineers deploy through CI/CD and never touch the console. Their starting belief: "our account can't really be hacked — almost nobody can get in."

⚠️ Addressing the premise: "only 2 people have access, so it can't be hacked"

Tight access is genuinely excellent — it makes account compromise unlikely, and it lets us weight this whole plan toward NimbusPay's far more probable disasters (human error and region outages). But DR must treat compromise as possible anyway, because "only 2 people" is not the same as "no way in":

  • An admin's long-lived access key leaks in a repo, a laptop, or a .env — the #1 cloud breach cause.
  • An admin gets phished (or their SSO session token is stolen) — 2 targets is a smaller haystack, not an impossible one.
  • A CI/CD role that 1,998 engineers can trigger is over-permissioned and can delete data — the "2 admins" fact is irrelevant to it.
  • A malicious or coerced insider among the 2 (or anyone who can assume their role) acts deliberately.
  • A supply-chain compromise (a poisoned dependency in the deploy pipeline) runs with pipeline privileges.

So the plan does two things at once: make compromise genuinely hard (Section 4), and make sure NimbusPay survives it if it happens anyway — via one immutable, cross-account backup copy the production credentials cannot delete (Sections 3–4, Runbook D). That copy costs a few dollars a month; skipping it is what turns a bad day into "pay the ransom."

🧭
How to read thisSections 1–4 are the design (classify the data, draw the architecture, set up backups, harden the account). Section 5 is the runbooks — the exact steps to run when a specific disaster hits. Sections 6–7 keep it honest with drills and a scorecard. Every command is illustrative; adapt identifiers to your environment.

1Classify — RPO/RTO per data tier

DR starts by refusing to treat all data the same. NimbusPay splits its PostgreSQL into three tiers, each with its own targets and therefore its own (very different) budget. This single table drives every later decision.

Tier 0 · critical
Payments ledger
Engine: Aurora PostgreSQL
RPO: ≤ 5 seconds
RTO: ≤ 5 minutes
Pattern: Global Database (active-passive, 2 regions)
Tier 1 · core
Core app DB
Engine: RDS PostgreSQL
RPO: ≤ 1 minute
RTO: ≤ 30 minutes
Pattern: Multi-AZ + cross-region read replica (warm standby)
Tier 2 · internal
Analytics / internal
Engine: RDS PostgreSQL
RPO: ≤ 24 hours
RTO: ≤ 4 hours
Pattern: Single-AZ + PITR + nightly snapshot (backup & restore)
TierDataRPORTODR patternMonthly cost weight
0Payments ledger (money movement)≤ 5s≤ 5mAurora Global Database, 2 regions$$$$
1Core app (users, orders, config)≤ 1m≤ 30mRDS Multi-AZ + cross-region replica$$$
2Analytics, internal tools, logs≤ 24h≤ 4hRDS single-AZ + PITR + nightly snapshot$
Gotcha: the mistake here is a single company-wide RPO/RTO — either you overspend making analytics active-active, or you underspend and put the ledger on nightly restore. Tiering is what makes DR affordable: NimbusPay pays for near-zero data loss only on the money that can't be lost, and lets the internal dashboards tolerate a nightly restore.

2The architecture — two regions, two accounts

The two ideas that shape everything: a second region (survives a regional outage) and a second, isolated account (survives a compromise of the production account). Backups land in both.

NimbusPay DR topology
  ┌─ AWS ORG ──────────────────────────────────────────────────────────────┐
  │                                                                          │
  │  APP ACCOUNT  (workloads; CI/CD deploys here)                            │
  │  ┌──────────────── us-east-1 (PRIMARY) ────────────┐   us-west-2 (DR)    │
  │  │ Tier0  Aurora ledger  (writer) ═══Global DB═════╪══► Aurora secondary │
  │  │ Tier1  RDS core  Multi-AZ (a+b) ──async repl────╪══► RDS read replica │
  │  │ Tier2  RDS analytics (single-AZ) + PITR         │                     │
  │  └────────────────────────┬────────────────────────┘                     │
  │                daily snapshots + continuous backup                        │
  │                           ▼                                               │
  │  BACKUP ACCOUNT  (no standing human access · separate credentials)        │
  │  ┌────────────────────────────────────────────────────────────────────┐ │
  │  │ AWS Backup vault  —  Vault Lock (WORM, immutable)                    │ │
  │  │ copies in us-east-1 AND us-west-2 · own KMS keys                     │ │
  │  │ PROD credentials CANNOT delete these  ← survives ransomware (Runbook D)│ │
  │  └────────────────────────────────────────────────────────────────────┘ │
  └──────────────────────────────────────────────────────────────────────────┘
   region loss → fail over to us-west-2   ·   account compromise → restore from BACKUP ACCOUNT
🧱
Why two accounts, not just two regionsA second region saves you from a regional outage — but if an attacker (or a bad admin action) compromises the production account, they can delete the database and its snapshots in that account, in every region, in one sweep. The second account — with its own credentials that production never holds, and a Vault Lock that makes backups un-deletable — is the copy that survives that. Two regions and two accounts defend two different disasters.

The same architecture, three ways

The ASCII map above is the quick glance. Here are two more lenses: an entity diagram of how the DR objects relate, and a sequence diagram of the region-failover choreography (Runbook C) as it actually plays out.

Entity diagram — the DR object model
erDiagram ORG ||--o{ ACCOUNT : contains ACCOUNT ||--o{ REGION : "operates in" REGION ||--o{ DB_CLUSTER : hosts DB_CLUSTER ||--|{ DB_INSTANCE : runs GLOBAL_CLUSTER ||--o{ DB_CLUSTER : spans DB_CLUSTER ||--o{ RECOVERY_POINT : "backed up as" BACKUP_VAULT ||--o{ RECOVERY_POINT : stores RECOVERY_POINT }o--|| KMS_KEY : "encrypted by" ACCOUNT { string account_id string type "app or backup" } REGION { string name "us-east-1 or us-west-2" string role "primary or DR" } DB_CLUSTER { string id string engine "aurora or rds-postgres" string tier "0 or 1 or 2" } BACKUP_VAULT { string name string lock "compliance WORM" int min_retention_days } RECOVERY_POINT { string arn datetime created_at bool immutable } KMS_KEY { string arn string region }
Renders with Mermaid. If your network blocks the CDN, the ASCII topology above and the runbooks carry the same information.
Sequence diagram — Runbook C, region failover
sequenceDiagram autonumber participant Mon as Monitoring participant Eng as On-call participant Appr as Approver participant Aurora as Aurora Global ledger participant RDS as RDS replica core participant App as Applications Mon->>Eng: ALARM — us-east-1 unreachable Eng->>Appr: confirm regional outage, request cutover Appr-->>Eng: approve failover Eng->>Aurora: failover-global-cluster, promote us-west-2 Aurora-->>Eng: us-west-2 is now the writer Eng->>RDS: promote-read-replica core-west RDS-->>Eng: standalone primary in us-west-2 Eng->>App: flip Route53 + DB secret, recycle pools App->>Aurora: writes now land in us-west-2 App->>RDS: writes now land in us-west-2 Eng->>App: run smoke tests + ledger checks App-->>Eng: healthy Eng->>Mon: declare recovered, update status page Note over Eng,App: RTO ~15 min · RPO ~seconds ledger / ≤1 min core
Renders with Mermaid. The same flow is in Runbook C's timeline and numbered steps.
🖼️
Three views, one systemThe ASCII map is for a glance in a terminal or a text runbook; the entity diagram shows what relates to what — one org → two accounts → regions → clusters → instances, and one vault → many immutable recovery points; the sequence diagram shows who does what, in order, during a failover. Same architecture, three lenses.

3Backup & replication design — per tier

Now build each tier to its targets. The rule from the playbook holds throughout: replication buys RTO, backups buy RPO and undo mistakes — every tier needs both, and one immutable copy lives in the backup account.

Tier 0 — Aurora Global Database (the ledger)

A single Aurora cluster spanning us-east-1 (writer) and us-west-2 (read-only secondary), replicated at the storage layer with typically sub-second lag. Continuous backup gives PITR; a secondary region gives seconds-RPO failover.

Aurora Global Database — span two regions
# wrap the primary cluster in a global cluster
$ aws rds create-global-cluster --global-cluster-identifier nimbus-ledger-global \
    --source-db-cluster-identifier arn:aws:rds:us-east-1:APP:cluster:nimbus-ledger

# attach a secondary region (read-only, promotable in ~1 min)
$ aws rds create-db-cluster --region us-west-2 \
    --db-cluster-identifier nimbus-ledger-west \
    --global-cluster-identifier nimbus-ledger-global --engine aurora-postgresql
$ aws rds create-db-instance --region us-west-2 \
    --db-instance-identifier nimbus-ledger-west-1 \
    --db-cluster-identifier nimbus-ledger-west --engine aurora-postgresql \
    --db-instance-class db.r6g.xlarge

# watch cross-region replication lag = the ledger's real RPO (alarm if > 5s)
$ aws cloudwatch put-metric-alarm --alarm-name ledger-global-lag \
    --namespace AWS/RDS --metric-name AuroraGlobalDBReplicationLag \
    --threshold 5000 --comparison-operator GreaterThanThreshold ...  # ms

Tier 1 — RDS Multi-AZ + cross-region read replica (core app)

Multi-AZ gives automatic in-region failover (HA); a cross-region read replica in us-west-2 is the warm standby you promote if the whole region is lost. Automated backups (14-day retention) provide PITR for mistakes.

RDS core — Multi-AZ, PITR window, and a warm DR replica
# in-region HA + a 14-day PITR window
$ aws rds modify-db-instance --db-instance-identifier nimbus-core \
    --multi-az --backup-retention-period 14 --apply-immediately

# cross-region read replica = warm standby for a regional failover
$ aws rds create-db-instance-read-replica \
    --db-instance-identifier nimbus-core-west \
    --source-db-instance-identifier arn:aws:rds:us-east-1:APP:db:nimbus-core \
    --region us-west-2 --db-instance-class db.r6g.xlarge   # size for FULL load

# alarm on replica lag = the core app's regional RPO (alarm if > 60s)
$ aws cloudwatch put-metric-alarm --alarm-name core-replica-lag \
    --namespace AWS/RDS --metric-name ReplicaLag --threshold 60 ...

Tier 2 — PITR + nightly snapshot (analytics)

No standby needed. Automated backups give a PITR window; a nightly snapshot is copied out to the backup account. On disaster, restore into a new instance — hours of RTO is acceptable here.

The immutable copy — AWS Backup, cross-account, Vault Lock

This is the linchpin that survives ransomware and account compromise. A central AWS Backup plan copies every tier's recovery points into a vault in the separate backup account, in both regions, and Vault Lock (compliance mode) makes them immutable — nobody, not even the backup account's own admin, can delete them before retention expires.

Central backups into an immutable, cross-account vault
# (in the BACKUP account) a locked vault — WORM, un-deletable within retention
$ aws backup create-backup-vault --backup-vault-name nimbus-dr-vault \
    --encryption-key-arn arn:aws:kms:us-east-1:BACKUP:key/dr-key
$ aws backup put-backup-vault-lock-configuration \
    --backup-vault-name nimbus-dr-vault \
    --min-retention-days 35 --max-retention-days 3650 --changeable-for-days 3
#   compliance-mode lock: after the 3-day window, the lock ITSELF can't be removed

# (org-level) a backup plan that copies each tier's recovery points
#   cross-ACCOUNT (into nimbus-dr-vault) AND cross-REGION (us-west-2)
$ aws backup create-backup-plan --backup-plan file://plan.json
#   rules: ledger hourly (35d) · core every 6h (35d) · analytics daily (35d)
#   each rule: CopyActions → [ backup-account vault (us-east-1), (us-west-2) ]
🔐
The one copy that always survivesEverything else — the Aurora secondary, the RDS replica, the automated snapshots — lives in the production account, so a full account compromise can reach them. The Vault-Locked copy in the backup account is the exception: production has no credentials there, and compliance-mode Vault Lock means even the backup account can't delete it early. It's the difference between Runbook D being "restore and move on" versus "there is nothing left." It costs a few dollars a month.
Gotcha: AWS Backup Vault Lock has two modes — governance (a privileged role can still remove the lock) and compliance (nobody can, ever, until retention expires). For a ransomware defense you want compliance mode — governance mode is defeated by the very admin-level compromise you're defending against. Set a short changeable-for-days grace window to catch config mistakes, then it's truly immutable.

4Hardening the account — the real answer to "can it be hacked?"

NimbusPay's instinct (few people = safe) is half the answer. The other half is making the access those few people have genuinely hard to steal or misuse, and ensuring the blast radius is bounded when something slips. Here's the design that earns the confidence they already feel.

Identity — remove the things that leak

The most common cloud breach is a leaked long-lived key, so NimbusPay has none. Humans log in through IAM Identity Center (SSO) with mandatory phishing-resistant MFA and short sessions; there are no IAM users and no long-lived access keys anywhere.

No standing keys; short, MFA-backed, federated sessions
# humans: federated SSO, hardware/passkey MFA, 1-hour sessions — nothing to leak
#   (IAM Identity Center; permission sets, not IAM users)

# machines/CI: short-lived roles via OIDC — NO stored AWS keys in CI
#   GitHub Actions assumes a role scoped to DEPLOY, with NO snapshot/db-delete
$ aws iam create-role --role-name ci-deploy \
    --assume-role-policy-document file://github-oidc-trust.json
#   trust: token.actions.githubusercontent.com, locked to the repo + branch
#   permissions: ecs/eks deploy + read; explicitly DENY rds:Delete*, backup:Delete*

# find and kill any legacy long-lived keys (they shouldn't exist)
$ aws iam list-access-keys --user-name ANY   # target: zero human/CI access keys

Guardrails — make destruction structurally impossible

Even a compromised admin shouldn't be able to erase the backups. Service Control Policies (SCPs) at the org level deny the destructive actions outright — a permission no principal in the app account can be granted, no matter what.

Org SCP — nobody in the app account can delete backups
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyBackupDestruction",
    "Effect": "Deny",
    "Action": [
      "rds:DeleteDBClusterSnapshot",
      "rds:DeleteDBSnapshot",
      "backup:DeleteRecoveryPoint",
      "backup:DeleteBackupVault",
      "backup:PutBackupVaultLockConfiguration",
      "kms:ScheduleKeyDeletion",
      "kms:DisableKey"
    ],
    "Resource": "*"
  }]
}

The backup account is separate and has no standing human access at all — access requires a logged, approved break-glass elevation. So even total compromise of the app account cannot reach the locked vault.

Detection & break-glass

CloudTrail (org-wide, to a locked S3 bucket) and GuardDuty watch for the signals of compromise — anomalous API calls, snapshot deletes attempts, logins from new geographies, root usage. A documented break-glass role (rarely used, heavily alarmed, MFA + approval) exists for the genuine emergency.

Watch for the compromise you hope never comes
# org-wide audit trail to a locked bucket in the backup account
$ aws cloudtrail create-trail --name org-trail --is-organization-trail \
    --s3-bucket-name nimbus-audit-locked --is-multi-region-trail

# threat detection on the API plane
$ aws guardduty create-detector --enable

# alarm on the actions that mean "someone is trying to destroy our recovery"
#   any Delete* on snapshots/vaults · root login · MFA disabled · new-region API
$ aws cloudwatch put-metric-alarm --alarm-name snapshot-delete-attempt ...  # → page
Gotcha: the belief "only 2 people have access" quietly ignores the CI/CD role that all 2,000 engineers can trigger. If that deploy role can run migrations or touch the database, then effectively 2,000 people (plus anyone who compromises the pipeline) can harm your data — the "2 admins" number is a comforting distraction. Scope the CI role to deploy-and-read only, deny it destructive database and backup actions, and require a separate, human-approved, MFA-gated path for anything that can delete data.

5The runbooks — step by step

Five disasters, five procedures. Each starts with the trigger, then the exact ordered steps. In a real incident the on-call engineer opens the matching runbook and follows it verbatim — no improvising. Expand each one.

AHuman error — a bad DELETE or migrationmost common

Trigger: a migration or query damaged/deleted data (e.g. DELETE without a WHERE) at a known time. Replicas already copied it — so this is a backup problem (PITR), not a failover.

  1. Stop the bleeding. Kill the offending job; put the affected service in maintenance if writes are still corrupting data. Pin the exact time of harm from CloudTrail / the app audit log — e.g. 2026-07-15 14:07:03Z.
  2. Restore to a NEW instance, just before the harm. Core (RDS): aws rds restore-db-instance-to-point-in-time --source-db-instance-identifier nimbus-core --target-db-instance-identifier nimbus-core-fix --restore-time 2026-07-15T14:07:00Z. Ledger (Aurora): aws rds backtrack-db-cluster if within the Backtrack window, else restore-db-cluster-to-point-in-time into a new cluster.
  3. Validate the restored copy before touching production — row counts, checksums, a smoke test. Confirm the damage is absent and legitimate data is present.
  4. Cut over. Repoint the app to the restored instance (swap the endpoint in the DB-URL secret / rename instances), and recycle connection pools. Keep the damaged instance running for forensics.
  5. Reconcile & close. If good writes happened between the target time and the cutover, replay them from the app's event log where possible; otherwise accept that small RPO gap. Decommission the damaged instance after forensics; run a blameless post-mortem.
Key point: never recover onto the damaged instance — always restore to a new one and validate first. The failover copies (Aurora secondary, RDS replica) are useless here; they faithfully copied the bad write. Only point-in-time backups undo it.
BAZ or instance failure (in-region)mostly automatic

Trigger: a database instance or an entire Availability Zone fails in us-east-1. This is what Multi-AZ and Aurora are built for — your job is mostly to verify, not to act.

  1. Let it fail over automatically. RDS Multi-AZ promotes the standby in the other AZ (~60–120s); Aurora fails to another AZ replica (~30s). No manual promotion needed.
  2. Verify the endpoint moved. Confirm the RDS/cluster endpoint now resolves to the healthy AZ and the database accepts writes: psql -h nimbus-core... -c "SELECT pg_is_in_recovery();"f (it's a writer).
  3. Recycle clingy connections. If app pools still dial the old host, restart the app tasks or let pool max-lifetime recycle them. RDS Proxy (if in front) makes this transparent.
  4. Confirm the standby rebuilds. Check that a new standby/replica is being provisioned so you're not left single-AZ. No data recovery is needed — the standby was synchronous.
The easy oneThis is the disaster your architecture handles by itself. If a game day shows the app limping for minutes after an AZ failover, the problem is almost always connection pooling / DNS caching (Runbook B step 3), not the database — fix it there.
CFull region outage — us-east-1 is downthe big one

Trigger: us-east-1 is impaired region-wide; the primary and its Multi-AZ standby are both unreachable. You fail the whole stack over to us-west-2.

Target timeline (rehearsed)
  T+0   detect (Health + our alarms)        T+8   repoint apps → us-west-2
  T+2   decide + approver authorizes         T+12  verify (smoke + ledger checks)
  T+4   fail over Aurora Global (ledger)      T+13  declare recovered + comms
  T+6   promote RDS replica (core)            later rebuild us-east-1 as secondary
  target: RTO ~15 min · RPO: ledger ~seconds, core ≤ ~1 min (last replica lag)
  1. Decide. Confirm it's regional (AWS Health dashboard), that failover criteria are met, and that the replica lag at outage time was within RPO. One named approver authorizes the cutover.
  2. Fail over Tier 0 (ledger). aws rds failover-global-cluster --global-cluster-identifier nimbus-ledger-global --target-db-cluster-identifier arn:aws:rds:us-west-2:APP:cluster:nimbus-ledger-west → the us-west-2 secondary becomes the writer.
  3. Promote Tier 1 (core). aws rds promote-read-replica --db-instance-identifier nimbus-core-west → the cross-region replica becomes a standalone read/write primary.
  4. Fence the old region. Ensure nothing in us-east-1 can still take writes when it returns (it's down now; do not let it auto-rejoin as a writer later) — prevents split-brain.
  5. Repoint the apps. Flip the Route 53 records (low TTL) and update the DB-URL secret to the us-west-2 endpoints; recycle app connection pools. This step, not the DB promotion, is usually where the RTO goes.
  6. Verify, then declare. Run smoke tests and ledger balance/consistency checks. Only after they pass, update the status page and notify stakeholders (and start any regulatory notify clock).
  7. Recover the region later. When us-east-1 returns, rebuild it as the new secondary and re-establish replication. Do not fail back during peak load; schedule it, and treat it as its own planned change.
Gotcha: Tier 2 (analytics) has no warm standby — during a regional outage it's simply restored from the vault into us-west-2 when capacity allows (RTO hours, which its tier permits). Don't spend the first 15 minutes on analytics; the ledger and core app come first, exactly as the tiers prescribed.
DAccount compromise / ransomwarethe premise, handled

Trigger: the app account is compromised — snapshots being deleted, GuardDuty firing, or an extortion demand. Assume everything in the app account is destroyed or untrustworthy. This is the scenario NimbusPay thought impossible — and the one the backup account exists for.

  1. Contain. This is a security incident first: revoke/disable the compromised identities and kill their sessions, rotate credentials, and pull in security + legal. Do not negotiate; do not trust anything in the app account.
  2. Reach for the untouchable copy. The Vault-Locked recovery points in the backup account are intact — production's credentials were never valid there, and compliance-mode Vault Lock means even the backup account couldn't delete them. The attacker could not reach them.
  3. Restore into clean infrastructure. In a fresh/clean account and region, restore each tier from the vault: aws backup start-restore-job --recovery-point-arn arn:aws:backup:us-west-2:BACKUP:recovery-point:... --iam-role-arn ... --metadata ... — ledger, then core, then analytics.
  4. Validate each restored database (integrity, recent-ness against your RPO), then stand the apps up against the clean environment.
  5. Rotate everything. Every secret, key, and credential is now suspect — rotate all of them. Rebuild the app account's infrastructure from IaC rather than "cleaning" the compromised one.
  6. Close the door & report. Forensics to find the entry vector (leaked key? phished admin? CI role?), fix it, and complete breach notifications per your obligations.
The payoff: without the cross-account, compliance-mode Vault-Locked copy, this runbook has no step 2 — every backup lived where the attacker could delete it, and the honest options become "pay" or "we've lost the data." That one immutable copy, costing a few dollars a month, is the entire difference. This is why "only 2 people have access" is a reason to feel good, not a reason to skip it.
ESilent data corruptionthe sneaky one

Trigger: checksum failures or an anomaly check reveal data has been silently corrupting — and it has already replicated to standbys and into recent backups.

  1. Find when it started. Use logs, data_checksums failures, and anomaly signals to bound the start time of corruption. Assume replicas and the most recent backups are already poisoned.
  2. Pick a PITR target BEFORE the corruption began — not the latest backup. This is why retaining enough WAL / a long-enough PITR window matters: the clean point may be further back than you'd like.
  3. Restore to a new instance at that clean target and validate it's actually corruption-free (the checks that first caught it should now pass).
  4. Reconcile the legitimate writes made after the clean point where you can (replay from the app event log); otherwise accept the RPO gap — a known, bounded loss beats keeping corrupt data.
  5. Fix the cause first, then cut over. Corruption is often a bug or bad hardware; resuming without fixing it just re-corrupts the fresh copy.
Gotcha: corruption is the case that punishes short retention. If you only keep 3 days of PITR but the corruption started 5 days ago, you have no clean point — you'd restore corrupt data. Detection latency (Runbook E step 1) directly adds to loss, which is why data_checksums and anomaly alerts are worth the small overhead.

6Keeping it real — the drill calendar

A DR plan is a hypothesis until it's exercised. NimbusPay puts these on the calendar and treats a failed drill as a real incident to fix.

CadenceExerciseWhat it proves
MonthlyRestore a Tier-2 snapshot from the vault into a scratch cluster; validate; time itBackups actually restore; measures real RTO; catches KMS/permission gaps (Runbook A/E)
QuarterlyRegional game day: fail Tier-1 core to us-west-2 and run Runbook C end to endPromotion + app repoint work; measures RTO/RPO; surfaces pool/DNS issues
QuarterlyAurora Global managed failover test for the ledgerTier-0 cross-region failover works within target; app follows the writer
Semi-annualAccount-compromise tabletop + restore from the vault into a clean account (Runbook D)The immutable copy is truly reachable and restorable; roles/access are right
AnnualFull DR audit: measured vs target RPO/RTO, SCPs, Vault Lock config, access reviewThe whole plan is current; the runbooks aren't stale; nobody's crept in privileges
🗓️
The rule that keeps DR honestLet the on-call engineer (not the DR expert) run the drill from the written runbook. You're testing the plan, not the person — if only one hero can recover the company, that's a DR risk, not a DR plan.

7Readiness scorecard

Ten yes/no questions. NimbusPay can answer "yes" to all ten — and that, not "only 2 people have access," is what actually makes them safe. Score your own setup against it.

Every data tier has a written RPO and RTO, owned by the business.
Backups exist in a second region AND a second account.
At least one copy is immutable (compliance-mode Vault Lock / Object Lock).
Production credentials cannot delete backups (org SCP + separate backup account).
No long-lived access keys; humans use SSO + phishing-resistant MFA with short sessions.
The CI/CD role cannot delete data, snapshots, or vaults (deny-listed, OIDC-scoped).
Replication lag is alarmed against each tier's RPO target — not just "connected".
A restore was performed and timed in the last 30 days (not just "backup succeeded").
The runbooks are current, stored outside the systems they recover, and drilled.
Detection (GuardDuty/CloudTrail) alarms on snapshot/vault deletion attempts and root usage.
The one-line takeaway: access control lowers the chance of a compromise; tiered RPO/RTO, an immutable cross-account backup, and rehearsed runbooks lower the cost of every disaster — including the ones access control can't prevent. Do both, and "our account can't be hacked" stops being a hope and becomes a plan you've tested.