Skip to main content
RunBook Academy

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

intermediatesecret-lease~35 min

It worked for exactly fifteen minutes after every restart

Reported symptoms

  • At 21:41 UTC the orders API begins failing every request across all four instances, eighteen minutes after a rolling release that the deployment pipeline soaked for five minutes and recorded as successful
  • The failure text is a PostgreSQL fatal error naming a database role that does not exist, and the generated username inside it is long enough that most readers skim straight past it
  • The database is healthy on every measure anyone checks: administrative connections succeed, the connection count is low, replication is current, and the server log contains nothing but the rejected authentications
  • The secret manager is unsealed and answering, and a credential requested by hand during the incident works immediately, which is used to exonerate it in the first five minutes
  • The application health endpoint reported green throughout, so the load balancer never removed a single instance and every request continued to arrive at a process that could not serve it
  • Restarting the service fixes it completely and instantly, and it fails again fifteen minutes later, and the second restart buys exactly another fifteen minutes
  • Two other services drawing credentials from the same database role are unaffected for the entire incident

Evidence

  • · The unit journal records a credential read one second after the process started, reporting a lease duration of 120 seconds and renewable as true, and then contains nothing further about credentials at all
  • · A lease lookup captured immediately after that read shows an issue time of 21:23:43 and an expire time of 21:25:43, exactly two minutes later, with renewal available
  • · Filtering the audit device for that lease identifier returns one entry, the credential read itself, and no renewal request at any point in the lease lifetime, which is the decisive negative result
  • · The same filter applied to one of the unaffected services returns a renewal request against the same role every sixty seconds throughout the identical period
  • · Reproducing the application failure by hand with the generated username returns the PostgreSQL fatal error naming that role as non-existent
  • · A catalogue query for roles carrying the dynamic credential prefix shows the two unaffected services holding roles with expiry times in the future and nothing at all belonging to the orders API
  • · The release diff removes the agent from the unit dependencies and replaces it with a client library call in the application start-up path
  • · The connection pool is configured with a maximum connection lifetime of fifteen minutes, which matches the interval between each restart and the next failure exactly
  • · The health endpoint implementation returns the state of the pool object and never executes a query, so an instance unable to open a new connection reports itself healthy
Diagnosis and resolutionclick to reveal

Root cause

Two independent defects, and the second one is why this ran for fifty-seven minutes instead of three. The first is that renewal was removed. Release 4.9.0 deleted the sidecar agent from the unit and replaced it with a direct call to the client library at start-up, as part of a change described in review as a dependency clean-up. The library reads the credential once and returns it. Nothing in the new code path ever renews. The lease is 120 seconds and is marked renewable, and no renewal request was ever made for it, so at the expiry time the secrets engine did exactly what it promises: it ran the configured revocation statements, the generated database role was dropped, and the username stopped existing. The second defect is that nothing in the estate could observe the consequence in time to matter. The health endpoint reports the state of the connection pool object rather than executing a query, so it cannot fail while the process is running, and the rolling deployment soaked for five minutes against exactly that endpoint before marking the release complete. Meanwhile the pool holds sessions that PostgreSQL authenticated when they were opened and does not reauthenticate them, so every request continued to be served normally for thirteen minutes after the credential ceased to exist. The first failure arrived when the pool began retiring connections at their fifteen minute maximum lifetime. That second defect is the one that made this an outage. Had the health check executed a trivial query, the four instances would have gone unhealthy within seconds of 21:25:43, the deployment gate would have failed its own soak, and 4.9.0 would have rolled back automatically at 21:26 with no customer ever seeing it.

Remediation

Stop restarting. Each restart buys fifteen minutes, discards every in-flight request, and resets the one pattern capable of telling you what is wrong. Write the times down instead: process start, first failure, restart, next failure. Two intervals of identical length are worth more here than any dashboard, and they say plainly that something acquired at start-up has a short life and is not being refreshed. Refuse the fix that is about to be proposed. Somebody will suggest raising the lease duration from two minutes to a day. It will appear to work and it is the wrong answer: it adds no renewal, it moves the identical failure to the same time tomorrow, it multiplies the window in which a stolen credential remains useful by several hundred, and it converts a dynamic credential back into a static one with extra machinery attached. Do not create the database role by hand either. A role that exists outside the engine is rotated by nobody, revoked by nothing, and invisible to the next access review. Confirm the diagnosis from the audit device before changing anything. Filter it for the lease identifier and establish that there is a credential read and no renewal request, then run the same filter against a service that is working and observe renewals arriving on a regular cadence. The absence is the evidence, and it is only convincing next to the presence. Then roll back to the release that runs the renewing agent. It restores renewal, it is a change already proven in production, and it is considerably faster than writing a renewal loop under pressure at half past ten. Fix forward in daylight. Before you close, take the health endpoint out of the rollout gate until it executes a query, or the next release carrying this defect will also be marked successful.

