Reported symptoms
The chilled-water loop at the primary site failed at 02:14 and the suite shut itself down to protect the hardware. The recovery estate at the second facility is intact, powered and reachable, and the DR runbook is being worked in order.
Step 1, network and routing, closes at 03:02. Step 2, DNS, closes at 03:19.
Step 3 is “unseal rbdr-vault-dr-01” — step 3 because every service in steps 4
through 19 reads its database passwords, API tokens and TLS material from that
store at boot.
Step 3 does not close. The process is running and the API port answers; the node
reports itself sealed and stays sealed. It unseals itself against the
recovery-site key appliance rbdr-hsm-dr, and the appliance is refusing the
connection before any credential is evaluated.
At 04:40 the incident commander asks who can issue the node a new certificate, and gets the answer that ends the shift: the issuing authority is a PKI engine inside the cluster that will not unseal.
Evidence provided
$ rbdr-secretctl statusInitialized true
Sealed true
Seal mode external-key-manager
Seal endpoint https://rbdr-hsm-dr.rbdr.internal:9443
Unseal progress 0/2
Last seal error seal endpoint rejected connection: remote error: tls: bad certificateIllustrative output
Progress is 0/2, not 1/2. Nothing has been offered and rejected; the
conversation ends in the handshake.
$ openssl x509 -in /etc/rbdr/tls/rbdr-vault-dr-01.crt -noout -subject -issuer -datessubject=CN = rbdr-vault-dr-01.rbdr.internal
issuer=CN = rbdr-ca-issuing
notBefore=Apr 20 09:14:02 2026 GMT
notAfter=Jul 19 09:14:02 2026 GMTIllustrative output
$ journalctl -u rbdr-hsm-agent --since '03:20' --no-pagerpeer=rbdr-vault-dr-01.rbdr.internal serial=0x4a19c2 result=refused
reason=client certificate expired notAfter=2026-07-19T09:14:02Z
no credential was read from peerIllustrative output
Three further facts come from the repository, not the wire. The Terraform
module that created the authority mounts rbdr-pki-issuing/ as a PKI engine in
the same secret-manager cluster, so the issuing private key is an object in that
cluster’s storage, readable only after unseal. The offline root rbdr-ca-root
is not in the cluster: its key material is a printed block in a dual-custody
safe six hours away. And the dependency register models 41 services with 40
edges, every one service-to-service.
Work the evidence before reading on
- Unseal progress is
0/2. What does that number rule out, and what does it leave? - Follow the issuer field. Where does the key that signed this certificate live, and what has to be true before anyone can use it?
- Somebody proposes skipping certificate verification. Who is verifying here, and does the proposal reach them?
- The dependency register is complete, current and correct by its own rules. Which vertices would it need before it could have shown this?
Root cause
The recovery order contains a cycle
The secret manager cannot unseal until it authenticates to the key appliance. It
authenticates with a client certificate. That certificate has to be signed by
rbdr-ca-issuing. The private key of rbdr-ca-issuing is an object in the
storage of the secret manager that has not unsealed.
Each arrow points at the next; the last points back at the first. No ordering of these four steps begins anywhere — which is what distinguishes this from a slow recovery: there is no first step to be slow at.
The expiry did not create the loop. The loop was created the day the authority was mounted inside the store; it stayed invisible while certificates kept arriving from a primary site that was up.
Nothing modelled the dependency below the service tier
The register was not neglected. It is current, reviewed, and right about everything it describes. Its vertices are services, so its edges can only be service-to-service, and a certificate authority, a trust anchor and a key appliance are not services in that model. They have no vertices, so a cycle running through them cannot be drawn as a cycle, and a graph that cannot draw the defect will never fail on it.
That is a control failure, not an oversight. Nobody skipped a step; the step that would have caught it did not have the vocabulary.
Why “the certificate simply expired” is not the answer
It did expire, forty days ago, and that is a real defect worth fixing. It is not the reason the recovery is stopped.
Test the alternative by acting on it. Renewal means asking an authority to sign
a request; openssl x509 names that authority as rbdr-ca-issuing, and the
Terraform module puts its private key inside the cluster. The remedy for the
expiry therefore needs the very thing the expiry is preventing from starting. An
expiry a renewal fixes is an incident; an expiry whose renewal needs the sealed
store is the cycle showing itself in the only way it could.
Resolution
Break the cycle at the trust anchor: the offline root is the only link in the
chain that is not inside anything. Retrieve rbdr-ca-root under dual custody
and reconstitute it on a machine outside the recovery estate. The appliance
already trusts the root, so a certificate signed directly by it validates
without touching any trust store:
ROOT_DIR=/media/rbdr-root-ca
KEY=/etc/rbdr/tls/rbdr-vault-dr-01.key
CSR=/srv/rbdr-dr/rbdr-vault-dr-01.csr
EXT=/srv/rbdr-dr/rbdr-vault-dr-01.ext
NEWCERT=/srv/rbdr-dr/rbdr-vault-dr-01.crt
openssl req -new -key "$KEY" -subj "/CN=rbdr-vault-dr-01.rbdr.internal" -out "$CSR"
printf 'subjectAltName=DNS:rbdr-vault-dr-01.rbdr.internal\nextendedKeyUsage=clientAuth\n' > "$EXT"
openssl x509 -req -in "$CSR" -days 7 -extfile "$EXT" \
-CA "$ROOT_DIR/rbdr-ca-root.crt" -CAkey "$ROOT_DIR/rbdr-ca-root.key" \
-CAcreateserial -out "$NEWCERT"
Seven days, not a year. The break-glass certificate exists to survive the outage, not to become the arrangement.
Install it, let the node authenticate to rbdr-hsm-dr, and allow the seal to
release. The moment the cluster unseals, rbdr-pki-issuing is available
again.
Then unwind the emergency in the order it was made: a normal certificate from
rbdr-ca-issuing, the break-glass one replaced and its serial revoked, the root
material back in custody with the register signed, and the elapsed time recorded
against the recovery objective.
Verification
ROOT=/media/rbdr-root-ca/rbdr-ca-root.crt
NEWCERT=/srv/rbdr-dr/rbdr-vault-dr-01.crt
openssl verify -CAfile "$ROOT" "$NEWCERT"
openssl x509 -in "$NEWCERT" -noout -enddate -ext subjectAltName
The secret manager then reports Sealed false and the appliance logs a
completed handshake naming the new serial — evidence that the seal released for
the intended reason. After unseal, the replacement certificate carries
rbdr-ca-issuing as issuer and the break-glass serial appears on the revocation
list.
The register is then extended and checked:
GRAPH=/srv/rbdr-dr/rbdr-recovery-graph.tsv
tsort < "$GRAPH" > /dev/null || echo "rbdr: recovery graph contains a cycle"
Run it against the pre-incident edge list first. If it does not report the cycle on the old graph, the new vertices are still missing and the check proves nothing: a guard that has never once fired is not yet a guard.
Prevention
Give the graph vertices for keys, anchors and appliances. Service-to-service
edges express only service-to-service ordering, and this loop ran through a
private key. Add a vertex for every artefact a service needs before it can
answer at all, and run tsort over the edge list in CI.
Ask what has to be running before this can be produced. Applied to
rbdr-ca-issuing the day it was mounted, the answer is “the secret manager” —
the whole defect in one sentence, years early.
Keep an issuance path that does not depend on the estate. An offline root on a medium the estate does not host is what made this recoverable. The course has measured the equivalent mechanisms on backup repositories:
$ borg key export --paper /work/brepoTo restore key use borg key import --paper /path/to/repo
BORG PAPER KEY v1
id: 31 / 6ece3c b2eaf0 547454 / ab77e5 a39843 - ce
1: 86a961 6c676f 726974 686da6 736861 323536 - 14
2: a46461 7461da 019ee5 d18e4e 246dfa 8e1bb1 - 59
3: 620691 f2dea2 918fcf ef0e59 4e1923 ca1ffa - 5d
4: 9156ef de13d2 0406ff a3be7b f4edec b27562 - 67
5: 49a3c5 f22540 1b9be8 bfa8a5 5ed425 fab040 - 3f
6: cb83da a857c9 e5bc97 32f394 452c1a b9c48b - e3
7: 72cc14 df7270 948df1 bed610 b7e152 81f4c4 - ed
8: 0e1d11 f617f9 3baa03 8b1189 f111e8 a2bec3 - 06$ restic key add --new-password-file /work/recovery-passsaved new key with ID 66c34166d8443d16e8c5899fe3f792e749cb24fa3a90ac90bbe8348979b57d90
>>> exit code: 0Rehearse the retrieval. Six hours of this outage was travel and dual custody: a design input, not a surprise, and only a rehearsal produces the number.
Never make an authority a tenant of the system it authenticates. The confidentiality case for it is sound, and silent on start-up order.
Run the exercise with the primary site unreachable. Four consecutive passes proved only that the recovery node could fetch a certificate over a working inter-site link, and the disaster removes the link.