Reported symptoms
The estate is a payments platform with roughly sixty services. Secrets
live in a three-node OpenBao cluster, bao-1 to bao-3, behind a VIP
published as vault.example.com. The cluster was initialised two years
ago with three key shares and a threshold of two. It has been reliable
enough that nobody has thought about it since.
The kernel patch window ran between 03:12 and 03:31 UTC and rebooted all three nodes. The change record says successful, and by any check the patching job performs it was.
The first page arrived at 07:18. By then the incident channel already contained four separate stories:
- The reporting worker has been alerting since 04:09. The overnight triage note reads intermittent database connectivity, watching.
- The morning batch fleet started at 06:40 and every job failed. The logs all stop at the same place, where the job fetches a database credential.
- A scheduled deployment of
payments-apibegan at 07:05. New Pods never reach readiness. The old Pods are untouched and are still serving every customer request, so the graphs are flat and nothing customer-facing has moved. - Twenty-four other services have logged nothing at all. Somebody uses that to argue that the secret manager cannot be the problem, because most of the estate is reading secrets perfectly well.
That last claim is the one worth pulling on, and it is wrong in an interesting way. None of those twenty-four services has read a secret since before 03:12.
Evidence provided
$ bao kv get kv/app/configCode: 503. Errors:
* Vault is sealedIllustrative output
$ bao statusThe 503 is the whole diagnosis if you read it as a protocol answer rather than as an error string. A refused connection means nothing is listening. A 503 means a healthy process accepted the request, understood it, and declined to serve it. That is why every reachability probe in the estate stayed green for four hours.
$ uptime --since$ bao write auth/approle/login role_id=@role_id.txt secret_id=@secret_id.txt"auth": {
"client_token": "s.[REDACTED]",
"accessor": "[REDACTED]",
"policies": ["app-read", "default"],
"token_policies": ["app-read", "default"],
"metadata": {"role_name": "app-role"},
"orphan": true,
"lease_duration": 1200,
"renewable": true
}Illustrative output
That number sets the clock on the whole incident. Every client token in the estate was issued with a 1200 second lease. Renewal is an API call. From 03:31 the API returned 503 to every caller, so no token in the estate outlived 03:32, whether its holder noticed or not.
$ journalctl -u report-worker --since '04:00' --until '04:20' --no-pager$ systemctl show --property=ActiveEnterTimestamp ledger-api pricing-api settlement-apiWork the evidence before reading on
The interesting question is not why the secret manager is refusing. It says why, in plain language, in the first block. The interesting question is why the failures arrived in three waves separated by hours.
- A 503 and a refused connection are different answers from different layers. Which one did every health probe in the estate test for, and what would each of them have returned at 03:32?
- Twenty-four services are healthy. Name the property they share, and then name what would happen to any one of them if it were restarted right now.
- The reporting worker failed at 04:09, not at 03:31. What was it doing in those thirty-eight minutes, and what does the gap tell you about how long the estate can coast?
- The node reports itself as both sealed and initialised. Which of those two fields would change if the storage backend had been lost instead, and why does the distinction decide the very first command you are allowed to run?
Before continuing: decide what a sealed node should look like to your monitoring, and explain why a reachability probe cannot see it.
Root cause
The seal is not a state the process can restore on its own
Everything in the store is encrypted with a data key, and that data key is itself wrapped by a root key. The root key exists only in the process memory of an unsealed node. Stopping the process, restarting the host or re-sealing deliberately all discard it, and on the way back up the node has an encrypted barrier it cannot open.
The initialisation record for this cluster stated the consequence plainly, two years before it mattered:
Vault initialized with 3 key shares and a key threshold of 2. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 2 of these keys to unseal it
before it can start servicing requests.
Note also that the tool prints Vault throughout. OpenBao retains the
original strings for compatibility, and the incident channel spent
eleven minutes arguing about whether the message even came from the
right product.
The estate monitored reachability, and a sealed node is reachable
A sealed node binds its listener, terminates TLS, parses the request, consults its policy engine far enough to know it cannot serve, and returns 503. Every layer a probe usually tests is working. The load-balancer backend was up, the certificate was valid, the synthetic check passed. There was no signal anywhere in the estate that observed the one field that had changed.
The blast radius followed the credential model, not the service map
This is why the failures looked random. Sort the estate by what each workload needs from the secret manager and the pattern is exact.
- Holds a static value in memory. Read once at process start, never read again. Survives indefinitely, and dies the instant it restarts.
- Renews a lease. Renewal is an API call. Fails at the first renewal after the seal, then fails again on every retry.
- Fetches at start. Fails immediately on start, which is why the batch fleet died all at once at 06:40 and the deployment stalled at 07:05.
Resolution
- Read the seal status on each node and confirm every one reports itself initialised. That single field separates a node that needs two key shares from a node that has lost its storage, and the two situations have opposite first commands. If any node reports itself uninitialised, stop and investigate the storage backend; do not run
bao operator initto clear the error. - Page the key-share custodians named in the ceremony document rather than looking for shares yourself. If you can assemble a threshold on your own, that is a finding for the review, and it does not change what you have to do in the next ten minutes.
- Have each custodian submit their own share to the same node in turn. The node accumulates progress and reveals nothing until the threshold is met. Shares must not be read out, pasted into chat, or typed by one person on behalf of another, because at that moment one individual can unseal alone and the ceremony has stopped meaning anything.
- Confirm the first node reports itself unsealed and let the load balancer route to it. Service is restored at this point for every consumer that retries, and restoring one node quickly is worth more than restoring three tidily.
- Unseal the second and third nodes as well, in the same way. Leaving the estate on a single unsealed node means the next single reboot repeats this incident, and you are one node away from doing it all again.
- Restart the failed workloads in dependency order, beginning with services that other services call. Anything that exited on its first failed fetch needs a restart; anything still retrying will recover without help. The batch fleet can be re-run once its dependencies are healthy.
- Do not paste secret values into environment variables, unit files or Pod specs as a stopgap. That trades a recoverable outage for an unmanaged copy of production credentials on hosts nobody is tracking, and every one of those values then has to be rotated.
- Record the exact clock times of the reboot, the first lease failure, the first fetch failure and the page. The three-hour gap between the outage starting and anyone knowing is the actual finding, and it will be argued away by next week if it is not written down now.
Verification
- From a host that took no part in the incident, log in with the application role and read a real production path. This proves the whole path a workload uses, which the seal status command does not.
- Read the audit device file directly and confirm both the login and the read are recorded against the application accessor rather than an operator token. The audit log is written by the server, so it is an independent witness to what the client believes happened.
- Confirm all three nodes report unsealed, not just the one you fixed first. One unsealed node is service restored; three is redundancy restored, and only the second claim survives the next reboot.
- Watch the batch fleet complete rather than start. A job that now fetches its credential and then fails on something else is a separate incident, and the distinction is invisible if you only check that the jobs launched.
- Confirm the stalled
payments-apirollout finishes and the new Pods pass readiness, then confirm the old Pods are retired. The deployment is the only workload in the incident whose failure was masked by healthy predecessors. - Check that the reporting worker recovered and that its alert cleared on its own. An alert that has to be silenced by hand after the fix means the alert is measuring the wrong thing.
- Deliberately restart one low-risk service. Every healthy service in this incident was healthy because it had not restarted, so until something restarts successfully you have not tested the thing that broke.
Prevention
- Move to auto-unseal and keep Shamir as the recovery path. An auto-unseal seal has the node unwrap its root key through an external key service at start-up, so a reboot becomes a reboot. Keep the shares for disaster recovery, rehearse them quarterly against a restored backup, and set a written pass mark of thirty minutes from page to unsealed.
- Alert on seal status, every 30 seconds. Warn at 60 seconds sealed and page at 120 seconds. Poll each node directly rather than through the VIP, so the alert cannot be answered by whichever node happens to be healthy.
- Never let a reachability probe stand in for a readiness signal. A sealed node passes every reachability test that exists. The probe that would have fired at 03:32 is one that reads a canary secret with a real role and asserts on the returned value.
- Split the nodes across separate patch groups. No single maintenance window should be able to restart every member of a quorum. This one did, and it was recorded as a success.
- Make consumers survive a 503. Cache the last known good value, retry with capped exponential backoff, and distinguish a 503, which is retryable, from a 403, which never becomes true by waiting. A process that exits on its first failed fetch turns a recoverable outage into a restart storm.
- Alert on renewal failures, not just on expiry. Renewals begin failing long before any credential actually dies. Two consecutive failed renewals is a free thirty-minute warning, and in this incident it was available from 03:32.