Verification

Verify from the database and from the audit device rather than from the application view of itself, since that view is one of the two things that was wrong. Query the activity view for the connections belonging to the service and record the role name they are using, then repeat the query twenty minutes later and confirm the connections are still authenticating. Surviving an interval longer than the pool maximum connection lifetime is the only observation that distinguishes a repair from a restart, and anything shorter than fifteen minutes proves nothing whatsoever. Read the audit device for renewal requests attributed to the workload role and confirm they arrive on a regular cadence at or before half the lease duration. A renewer that is configured and a renewer that is running are different claims, and only the second one survives the next sixty seconds. Confirm exactly one active lease exists per instance. Four instances restarted three times under pressure leave leases behind, and an unrenewed lease expires quietly whether or not anybody is watching for it. Then break the health check deliberately in a staging environment by stopping the database, and confirm the endpoint now reports unhealthy within its check interval. Until that has been demonstrated, the rollout gate is decorative. Finally, confirm the customer facing error rate has been flat for longer than one pool lifetime before declaring the incident closed.

Prevention

Renew at half the lease. A renewer must attempt renewal at or before 50 per cent of the lease duration, which for a 120 second lease means every 60 seconds, and it must escalate rather than continue quietly after two consecutive failures. Alert on the absence of renewal rather than on the failure it eventually causes. If a lease marked renewable comes within 30 seconds of its expire time with no renewal recorded against it, page. On this configuration that fires 90 seconds before any user notices anything at all. Make a health check perform the work it claims to check. An endpoint reporting the state of a connection pool object reports that the object exists. One that executes a trivial query fails when the database refuses the credential, which is the entire reason the endpoint is wired into the load balancer and the rollout gate. Bound the detection delay with the pool. A maximum connection lifetime shorter than the credential lifetime makes a broken renewal visible within one lease period rather than a quarter of an hour later, at a cost of a few additional connections per minute. Treat any change to how a workload obtains a credential as a change to the credential itself. Removing an agent is not a dependency clean-up; it relocates responsibility for renewal into the application, and the review should ask who renews now and refuse the change until somebody can answer. Drill it quarterly in staging: block renewal for one workload without warning, require the failure to be visible within 60 seconds and the instance out of rotation within 90, and treat a missed drill as a release blocker.

Reported symptoms

The orders API runs on four instances, app-01 to app-04, behind a load balancer, and reads from appdb using a dynamic credential. The lease is short on purpose: two minutes, renewed continuously, so a credential lifted out of a process is worthless before anyone can carry it anywhere. Until this evening the arrangement had been running for eleven months without anybody thinking about it.

Release 4.9.0 rolled out between 21:20:10 and 21:23:42 UTC. The deployment gate soaked for five minutes, found all four instances healthy at 21:28:45, and marked the release complete.

  • At 21:35 the first instance starts failing and by 21:41 all four are, in the same order and at the same spacing as the rollout, which nobody notices at the time. The failures did not start at deployment time, which is used within a minute to rule the release out.
  • The error is a PostgreSQL fatal message about a role that does not exist. The database team is pulled in.
  • appdb is healthy on every measure. Administrative connections work, the connection count is low, replication is current, and its log carries nothing but the rejected logins.
  • The secret manager is unsealed. A credential requested by hand during the incident works immediately, and that is taken as clearing it.
  • The health endpoint is green on all four instances throughout, so the load balancer keeps sending traffic to processes that cannot serve it.

At 21:47 somebody restarts the service. Everything recovers instantly. At 22:02 it fails again. A second restart at 22:05 buys another recovery, and at 22:20 it fails again. It is the third identical interval that finally gets someone to look at a clock.

Evidence provided

