Skip to main content
RunBook Academy

Docker & ContainersXIV Β· SecretsRotation

Secret rotation β€” without breaking production

Intermediate⏱ ~22 min

What you'll learn

  • Classify an application by how it picks up a changed secret
  • Run an overlap-window rotation with the right database primitive
  • Detect a rotation that appeared to succeed but was never picked up
  • Verify that the old credential no longer works

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12

Not yet marked complete on this device.

Rotating a secret is two independent changes that people treat as one: the credential store gets a new value, and every process using the old value has to start using the new one.

The first is a one-line command. The second is the entire problem, and it has exactly two solutions. Either the application re-reads the secret, or you restart it. There is no third option. Every pattern below is a way of arranging one of those two around a window where both credentials work.

Which kind of application do you have?

Before designing a rotation, find out what you are rotating into.

BehaviourRotation needsHow common
Reads the secret on every useNothing. Replace the file.Rare
Re-reads on a timer or on file changeNothing, within the refresh intervalAgent-driven apps
Re-reads on SIGHUPA signalnginx, haproxy, many daemons
Reads once at start, caches foreverA restartThe overwhelming majority
Reads once, and holds a warm connection poolA restart, and the pool must drainMost database clients

The last row is the one that hides the failure, and it deserves its own note. A pooled database client authenticates when a connection is opened. Existing connections are already authenticated and stay valid whatever happens to the credential afterwards. So an application with a warm pool keeps working perfectly after you revoke its password β€” until a connection is closed for any reason and the pool tries to open a replacement.

That is why β€œwe rotated it and nothing broke” is not evidence that the rotation worked.

Read-only / Safeclassify the application
CONTAINER=api
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")

# Is the secret file still open? An open fd suggests it re-reads.
ls -l "/proc/$PID/fd" 2>/dev/null | grep -F secrets

# Is anything watching the file or its directory?
grep -c inotify "/proc/$PID/fdinfo/"* 2>/dev/null | grep -v ':0$'

# When did the process last exec? Compare with the last rotation.
ps -o lstart= -p "$PID"

No open descriptor and no inotify watch means the value is in memory and only a restart will change it. That is your answer, and it takes thirty seconds to get.

The failure this lesson exists for

The overlap window

The rule that prevents outages is simple and absolute: the new credential must work before the old one stops. Never revoke first.

How you get an overlap depends entirely on the database.

MySQL 8.0 and later has real dual passwords. The primary and secondary password both authenticate:

-- Set the new password, keeping the old one as secondary
ALTER USER 'api'@'%' IDENTIFIED BY 'REPLACE_ME_NEW' RETAIN CURRENT PASSWORD;

-- ... deploy, verify every client is on the new value ...

-- Then drop the old one
ALTER USER 'api'@'%' DISCARD OLD PASSWORD;

This is the cleanest primitive available. Note the documented limitations: it requires APPLICATION_PASSWORD_ADMIN (or CREATE USER), it fails on an account with an empty password, and changing the authentication plugin at the same time discards the secondary password.

PostgreSQL has no equivalent. ALTER ROLE ... PASSWORD replaces the password; a role has exactly one. Guidance that says otherwise is wrong, and following it produces a rotation that cuts over instantly with no window at all.

The Postgres pattern is two roles sharing one set of grants:

-- A group role owns the privileges
CREATE ROLE api_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO api_app;

-- Login roles are disposable and rotate independently
CREATE ROLE api_2026a LOGIN PASSWORD 'REPLACE_ME_A' IN ROLE api_app;
CREATE ROLE api_2026b LOGIN PASSWORD 'REPLACE_ME_B' IN ROLE api_app;

-- Rotation is: point clients at api_2026b, verify, then
DROP ROLE api_2026a;

Rotating the user rather than the password gives you the overlap the password mechanism does not. It also means the credential in your secret manager is a username and a password together, which is worth designing for on day one.

Dynamic credentials remove the problem entirely. Vault’s database secrets engine creates a role per lease with a short TTL and drops it when the lease ends. There is no long-lived password to rotate, no overlap to arrange, and no revocation step. If you are building this from scratch and the database supports it, this is the design to aim at β€” the rotation procedures below become unnecessary rather than automated.

Making an application re-read

For daemons that reload on a signal, the rotation step is a signal rather than a restart:

Read-only / Safereload without restart
CONTAINER=proxy

# Send the reload signal to PID 1 in the container
docker kill --signal=HUP "$CONTAINER"

# Confirm PID 1 did not change: a restart would have created a new one
docker inspect --format '{{.State.Pid}} started={{.State.StartedAt}}' "$CONTAINER"

That second command is the check. If StartedAt moved, the process did not reload β€” it crashed on the signal and the restart policy brought it back, which is a different and much more disruptive event.

Verification that can fail

A rotation is verified by two facts, and both need proving separately:

Read-only / Safeverify the rotation
CONTAINER=api
DB_HOST=db.example.com

# 1. The application can open a NEW connection, not just reuse old ones.
#    An endpoint that forces a fresh connection is ideal; a restart of one
#    replica is the universal version.
docker restart "$CONTAINER"
docker inspect --format '{{.State.Health.Status}}' "$CONTAINER"

# 2. The OLD credential no longer authenticates. This must FAIL.
#    Keep the retired value only for the length of this check, then delete it.
OLD_PASSWORD=$(cat /root/rotation/old-password)

docker run --rm -e PGPASSWORD="$OLD_PASSWORD" postgres:16 psql -h "$DB_HOST" -U api -c 'select 1' && echo 'FAIL: old credential still works' || echo 'old credential rejected'

Check 2 is the one that turns rotation from a ritual into a control. A rotation where the old credential still works has not reduced anyone’s exposure; it has only added a second valid credential.

And on the database side, confirm which identity is actually connected rather than trusting the application:

Read-only / Safewho is connected
$ psql -c "select usename, count(*), min(backend_start) from pg_stat_activity group by usename"
 usename    | count |          min
------------+-------+------------------------
api_2026b  |    24 | 2026-08-12 09:41:07+00
api_2026a  |     3 | 2026-07-30 22:15:52+00

Illustrative output

Three connections still on api_2026a, opened two weeks ago. Those are the connection-pool survivors. Drop that role now and three requests fail the moment those connections cycle. That table is the single most useful artefact in a rotation, and it exists in some form for every database worth using.

  1. Classify the application first. Does it re-read, reload on a signal, or need a restart? Thirty seconds of /proc beats an assumption.
  2. Create the overlap before you create the new secret. MySQL RETAIN CURRENT PASSWORD, or a second Postgres login role in the same group role.
  3. Deploy the new value to the secret store and render it.
  4. Restart or reload one instance and confirm it comes back healthy. This is the step that converts a deferred 03:00 failure into a 14:00 one.
  5. Roll the rest, then confirm on the database side that no session is still using the old identity.
  6. Revoke the old credential.
  7. Verify the old credential now fails. A rotation where it still works has added exposure, not removed it.

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. You rotate a database password and revoke the old one. The application keeps serving traffic with no errors for days. What has this proved?

  2. Q2. An application watches its secret file with inotify and never notices a rotation. Why?

  3. Q3. Which of these give you a genuine overlap window where two credentials both work? Select all that apply.

  4. Q4. The new credential must be accepted before the old one is revoked, which is why the overlap window exists.

  5. Q5. Which single step best converts a rotation failure that would surface at 03:00 into one you find during the change window?

Passing score: 75%. Answers are checked in this browser.