Business Continuity & ICT Resilience Policy
Satisfies Annex A 5.29 (information security during disruption), 5.30 (ICT readiness for business continuity), 8.13 (information backup), and 8.14 (redundancy of information processing facilities). Grounded in `register/facts.md` and `docs/runbooks/db-recovery.md`; statuses match `register/controls.yaml`.
1. Policy statement
Stratify's business-continuity posture has one real strength and one real, honestly-stated weakness, and this policy leads with both rather than burying the weakness in qualifying language. The strength: a nightly, automated database backup has been running since the Hetzner cutover, with stated recovery objectives. The weakness: that backup has never once been used to actually restore anything. A backup mechanism that has never been tested is not the same claim as "backup and recovery works" — this policy does not conflate the two.
2. Information backup (8.13) — status: partial
What exists, and is real:
- A nightly full
pg_dumpof thestratify-dbcontainer, via
scripts/hetzner-backup-postgres.sh, writing to /data/stratify/backups/stratify-<UTC-stamp>.sql.gz on the Hetzner host (docs/runbooks/db-recovery.md §1).
- 14-dump retention, rotated automatically by the script (
STRATIFY_BACKUP_KEEP,
default 14).
- Nightly 05:30 UTC schedule via
/etc/cron.d/stratify, with logging to
/var/log/stratify-backup.log (db-recovery.md §1).
- Stated recovery objectives: RPO ≤ 24 hours, RTO ≤ 60 minutes (
db-recovery.md§3.1),
with an explicit note that a manual backup run immediately before any planned higher-risk operation tightens RPO to roughly one minute.
- A quarterly test schedule is defined —
db-recovery.md§4 names four distinct drills
(full restore, single-table restore, chain verification over a restored DB, outage failover) with named owners (Toby for Q1/Q2/Q4, Antonios for Q3).
The gap, stated plainly, is the single highest-priority business-continuity item in this ISMS: restore has never been tested. db-recovery.md's own setup checklist has "Restore einmal manuell getestet" (restore tested manually at least once) as an unchecked item, and the runbook's decision log ends with a row that simply reads "TBD | Erster vollständiger Hetzner-Restore-Drill abgeschlossen" (first full Hetzner restore drill completed) — an open item, not a completed one. This is tracked as RISK-004 in register/risks.yaml (likelihood 2, impact 5, inherent score 10, treatment: mitigate) — a low-likelihood, high-impact risk, which is exactly the profile of "we assume this works because we've never needed it to."
Because of this, 8.13 is scored partial, not implemented, in controls.yaml. A backup job running nightly is a real control; a backup job that has never been proven to produce a working restore is not yet a *demonstrated* one, and this policy does not claim otherwise.
Commitment: the first full restore drill (the Q1 item in db-recovery.md §4, owned by Toby) is the concrete next action for this control — not a vague intention, a specific, already-scheduled drill that has simply not been executed yet. Once it runs, it should be logged into audit_log per the runbook's own convention (actorRole: 'system', eventType: 'ops.recovery_drill', db-recovery.md §4) and referenced from PRO-07 (Backup & Restore procedure, pending per PLAN.md §4) as the first piece of real evidence for this control.
3. Redundancy of information processing facilities (8.14) — status: not_started
Production runs on a single Hetzner host with no redundancy beyond a manual read-only-mode failover toggle for host-level outages (db-recovery.md §3.4: STRATIFY_READ_ONLY=1, which makes server actions fail soft with a "we're briefly unreachable" message rather than hard-erroring). There is no automatic failover, no standby host, and no multi-region deployment.
This is tracked as RISK-007 in register/risks.yaml, and it is important to state precisely what kind of gap it is: RISK-007 is recorded with `treatment: accept` and `status: accepted` — this is a deliberate, accepted risk at pre-seed stage with one engineer, not a hidden gap being passed off as mitigated. Building real redundancy (a standby database, multi-host failover, automated DNS cutover) is a meaningful engineering investment that is disproportionate to a single-engineer team's current capacity, and accepting that tradeoff explicitly — rather than pretending the manual toggle is equivalent to redundancy — is the honest position controls.yaml and risks.yaml both take. This policy does not relitigate that acceptance; it records it.
What is committed to, despite the accepted-risk framing: the manual read-only-mode toggle itself has also never been tested end-to-end (db-recovery.md §4's Q4 drill, owned by Toby, is scheduled but — like the Q1 restore drill — not yet executed). An accepted risk whose one compensating control has never been exercised is a weaker position than an accepted risk whose compensating control is proven to work. This policy commits to testing the failover toggle at least once, alongside the restore drill in §2, as the concrete next step — closing the gap between "we accepted this risk because we have a manual toggle" and "we've confirmed the manual toggle actually does what the runbook says."
4. Information security during disruption (5.29) — status: not_started
No continuity plan exists for disruption scenarios beyond the single host-outage scenario covered in db-recovery.md §3.4. Broader disruption planning — sustained loss of a sub-processor (e.g., AWS SES going down and blocking all transactional email), a compromised founder laptop, or an extended Hetzner regional outage beyond the host itself — is not addressed anywhere in the current runbook set. This is named honestly as not_started rather than inferred from the narrower host-outage scenario that does exist.
5. ICT readiness for business continuity (5.30) — status: partial
The read-only-mode failover toggle documented in db-recovery.md §3.4 is a real, specific ICT-continuity mechanism for one scenario (host outage) — concrete enough to be scored partial rather than not_started. No broader ICT-continuity plan exists: there is no alternate hosting arrangement, no documented data-export escrow, and no plan for standing up a temporary environment beyond the informal note in db-recovery.md §3.4 step 3 ("bei verlängertem Outage (>4h): jüngsten Dump in eine temporäre Postgres-Instanz EU einspielen"). That note is a reasonable sketch, not a tested procedure.
6. Forward path
The two concrete, already-scheduled next actions this policy commits to, without inventing a delivery date beyond what db-recovery.md already states as its quarterly cadence:
- Execute the first full restore drill (§2) — the Q1 item in
db-recovery.md§4,
already scheduled, not yet run.
- Execute the read-only-mode failover drill (§3) — the Q4 item in the same table,
likewise scheduled and not yet run.
Both should be logged to audit_log per the runbook's own convention, and both are the concrete evidence PRO-07 and PRO-13 (pending, per PLAN.md §4) will document once they exist. Until then, this policy does not claim either control is more mature than it is.
7. Related documents
- PRO-07 — Backup & Restore (including the restore test) — pending; will document the drill
in §2 once it runs
- PRO-13 — Business Continuity & DR Test — pending; will document the drill in §3
docs/runbooks/db-recovery.md— the operative runbook this entire policy is grounded inregister/risks.yaml— RISK-004 (untested restore), RISK-007 (accepted single-host risk)- POL-09 — Incident Management Policy — the audit-chain-corruption scenario in
db-recovery.md §3.3 draws on the same backup mechanism described here