Read-only / Safeapp-02 - what the process did about credentials, and when it stopped
$ journalctl -u orders-api.service --since '21:23' --until '21:45' --no-pager
Read-only / Safeapp-02 at 21:23:44 - a two minute lease that something is expected to keep alive
$ 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 renewable true as an offer rather than a promise. The engine will extend this lease whenever it is asked. It has no way to ask on anyone’s behalf, and it will not.

Read-only / Safethe audit device - every request that touched this lease, from a log nobody can edit
$ grep -F 'database/creds/app-readonly/xoHI541EXoFgKn1OTiusOdd9' /openbao/audit/audit.log | jq -r '.request.path' | sort | uniq -c
Read-only / Safethe same log, measured the other way - who is renewing anything at all
$ grep -F 'sys/leases/renew' /openbao/audit/audit.log | jq -r '.auth.metadata.role_name' | sort | uniq -c
Read-only / Safereproducing the application failure by hand at 22:24, using the username from the log
$ 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-only / Safeappdb - which generated roles still exist, and whose they are
$ psql -U bao_admin -d appdb -c "SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolname LIKE 'v-token-%' ORDER BY rolvaliduntil;"
Read-only / Safethe release that shipped at 21:20, reviewed as a dependency clean-up
$ git diff v4.8.3..v4.9.0 -- deploy/orders-api.service src/bootstrap/database.go
Read-only / Safeapp-02 - the number that decides when the failure becomes visible
$ grep -n 'max_lifetime' /etc/orders-api/pool.toml

Work the evidence before reading on

Nothing in this incident is faulty. The database is well, the secret manager is well, and the secrets engine performed the operation it guarantees, on time. The whole outage lives in the distance between an event and its symptom.

  1. app-02 took its lease at 21:23:43 and it expired at 21:25:43. That instance did not fail until 21:38:45. Account for those thirteen minutes, using one number from the evidence.
  2. Every restart restores the service and it fails again fifteen minutes later, three times. Which of the two intervals in this incident is fifteen minutes long, and which is two, and which one is the cause?
  3. The audit device shows one credential read and no renewals for this workload, and regular renewals for two others against the same role. Say what that pair of observations proves and, more usefully, what it rules out.
  4. The health endpoint was green while every request was failing. What is it actually measuring, and what would it have had to do instead for the deployment gate to have caught this at 21:26?

Before continuing: name the component that was supposed to keep this credential alive, and say what shipped in its place.

Root cause

The renewal was deleted, and nothing replaced it

Release 4.9.0 removed the agent from the unit and moved the credential fetch into the application, calling the client library once during start-up. The change was reviewed as a dependency clean-up, which is a fair description of the diff and a poor description of its effect: the agent was not a dependency, it was the component performing renewal.

The lease was 120 seconds and marked renewable. Renewable means the engine will extend the lease when asked. Nothing asked. At 21:25:43 the lease reached its expire time, the engine ran the revocation statements configured on the role, the generated database role was dropped, and the username ceased to exist. Every part of that is the system working correctly, and the audit device records it as an uneventful lease expiry rather than as anything resembling an error.

The pool kept serving with a credential that no longer existed

PostgreSQL authenticates a connection when the connection is opened. It does not reauthenticate on each statement, and dropping a role does not close sessions that were already established under it. The pool had opened its connections at 21:23:45 with a credential that was valid at that moment, and it went on using them.

So for thirteen minutes the service was, from the outside, completely healthy while holding a credential that had been revoked. The failure arrived when the pool started retiring connections at their fifteen minute maximum lifetime and could not replace them. That is the source of the interval, and it is why restarting appeared to fix the problem: a restart obtains a fresh credential, opens a fresh pool, and starts the same fifteen minute countdown again.

The health check could not fail

The endpoint wired into the load balancer and into the deployment gate returns the state of the connection pool object. The pool object was fine. It existed, it had a configuration, and it reported a count. It never attempted to obtain a connection, so it never discovered that it could not.

That is what allowed the rollout to complete: the gate soaked for five minutes, comfortably spanning 21:25:43, and asked four instances a question none of them could answer wrongly. It is also why no instance removed itself from rotation once the failures began, so all four kept receiving traffic they could not serve for fifty-seven minutes.

