Skip to main content
RunBook Academy

← All break/fix scenarios in Secrets, PKI & Certificates

intermediatedynamic-credential~35 min

A lease was revoked while a running job was still using it

Reported symptoms

  • At 21:27 UTC the nightly consolidation job alerts: it is stuck at sixty per cent and every attempt to open a new database connection fails immediately
  • The failure text is a PostgreSQL fatal error naming a role that does not exist, so the first twenty-two minutes are spent with the database team looking for a user somebody supposedly dropped
  • The database is entirely healthy: administrative connections succeed, the connection count is normal, replication is current and there is nothing in its log except the rejected authentications
  • Two other services that draw dynamic credentials from the same database are completely unaffected and continue working throughout
  • The secret manager is unsealed, responsive and healthy, and the incident channel uses that to rule it out within the first five minutes
  • Nobody restarts the worker, because the run has already consumed forty minutes and restarting it means losing the whole night

Evidence

  • · The run record for the job shows the lease identifier it holds, a lease duration of 120 seconds, renewable true, and a generated database username beginning with the dynamic-credential prefix
  • · The database refuses the connection with a fatal error naming that exact generated username as a role that does not exist
  • · A query of the database catalogue for every role matching the dynamic-credential prefix for this role returns no rows at all
  • · The operator shell history and the audit device both record a lease revocation issued at 21:26, roughly two minutes before the first application failure, under an operator token rather than the job AppRole
  • · The lease lookup captured one second after the run started shows an issue time of 21:23:43 and an expiry two minutes later, with renewal enabled
  • · A query run 200 milliseconds after issue proves the credential was genuine, returning the generated username as the current user
  • · The database role carried its own expiry, set by the creation statement at issue time and pushed forward by each renewal, so the database enforced the lifetime independently of the secret manager
  • · Requesting a fresh credential for the same role during the incident succeeds instantly and the new credential connects, which proves the database, the network and the secrets engine are all working
Diagnosis and resolutionclick to reveal

Root cause

Revocation of a dynamic database credential does exactly what it says. The secrets engine runs the configured revocation statements, the generated role disappears from the database catalogue, and every connection attempt using that username is refused from that instant. The zero rows returned by the catalogue query are not damage; they are proof that revocation worked correctly and completely. Two independent defects turned a correct operation into a lost night. The first is procedural: a quarterly audit produced a list of lease identifiers described as orphaned, and nothing in the platform maps a lease identifier to the workload holding it. Orphaned was an assumption drawn from age alone. The revocation was executed with no drain, no notice, and no owner able to say whether anything still depended on it. The second defect is the one that made this an outage rather than a shrug: the consolidation worker reads its credential once, at process start, into the configuration of its connection pool, and has no code path that returns to the secret manager. Its pool classifies an authentication refusal as a transient connection error and retries the same dead username indefinitely. A consumer able to re-fetch on an authentication failure would have recovered in under a second and produced nothing more than a log line. The reason it took fifty-one minutes to find is the third element: the error text is a PostgreSQL fatal error, so it was read as a database fault, and the generated username in the middle of it was skimmed as noise rather than as the routing signal it actually is.

Remediation

Start by refusing the fastest fix. Somebody will suggest recreating the role in PostgreSQL by hand with the same name and a chosen password, and it would restore the job in about ten seconds. Do not do it. That credential would then exist outside the secret manager entirely: nothing will ever rotate it, no lease covers it, revocation will not reach it, and it will not appear in any inventory. You would trade a lost batch run for a permanent unmanaged database account. Do not restart PostgreSQL either; nothing is wrong with it. Instead, read the username in the error and recognise its shape. A generated dynamic username identifies the secrets engine as the owner of this credential and moves the incident to the right team immediately. Confirm the diagnosis from the audit device: find the revocation request, its timestamp and the token that issued it, and confirm it lands before the first application failure. Then prove the supporting systems are healthy by requesting a fresh credential for the same role and connecting with it from a bastion host. If that works, the database, the network and the secrets engine are all exonerated and the fault is entirely in the consumer. Restart the worker so it fetches a new credential, and accept that the run is lost. Re-run the job once it is authenticating. Revoke nothing else while the incident is open, including anything else on the audit list, because the same reasoning that produced this revocation produced the rest of that list.

