01Status is a verification outcome
Only a successful rehearsal writes PROVEN, and proof expires at twice the verification interval. The field that is reported, paged on and audited is last_proven_restore.
ADR-0126 views, in reading order. Every view ships three ways: an HTML page, an SVG that re-opens in diagrams.net fully editable, and draw.io source.
A platform that copies 120 datastores into storage it cannot delete from, and then proves every day that the copies turn back into working systems within a stated time. It runs on hardware the organisation owns: Ceph object storage with Object Lock in two data centres, WORM tape at a third site, each engine's own backup tool behind one adapter contract, and an isolated cluster where restores are rehearsed through the same path an emergency would use. Read the set in order. Acts 1 and 2 fix the boundary and the people who depend on it. Act 3 shows the parts, act 4 which store may be lost and which may not, act 5 the restore paths including the one where nobody knows when the corruption started, and acts 6 and 7 how it is run and why it survives its own operators. One decision governs every page: protection status is written by the verifier, never by a backup job.
A successful backup job proves that something was written. Only a restore proves that something can be recovered. This architecture is built around that difference.
Almost every serious data loss story has the same shape. There were several backup mechanisms, each reporting success, and none of them had ever been restored from until the day it was needed. The tool had drifted from the engine version, or the key had been rotated away, or the restore took nine hours against a one-hour promise, or the only copies sat in an account the attacker also controlled. The estate here is 120 datastores across six kinds of engine, about 1.4 PB, run in two of the organisation's own data centres. The job is not to take more copies. It is to be able to say, for each datastore, when a restore last actually worked and how long it took.
Each engine's own tool takes the copy (pgBackRest, XtraBackup, TiDB BR, ClickHouse BACKUP, Velero with Kopia) behind one adapter contract. A version gate refuses any tool and engine pair that has not been certified by a real restore. Copies are encrypted at the source and written to Ceph object storage with Object Lock, in a custody domain the backup platform cannot delete from. A verifying copier fills a second, independently administered cluster in the other data centre, and Tier 1 and the regulatory set also go to WORM tape at a third site. A PostgreSQL catalogue indexes it all, but every copy describes itself, so the catalogue can be rebuilt by scanning. Every day, an isolated cluster with no route out restores each Tier 1 datastore through the same Temporal workflow an emergency would use. It checks structure, content and whether the application boots, and records a proof with a measured duration. Protection status is computed from those proofs and nothing else, and a monitor run by another team pages when the proofs stop arriving.
Only a successful rehearsal writes PROVEN, and proof expires at twice the verification interval. The field that is reported, paged on and audited is last_proven_restore.
ADR-01Rehearsals use the same workflow, adapters, keys and custody reads as an emergency, to a random second, alternating custody copies. A special verification path would only prove itself.
ADR-03Healthchecks, run by the observability team, expects a success ping per tier and per Tier 1 datastore, and pages at twice the interval. A pipeline that stops looks exactly like a healthy one to a monitor that only watches for failures.
ADR-04Ceph RGW with Object Lock set at write time, on separate racks with separate administrators and credentials. The capture role can put and get, and nothing else.
ADR-06The permission to shorten a lock lives only in a sealed OpenBao instance that needs 2 of 5 officer key shares to open. It issues one scoped 15-minute credential per unseal, and no standing identity holds that permission.
ADR-07WORM tape written by Bareos and vaulted at a third site, for Tier 1 and the regulatory set, with its own quarterly restore rehearsal.
ADR-09Six mature engine tools do the byte-level work and each engine's own point-in-time recovery. The platform standardises the artefact envelope, the recovery-point map and the promise.
ADR-10A nightly restore matrix certifies each engine version against each tool version at depth 3. Adapters refuse any pair not in the signed list, so drift shows up as a blocked capture that day, not a failed restore months later.
ADR-12A signed envelope beside every artefact carries everything a restore needs. The catalogue is rebuilt from a scan every week and diffed against the live one.
ADR-15Hourly block-incremental bases keep WAL replay under an hour for Tier 1. The 60-minute RTO is allocated to authorisation, transfer, replay and assertions, and each part is measured every day.
ADR-19Backup platforms usually age badly in one of three ways. The tooling is replaced and nobody can read the old copies. A vendor or licence changes and the custody layer has to be migrated under pressure. Or the drills quietly stop, and the platform is back to reporting job success. The design defends against each of these on purpose.
A copy counts once a restore from it has worked. That was true of tape in 1990 and will be true of whatever replaces object storage. The verifier, the proof record and the silence alarm are the lasting parts, and none of them assumes a particular engine, tool or storage product.
Adapters are replaceable one engine at a time. The envelope is a versioned, documented JSON schema stored next to tool-native data, so a copy written by pgBackRest in 2026 can still be found, described and restored with the pgBackRest version recorded in it, even after the platform has moved to another tool.
The platform uses nothing from Ceph except the S3 API, Object Lock and STS. Any S3-compatible store with Object Lock can replace it, and moving is a copy under a new lock, not a redesign. Nothing depends on the Ceph admin API, deliberately.
Recent relicensing forced forks and migrations elsewhere in the industry: HashiCorp Vault moved to the Business Source License in 2023, and MinIO's community edition narrowed sharply in 2025. Custody and keys therefore sit on foundation-governed or long-stable projects: Ceph, PostgreSQL, and OpenBao under the Linux Foundation. Every choice has a named alternative in the decision record.
Artefacts are encrypted with data keys wrapped by per-classification key-encryption keys in OpenBao, and the envelope records the key id and algorithm. Rotating a key, or moving to a new algorithm, rewraps small data keys and leaves the stored bytes alone. Old key versions are kept for longer than the longest retention.
RTO, restore throughput and replay speed are measured again every day, so when hardware, data volume or engine versions change, the published numbers change too. A design sized once, on paper, goes out of date without anyone noticing. This one shows its drift in the daily report.
Two-person rules are enforced with key shares that are re-issued whenever a holder leaves, not by remembering who is trusted. Runbooks are proven by gamedays run by people who did not write them, so knowledge does not leave with the original team.
Targets from the requirement, and the mechanism that meets each one. Every duration and throughput figure is replaced by a measured value at the first drill and republished monthly.
| Quality | Target | How it is met | View |
|---|---|---|---|
| Tier 1 RPO | ≤ 5 min; log shipping lag p99 ≤ 60 s | Engine-owned continuous log shipping (WAL archive-async with a 60 s timeout, binlog streamer, TiDB log backup) that keeps running without the control plane | 09 |
| Tier 1 RTO | ≤ 60 min, measured | Budget of 10 min start, about 17 min transfer at 4 GB/s, ≤ 10 min replay (bounded by hourly bases) and 10 min asserts; escalated in flight when the estimate passes target | 14 |
| Recoverability evidence | 100% of Tier 1 proven in interval; 98% of Tier 2; 95% of Tier 3 | Daily Tier 1 rehearsal at depth 4, Tier 2 rotation of 3 a day, Tier 3 sampling plus a quarterly full bucket restore | 15 |
| Verifier silence | Paged within 2× interval | Success heartbeat per tier and per Tier 1 datastore to Healthchecks, run outside the platform | 21 |
| Deletion resistance | No single person or component can delete or shorten | Object Lock at write, capture role without delete, sealed break-glass OpenBao needing 2 of 5 shares, separately administered custody B and tape | 25 |
| Restore path availability | ≥ 99.95% monthly; works with control plane down | Orchestrator deployable in either data centre; manual runbook needing only custody, envelope, key and engine tool, rehearsed quarterly | 18 |
| Capture throughput | ≥ 2 GB/s sustained, provisioned 3 GB/s | Agents beside sources on a dedicated 2 × 100 GbE backup network; 8 RGW gateways; Tier 3 sweeps rate-limited below Tier 1 log shipping | 19 |
| Catalogue query | p95 ≤ 500 ms at 30 M artefacts | PostgreSQL B-tree range scans on (datastore_id, from_ts) over monthly partitions | 11 |
| Catalogue loss | Restore possible with catalogue absent | Self-describing envelopes; weekly rebuild by scan into a shadow catalogue, diffed | 10 |
| Unprotected resources | 0 older than 72 h | Discovery from NetBox, the Kubernetes API and RGW every 15 minutes; admission refuses a PVC without a tier label; incident to the owner at 72 h | 04 |
| Evidence retention | Audit and drill records 7 years, immutable | Evidence bucket in compliance-mode Object Lock; intent written before every destructive action | 17 |
Not a slice of every feature. The proof shows the properties everything else depends on: a copy the platform cannot delete, a restore that works with the platform switched off, and a proof record that shows the silence alarm working.
| Risk | If it lands | Response |
|---|---|---|
| Storage administrators can bypass Object Lock | A rogue or compromised Ceph administrator in custody A can destroy locked objects below the S3 layer | Two-person root access to custody hosts; custody B and tape administered by different people; out-of-band RADOS operations alerted on (ADR-06, ADR-09) |
| The 3% daily change rate is a guess | Custody and capture capacity sized wrong by as much as half | Measured in the first 30 days; custody bought in two stages; Tier 3 retention is the first lever, with the owner's written agreement (ADR-06) |
| Teams do not write invariants | Datastores stay at depth 2, and silent data loss passes rehearsal | Invariant templates per engine; depth reported per datastore; the owner accepts a lower depth in writing, recorded against the tier (ADR-02) |
| Both data centres lost to one regional event | Only tape survives, with an RTO of days | Accepted and placed on the enterprise risk register; a third-site custody cluster is priced and deferred (ADR-26) |
| Temporal becomes a hidden dependency | Captures or restores stall during a control plane outage | Tier 1 log shipping outside Temporal, fail-static CronJobs for base copies, and a manual restore rehearsed quarterly with Temporal switched off (ADR-22) |
Twenty-six decisions make up this architecture. Everything else on the twenty-six views is convention, and convention needs no defence. Each record starts with the question that forced a decision, says what was chosen and how it is built on hardware the organisation owns, lists the options that lost and why, and states what the choice costs. It then gives the conditions under which a different organisation should choose differently, and explains why the choice should still be right after the tools, the hardware and the team have all changed. Read the one-pager first. Read a record when you want to argue with it.
Status of this document. This is a design, not a report on a running system. Throughput, capacity and duration figures are sized targets and stated assumptions taken from the requirement, and each one is replaced by a measured value at the first drill. Four product behaviours are relied on and must be confirmed in the proof of concept before any production copy depends on them: Ceph RGW honouring governance bypass only for a caller with the explicit permission, RGW STS web-identity roles accepting SPIRE-issued JWT-SVIDs, ClickHouse BACKUP to an encrypted disk over S3, and TiDB BR log backup writing to a bucket with Object Lock enabled.
What makes a datastore count as recoverable, how deep that proof goes, and who notices when the proving stops.
ADR-01Protection status is written by the verifier, never by a capture job ADR-02Verification depth is chosen per tier by the failure class it catches ADR-03Rehearsals use the emergency restore path, to a random second, alternating custody copies ADR-04Page on the verifier's silence, from a monitor outside the platform ADR-05Rehearse in a dedicated enclave with no route to production or the internetWhere the copies live, who can and cannot destroy them, and the second custodian that fails differently.
ADR-06Custody on Ceph RGW with Object Lock, in a separately administered domain ADR-07Governance-mode locks, reduced only through a sealed OpenBao needing two of five officers ADR-08Two independent custody clusters joined by a verifying copier, not RGW multisite ADR-09WORM tape at a third site as the second custodian, with its own rehearsed restoreHow six kinds of engine are copied consistently, at the right version, without the platform reimplementing any of them.
ADR-10Engine-native tools behind one adapter contract and one envelope ADR-11Point-in-time recovery is each engine's capability, orchestrated rather than reimplemented ADR-12Refuse to capture with a tool and engine version pair that has not been certified by a restore ADR-14Capture buckets from change notifications, reconciled by a weekly listing ADR-25Consistency groups by marker transactions, only for applications that cannot tolerate skewWhat the catalogue is, what it is not, and the evidence that must outlive the copies.
ADR-15Artefacts are authoritative and self-describing; the catalogue is a rebuildable index ADR-16The catalogue is an append-only event log in PostgreSQL with projections ADR-17Evidence is written before action, kept 7 years under a compliance lock, apart from the catalogueHow a copy becomes a running system inside its RTO, including the dangerous and the unknown cases.
ADR-18Side by side by default; in-place needs a second approver and a mandatory snapshot ADR-19RTO is a budget, and hourly block-incremental bases keep log replay inside it ADR-20Find the last clean recovery point by parallel nine-way search against an owner invariant ADR-21A manual restore path that needs only custody, the envelope, the key and the engine toolWhat orchestrates the work, how protection is declared, and where everything runs.
ADR-22Temporal for durable workflows, with fail-static CronJobs for base copies ADR-23Protection as policy on a declared tier, with no default and continuous discovery ADR-26Two data centres and a vault site, a warm control plane, and a dedicated backup networkWho may do what to which copy, and how cryptography enforces the separations that policy alone would not.
ADR-13Encrypt at the source with data keys wrapped by per-classification keys in OpenBao ADR-24Four disjoint custody roles assumed with SPIRE workload identity; no long-lived credentialsEvery capability on the views, the package that provides it, a credible alternative, and the record that justifies the choice. Everything runs on hardware the organisation owns, with no managed service and no external network dependency.
| Capability | Choice | Origin | Credible alternative | Why this one | Record |
|---|---|---|---|---|---|
| Custody object storage | Ceph RGW (Squid line), S3 Object Lock, EC 8+3 | Open source | MinIO; Garage; SeaweedFS | Mature Object Lock and STS behind the S3 API, a very large production base, and governance that has not changed hands | ADR-06 |
| Two-person lock reduction | Dedicated sealed OpenBao, Shamir 2 of 5 | Open source | Custom approval service holding a bypass key; HSM quorum cards | Uses an audited, existing threshold-cryptography primitive instead of inventing one | ADR-07 |
| Custody replication | Custody copier (Go), verifying SHA-256 | This design | RGW multisite zone sync; rclone | Keeps the two clusters in separate administrative realms and checks every byte before it counts as a second copy | ADR-08 |
| Second custodian | Bareos with LTO-9 WORM media | Open source | Third-site Ceph cluster; Bacula | Different media, different software, offline once vaulted | ADR-09 |
| PostgreSQL capture and PITR | pgBackRest 2.5x, block incremental, archive-async | Open source | WAL-G; Barman | Parallel restore, block incremental, repository encryption and a built-in verify command | ADR-10 |
| MySQL capture and PITR | Percona XtraBackup 8.4 plus binlog streaming | Open source | MySQL Shell dump; mydumper | Physical hot backup with fast restore and binlog position recorded for replay | ADR-11 |
| Distributed SQL capture and PITR | TiDB BR snapshot plus log backup | Open source | Logical Dumpling export | Consistent at a cluster-wide timestamp, with native point-in-time restore | ADR-11 |
| Analytics capture | ClickHouse BACKUP with base_backup to an encrypted S3 disk | Open source | clickhouse-backup (Altinity) | Engine-native, part-level incremental, no extra daemon | ADR-10 |
| Volume and config capture | Velero with Kopia data mover, CSI snapshots | Open source | Kasten K10 (commercial); Stash | Kubernetes-native, deduplicating, encrypted repository, freeze hooks | ADR-10 |
| Bucket capture | Object mirror (Go) consuming RGW notifications via Kafka | This design | Nightly rclone listing sync | Work in proportion to what changed, not to how many objects exist | ADR-14 |
| Version certification | Restore matrix in GitLab CI; signed pair list in Git | This design | Pin versions by convention | Turns silent drift into a blocked capture on the same day | ADR-12 |
| Workflow orchestration | Temporal on PostgreSQL | Open source | Argo Workflows; Kubernetes CronJobs alone | Durable timers, signals for approvals and retries that survive worker restarts | ADR-22 |
| Catalogue | PostgreSQL 17 with Patroni, append-only events | Open source | Kafka with ksqlDB; ClickHouse | 30 M rows is an ordinary index workload; transactional projections are simple | ADR-16 |
| Evidence store | Compliance-mode locked bucket, hash-chained daily segments | This design | Audit table in the catalogue only | Evidence must not share the catalogue's failure or its administrators | ADR-17 |
| Key management | OpenBao Transit, 5 voters over three sites | Open source | HashiCorp Vault (BSL); HSM with PKCS#11 | Envelope encryption, key versioning and rewrapping, community governed | ADR-13 |
| Workload identity | SPIRE JWT-SVIDs to RGW STS roles | Open source | Static RGW keys in Kubernetes secrets | No long-lived custody credential exists to steal | ADR-24 |
| Human identity and approvals | Keycloak with WebAuthn | Open source | LDAP with OTP | Phishing-resistant second person for in-place restore approvals | ADR-18 |
| Restore authorisation | Open Policy Agent | Open source | Rules coded in the API | Owner and classification rules as reviewable, testable policy | ADR-18 |
| Dead-man monitor | Healthchecks (self-hosted) on the shared observability estate | Open source | Prometheus absent() rules | Push-based absence detection, hosted by another team | ADR-04 |
| Isolated rehearsal | Dedicated Kubernetes cluster, Cilium deny-all egress, own VRF | Open source | Namespace in a shared cluster | A full copy of production needs a network boundary, not a label | ADR-05 |
| Policy and inventory | Git plus Argo CD, NetBox, Kyverno admission | Open source | Per-datastore job configuration in a UI | Protection bound to a class and tier, reviewed like code, inherited by new resources | ADR-23 |
| Observability | Prometheus, Grafana, Loki, Alertmanager (shared) | Open source | A dedicated stack for the platform | Existing on-call routes; only the dead-man check needs independence | ADR-04 |
What event makes the platform say a datastore is protected?
What does 'verified' actually mean, and should it mean the same thing for every datastore?
Should verification run through its own optimised path, or through exactly the path a real restore would use?
How do we find out that verification has quietly stopped?
Where do daily restores of full production data go?
Where do copies live so that nothing able to destroy the original can reach them?
Who can shorten a retention lock, release a legal hold, or delete before expiry, and how do we make sure that is never one person?
How does a Tier 1 copy reach the second data centre?
What takes the role of the off-provider copy for an estate that is already on-premises?
Do we build one backup mechanism for everything, or use each engine's own?
Does the platform ship and replay its own change log, or rely on each engine's?
How do we stop version drift from producing copies that write cleanly and do not restore?
How do we protect 1.3 PB of object storage without listing billions of objects every night?
How are several datastores restored to a mutually consistent point when their engines share no common quiesce?
If the catalogue is lost, are the copies still usable?
How is catalogue state stored so that its history survives corrections?
Where do audit events and drill records live, and what happens if they cannot be written?
Who may restore, into what, and how is overwriting production made safe?
What actually determines whether a Tier 1 restore finishes in 60 minutes?
How do we recover to before a corruption whose start time nobody knows?
Can a restore be done when the platform itself is part of the outage?
What orchestrates captures, restores and rehearsals, and what happens when it is down?
How does a new datastore get protected without someone remembering to set it up?
Where does everything run so that one site's loss leaves both copies and the ability to restore them?
Who can read a copy, and how do we make a restore into the wrong environment fail?
How are the four identities in the requirement made real, and what is stolen if a host is compromised?
Every open-source package named on the views, what it is, and the job it does here.
| Package | What it is | What it does here | Considered instead |
|---|---|---|---|
| Ceph (RGW) | Distributed storage system; RGW is its S3-compatible object gateway | Both custody clusters: Object Lock, versioning, STS roles, erasure-coded storage | MinIO, Garage, SeaweedFS |
| OpenBao | Community fork of HashiCorp Vault under the Linux Foundation | Transit key wrapping, short-lived database credentials, and the sealed break-glass issuer | HashiCorp Vault, HSM |
| Bareos | Network backup system descended from Bacula, with tape support | Writes Tier 1 and regulatory artefacts to LTO-9 WORM media at the vault site | Bacula, Amanda |
| pgBackRest | PostgreSQL backup and restore tool | Full, differential and block-incremental base copies; asynchronous WAL archiving; parallel PITR restore | WAL-G, Barman |
| Percona XtraBackup | Hot physical backup tool for MySQL | MySQL base copies, paired with mysqlbinlog streaming for PITR | MySQL Shell dump utilities |
| TiDB BR | TiDB's Backup and Restore tool | Cluster-consistent snapshots and continuous log backup for point restore | Dumpling |
| ClickHouse BACKUP | ClickHouse's native backup statement | Incremental table and partition backups to an encrypted S3 disk | clickhouse-backup |
| Velero with Kopia | Kubernetes backup tool with a deduplicating, encrypted repository | Persistent volume snapshots moved to custody, and configuration exports | Stash, Kasten K10 (commercial) |
| Apache Kafka | Distributed event log | Carries persistent RGW bucket notifications to the object mirror | Redpanda, NATS JetStream |
| Temporal | Durable workflow execution engine | Orchestrates captures, restores, rehearsals, approvals and clean-point search | Argo Workflows, Cadence |
| PostgreSQL with Patroni | Relational database with an HA manager | Catalogue event log and projections; Temporal persistence | CockroachDB, MySQL with Orchestrator |
| SPIRE | Reference implementation of SPIFFE workload identity | Issues JWT-SVIDs that capture, restore and verify workloads exchange for RGW STS roles | Kubernetes projected service-account tokens |
| Keycloak | Identity and access management server | Operator sign-in and WebAuthn step-up for approvals; a separate realm for custody | Authentik, Dex with an upstream IdP |
| Open Policy Agent | General-purpose policy engine | Restore authorisation by ownership and target classification | Cedar, rules in code |
| Kyverno | Kubernetes policy engine | Refuses PVCs and operator resources without a declared tier; guards enclave secrets | OPA Gatekeeper |
| Cilium | eBPF-based Kubernetes networking and policy | Deny-all egress in the rehearsal enclave | Calico |
| NetBox | Infrastructure source-of-truth application | Inventory and ownership of non-Kubernetes datastores for discovery | A CMDB |
| Argo CD | GitOps continuous delivery for Kubernetes | Syncs policies, platform services and the warm DC-B control plane | Flux |
| GitLab CE and Renovate | Source control with CI, and a dependency update bot | Runs the certification matrix when tools or engines change | Forgejo with Woodpecker |
| cosign and Harbor | Artefact signing, and an OCI registry | Signed adapter images and signed certification lists; mirrored images at the vault site | Notation, Zot |
| Healthchecks | Self-hostable dead-man's-switch monitoring service | Pages when rehearsal success pings stop arriving | Prometheus absent() rules |
| Prometheus, Grafana, Loki, Alertmanager | Metrics, dashboards, logs and alert routing | RPO lag, proof age, coverage and showback, routed to on-call | VictoriaMetrics, OpenSearch |
svg/<view>.svg or drawio/<view>.drawio in draw.io Desktop or at app.diagrams.net to edit. The SVG carries the diagram inside it, so it is both the picture and the source. This folder is self-contained — copy it whole and every link still resolves.