Resolution

  1. Stop restarting the service. Each restart buys fifteen minutes, throws away every in-flight request, and resets the only pattern capable of identifying the fault. Write the four times down instead: process start, first failure, restart, next failure.
  2. Refuse the lease extension before anyone builds it. Raising the lease from two minutes to a day adds no renewal, relocates the identical failure to the same time tomorrow, and multiplies the window in which a stolen credential is useful by several hundred. Refuse the hand-made role for the same reason: recreating the generated username in PostgreSQL with a chosen password restores service in seconds and leaves an account that nothing rotates, nothing revokes and no access review can see.
  3. Confirm the diagnosis from the audit device rather than from the application logs. Filter for the lease identifier and establish that there is a credential read and no renewal request anywhere in the lease lifetime.
  4. Run the same filter against a workload that is working. Renewals arriving every sixty seconds for two other services against the same role are what turn an absence into evidence, because they prove renewal is available and functioning on this cluster.
  5. Roll back to 4.8.3, which runs the agent. Rolling back restores renewal, it is a change already proven in this environment, and it is far faster and far safer than writing a renewal loop under pressure at half past ten at night.
  6. Confirm recovery over an interval longer than the pool maximum connection lifetime before standing down. Anything under fifteen minutes of stability is indistinguishable from the three restarts that preceded it.
  7. Remove the health endpoint from the rollout gate until it executes a query. Leaving it wired in means the next release carrying this defect will also soak cleanly and also be marked successful.
  8. Record the interval between the lease expiry at 21:25:43 and the failure of that instance at 21:38:45. That gap is the finding, and it belongs to the pool configuration rather than to anyone's judgement during the incident.

Verification

  1. Query the database activity view for the connections belonging to the service and record the generated role name they are authenticating as. The database is the party that accepts or refuses the credential, so it is the only witness whose answer settles anything.
  2. Repeat that query twenty minutes later and confirm connections are still being established. Surviving an interval longer than the pool maximum connection lifetime is the only observation that distinguishes a repair from a restart.
  3. Read the audit device for renewal requests attributed to the workload role and confirm they arrive on a regular cadence at or before half the lease duration. Renewal configured and renewal running are different claims, and only the second one survives the next sixty seconds.
  4. Confirm the role in the database catalogue carries a validity horizon in the future, and watch that horizon move forward once. A horizon that never advances is a renewal that is being requested and refused, which is a different fault with an identical symptom.
  5. Confirm exactly one active lease exists per instance. Four instances restarted three times under pressure leave leases nobody is renewing, and each of those will expire quietly tonight whether or not anyone is watching.
  6. Stop the database in a staging environment and confirm the health endpoint now reports unhealthy within its check interval. Until that has been demonstrated once, the rollout gate and the load balancer are both asking a question that cannot be answered wrongly.
  7. Confirm the customer facing error rate has been flat for longer than one pool lifetime, and reconcile the order counts for the incident window so that anything lost between 21:38 and 22:38 is known rather than assumed.

Prevention

  • Renew at half the lease. A renewer attempts renewal at or before 50 per cent of the lease duration, which on a 120 second lease means every 60 seconds, and it escalates rather than continuing quietly after two consecutive failures. Whether that runs in an agent or in the application matters far less than whether it runs at all.
  • Alert on the absence of renewal, not on the failure it eventually causes. If a lease marked renewable comes within 30 seconds of its expire time with nothing recorded against its renew path, page. On this configuration that fires roughly 90 seconds before a single user notices anything.
  • Make the health check do the work it claims to check. An endpoint that reports the state of a pool object reports that the object exists. One that executes a trivial query fails when the database refuses the credential, which is the entire reason it is wired into a load balancer and a deployment gate.
  • Bound the detection delay with the pool. Setting the maximum connection lifetime below the credential lifetime forces reauthentication once per credential period, so a broken renewer becomes visible within one lease instead of thirteen minutes later. It costs a few extra connections a minute.
  • Treat a change to how a workload gets a credential as a change to the credential. Removing an agent is not a dependency clean-up. It relocates the renewal obligation into the application, and the review should refuse the change until somebody can name what renews now.
  • Drill it quarterly. Block renewal for one staging workload without warning. Require the failure to be visible within 60 seconds and the instance out of rotation within 90, and treat a missed drill as a release blocker for that service.
  • Borrow the instrumentation from the platform courses. Observability for Production Sysadmins - Part XI (Blackbox) covers probing a service the way a client does rather than the way it describes itself, and Part XVIII (AlertingRules) covers expressing an alert on something that failed to happen, which is the harder half of this. Linux for Production Sysadmins - Part LXXII (Secrets) covers running a credential agent alongside a unit and the ordering that keeps the two in step.