Verification

Do not verify by reading the lease inventory in the secret manager, which only tells you what the manager believes it issued. Verify from the database, which is the party that actually accepts or refuses the credential. Query the activity view for the worker connections and confirm the connected username is a new generated name, different from the one in the failure. Confirm the database role for that new username exists in the catalogue and carries an expiry in the future, and watch that expiry move forward once as the renewer runs, which proves renewal is working rather than merely configured. Cross-check the audit device for a credential read at the restart time attributed to the worker AppRole accessor rather than an operator token, so you know the new credential arrived through the normal path and was not injected by hand during the incident. Confirm exactly one active lease exists for that role, because a worker restarted twice under pressure can easily leave a second lease behind that nobody renews. Finally, verify at the business level: the consolidation job completes, the downstream report lands, and the row counts reconcile against the previous successful run. A job that authenticates and then produces a short report has a different problem and it is not this one.

Prevention

Record the consumer at issue time. Every dynamic credential request should carry metadata naming the workload, the environment and an owner, so a lease inventory answers who is using this rather than how old is this. An audit that can only see age will keep producing lists of supposedly orphaned leases. Gate revocation of anything broader than a single lease behind a two-person rule and 24 hours of notice to the named owner, and forbid prefix revocation entirely during a change freeze. Single-lease revocation on a credential believed compromised stays immediate; that is the whole point of the mechanism and it must not be slowed down. Fix the consumers, because that is the durable control. A client that receives an authentication failure must return to the secret manager for a new credential rather than retrying a dead one: at most five attempts with exponential backoff capped at 30 seconds, then a fresh fetch. Alert when lease renewal fails twice within 60 seconds, which gives roughly half of a 120 second lease as warning. Run a quarterly game day in staging that revokes a live lease without notice and requires the workload to recover within 60 seconds with no human action, and treat a failure of that drill as a release blocker. Finally, teach the username shape: a generated dynamic username in an error message routes the incident in seconds.

Reported symptoms

The reporting platform runs a consolidation job every night against appdb. It takes about an hour. It draws a read-only database credential from the secrets engine at start-up, and a renewer keeps that credential alive for the length of the run. The design is deliberate: leases here are short, so a credential that escapes is worthless within minutes.

The run started at 21:23:43 UTC. At 21:27 it alerted.

The channel filled up with a very confident, entirely wrong diagnosis:

  • The job is stuck at sixty per cent. It finished its extraction phase and cannot open the connections it needs for the write-back phase.
  • The failure is a PostgreSQL fatal error saying a role does not exist. Somebody dropped a user. The database team is paged.
  • The database is fine by every measure anyone checks. Administrative connections work, the connection count is normal, replication is current, and its log contains nothing but the rejected logins.
  • Two other services take dynamic credentials from the same database and neither has noticed anything.
  • The secret manager is unsealed and answering. That is used, in the first five minutes, to rule it out.

Nobody restarts the worker. Forty minutes of the run are already spent and a restart throws them away. That decision is reasonable, and it is the reason the incident lasted fifty-one minutes instead of one.

Evidence provided

Read-only / Safethe run record: which lease this job holds, with the password redacted at write time
$ jq '{lease_id, lease_duration, renewable, data}' /var/log/report-worker/run-4471.json
{
"lease_id": "database/creds/app-readonly/xoHI541EXoFgKn1OTiusOdd9",
"lease_duration": 120,
"renewable": true,
"data": {
  "password": "[REDACTED]",
  "username": "v-token-app-read-ghRGRAxnCRE9Q8zLVsIw-1787779423"
}
}

Illustrative output

