Docker & ContainersXIV Β· SecretsRotation
Secret rotation β without breaking production
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
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.
| Behaviour | Rotation needs | How common |
|---|---|---|
| Reads the secret on every use | Nothing. Replace the file. | Rare |
| Re-reads on a timer or on file change | Nothing, within the refresh interval | Agent-driven apps |
Re-reads on SIGHUP | A signal | nginx, haproxy, many daemons |
| Reads once at start, caches forever | A restart | The overwhelming majority |
| Reads once, and holds a warm connection pool | A restart, and the pool must drain | Most 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.
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:
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:
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:
$ 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+00Illustrative 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.
- Classify the application first. Does it re-read, reload on a signal, or need a restart? Thirty seconds of
/procbeats an assumption. - Create the overlap before you create the new secret. MySQL RETAIN CURRENT PASSWORD, or a second Postgres login role in the same group role.
- Deploy the new value to the secret store and render it.
- 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.
- Roll the rest, then confirm on the database side that no session is still using the old identity.
- Revoke the old credential.
- Verify the old credential now fails. A rotation where it still works has added exposure, not removed it.
Knowledge check
Knowledge check Β· 5 questions
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?
Q2. An application watches its secret file with inotify and never notices a rotation. Why?
Q3. Which of these give you a genuine overlap window where two credentials both work? Select all that apply.
Q4. The new credential must be accepted before the old one is revoked, which is why the overlap window exists.
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.