Read-only / Safereproducing the worker failure by hand at 21:34 UTC
$ psql -U v-token-app-read-ghRGRAxnCRE9Q8zLVsIw-1787779423 -d appdb -c "SELECT 1;"
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL:  role "v-token-app-read-ghRGRAxnCRE9Q8zLVsIw-1787779423" does not exist

Illustrative output

Read that username rather than the word FATAL. The generated prefix says the credential came from the secrets engine, not from a human, and that single fact routes the incident correctly.

Read-only / Safecaptured at 21:23:44, one second after the run started
$ bao lease lookup database/creds/app-readonly/xoHI541EXoFgKn1OTiusOdd9
expire_time     2026-08-26T21:25:43.891514518Z
issue_time      2026-08-26T21:23:43.891514368Z
renewable       true
ttl             1m59s

Illustrative output

Read-only / Safe200 milliseconds after issue: the credential was genuine and the database accepted it
$ psql -U v-token-app-read-ghRGRAxnCRE9Q8zLVsIw-1787779423 -d appdb -c "SELECT current_user, now();"
                   current_user                   |              now
--------------------------------------------------+-------------------------------
v-token-app-read-ghRGRAxnCRE9Q8zLVsIw-1787779423 | 2026-08-26 21:23:43.940523+00

Illustrative output

Read-only / Safethe same query at 21:23:45: the database holds its own expiry for this role
$ psql -U bao_admin -d appdb -c "SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolname LIKE 'v-token-app-read-%';"
                     rolname                      |     rolvaliduntil
--------------------------------------------------+------------------------
v-token-app-read-ghRGRAxnCRE9Q8zLVsIw-1787779423 | 2026-08-26 21:25:48+00

Illustrative output

Read-only / Safethe contrasting capture: the diagnosis lives in the difference
$ psql -U bao_admin -d appdb -c "SELECT rolname FROM pg_roles WHERE rolname LIKE 'v-token-app-read-%';"
Destructivefound in the operator shell history, timestamped 21:26
$ bao lease revoke database/creds/app-readonly/xoHI541EXoFgKn1OTiusOdd9
All revocation operations queued successfully!

Illustrative output

Read-only / Safethe server side of the same event, from a log the operator cannot edit
$ grep -F 'sys/leases' /openbao/audit/audit.log | tail -5

Work the evidence before reading on

Nothing here is broken in the sense of being faulty. Every component did precisely what it was configured to do, and the outage is entirely in the gaps between them.

  1. The catalogue query returns zero rows. Is that damage, or is it the expected result of a correctly executed operation? What would a partial result have told you instead?
  2. A fresh request for the same role succeeds during the incident and the new credential connects. Given that, how much of the estate can you exonerate in one command, and what is left?
  3. The database enforces its own expiry on the generated role, set at issue time. What does that tell you about who is authoritative for the lifetime of a dynamic credential, and what still happens if the secret manager becomes unreachable?
  4. The revocation is at 21:26 and the first failure is at 21:27. Which of the two things that had to be true for that gap to become an outage is the one you can actually fix?

Before continuing: say what the worker should have done in the millisecond after the database refused it, and why that is a property of the client and not of the secret manager.

Root cause

Revocation worked, and worked completely

The secrets engine holds revocation statements for the role. When the lease is revoked it runs them, the generated role is dropped, and the username stops existing. Every subsequent connection attempt is refused by the database at authentication, before any query runs.

That is the contract of a dynamic credential and the reason it is worth the complexity: the credential is real, unique to one consumer, and can be withdrawn in one command with no coordination. The zero rows in the catalogue are the receipt.

Nothing in the platform could say who was holding the lease

The quarterly access audit produced a list of lease identifiers, sorted by age, headed candidates for clean-up. It could not do better, because the credential request carried no metadata: no workload name, no environment, no owner. Age was the only available signal, and age was read as evidence of abandonment.

A long-lived lease on a short default duration is in fact evidence of the opposite. It means something has been renewing it continuously, which is exactly what a live consumer looks like.

The consumer could only obtain a credential by dying

The worker reads the credential once, at start, and passes it into the connection pool as configuration. There is no path from an authentication failure back to the secrets engine. The pool treats the refusal as a transient connection error, so it retries the same dead username, forever, with backoff.

A restart would have fixed it in under a second. The team did not restart, for a perfectly sound reason, and that is the honest shape of this incident: the only recovery available was one the operators were right to resist.

Resolution

  1. Rule out the fast wrong answer explicitly and say so in the channel: nobody recreates the database role by hand. It restores service in seconds and leaves a permanent unmanaged credential that no rotation, revocation or audit will ever reach. Do not restart PostgreSQL either, because nothing is wrong with it.
  2. Read the username in the failure rather than the severity keyword. A generated dynamic username names the secrets engine as the owner of the credential, and it is the fastest routing signal available in the whole incident.
  3. Confirm the timeline from the audit device rather than from anyone's memory: find the revocation request, its timestamp, and the accessor that issued it, and confirm it precedes the first application failure.
  4. Prove the supporting systems are healthy before touching the consumer. Request a fresh credential for the same role and connect with it from a bastion host. If that succeeds, the database, the network, the engine and the role configuration are all exonerated in one step, and everything remaining is in the client.
  5. Restart the worker so it fetches a new credential, and accept that the run is lost. This is the only recovery the current design offers, and delaying it does not make it cheaper.
  6. Re-run the consolidation job once the worker is authenticating, and confirm the run window still fits before the downstream report deadline. If it does not, tell the report consumers now rather than at 06:00.
  7. Revoke nothing else while the incident is open, including the remaining entries on the audit list. The reasoning that produced this revocation produced the rest of that list, and it has not been corrected yet.
  8. Record the lease identifier, the revocation time, the first failure time and the recovery time. The interval between the revocation and the correct diagnosis is the finding, and it is a property of the error text and the inventory, not of anyone's competence.

Verification

  1. Query the database activity view for the worker connections and confirm the connected username is a new generated name, different from the one in the failure. The database is the party that accepts or refuses the credential, so it is the only witness worth asking.
  2. Confirm a role for that new username exists in the catalogue and carries a validity horizon in the future.
  3. Watch that horizon move forward once as the renewer runs. Renewal configured and renewal working are different claims, and only the second one survives the next two minutes.
  4. Check the audit device for a credential read at the restart time attributed to the worker AppRole accessor rather than an operator token, which proves the new credential arrived by the normal path and was not injected by hand under pressure.
  5. Confirm exactly one active lease exists for the role. A worker restarted twice during an incident easily leaves a second lease that nobody is renewing, and it will expire quietly tonight.
  6. Confirm the consolidation job completes and the downstream report lands.
  7. Reconcile the row counts against the last successful run. A job that authenticates and then produces a short report has a different problem, and it will be blamed on this one.

Prevention

  • Record the consumer at issue time. Attach the workload name, the environment and an owner to every credential request, so the lease inventory answers who holds this rather than how old is this. An audit that can only see age will keep producing lists of leases it wrongly believes are abandoned.
  • Separate emergency revocation from clean-up revocation. Revoking a single lease believed compromised stays immediate and needs no approval; that is the entire value of the mechanism. Revoking a prefix, or anything from a list, needs a second pair of eyes, 24 hours of notice to the named owner, and no change freeze in progress.
  • Make consumers re-fetch on authentication failure. At most five attempts with exponential backoff capped at 30 seconds, then a fresh request to the secrets engine. This is the control that makes every other one optional, and it costs about twenty lines.
  • Alert on renewal failure, not on credential expiry. Two failed renewals within 60 seconds is roughly half a lease of warning on this configuration, and it fires before any user notices.
  • Drill it quarterly. Revoke a live lease in staging without notice, require recovery within 60 seconds and no human action, and treat a failed drill as a release blocker for that service.
  • Teach the username shape. Every engineer on call should be able to look at a generated dynamic username inside a database error and route the incident in ten seconds. That habit is worth more than any dashboard here.