Vault Dynamic Database Secrets: Designing Your App for Credentials That Disappear
Vault's dynamic database secrets engine creates real, short-lived database users on demand and deletes them at lease expiry. Here's how it works, a working PostgreSQL setup, and the connection-pool design decisions it forces on your application.
06 Aug 2025, 00:25 UTC

Most database password leaks aren't dramatic. A connection string ends up in a CI log, a config file gets committed, a former contractor still has the password in an old laptop image. The common thread: the credential was static, so it stayed valid long after it escaped. Vault's dynamic database secrets engine attacks exactly that problem — instead of storing one shared password, Vault creates a real, unique database user on demand, hands it to your service with a time-to-live (TTL), and deletes the user when the lease ends.
The catch is that "credentials that disappear" is an application design constraint, not just a security feature. This post walks through how the engine actually works, a working configuration against PostgreSQL, and the connection-handling decisions you need to make before you turn it on.
What Vault actually does when you read a credential
When a client reads from database/creds/<role>, Vault doesn't look up a stored password. It connects to the database using a privileged account you configured, runs the role's creation statements (SQL that creates a user and grants it privileges), and returns the new username and password along with a lease. The generated username typically looks like v-token-myrole-3f9a1c... — you can watch these users appear in pg_roles on PostgreSQL.
When the lease expires or is revoked, Vault runs revocation statements that drop that user. Two consequences follow:
- A leaked credential self-destructs. Its blast radius is bounded by the remaining TTL, not by the next time someone remembers to rotate.
- Revocation becomes an incident-response lever.
vault lease revoke -prefix database/creds/myrolekills every live credential for that role at once. You can't do that with a password in an environment variable.
A minimal working setup
Run these against a Vault server where you have permission to enable secrets engines and write database config (in dev, the root token works; in production, a policy scoped to database/*). The target is a PostgreSQL instance reachable from Vault, with an admin account Vault can use to create users.
# Enable the engine (run as a Vault operator, e.g. via vault CLI)
vault secrets enable database
# Point Vault at PostgreSQL. Rotate the admin password immediately so
# only Vault knows it.
vault write database/config/ordersdb \
plugin_name=postgresql-database-plugin \
allowed_roles="orders-role" \
connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/orders?sslmode=require" \
username="vault_admin" \
password="initial-admin-password"
vault write -force database/rotate-root/ordersdb
# Define a role: what SQL creates the user, and how long it lives
vault write database/roles/orders-role \
db_name=ordersdb \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"Then any authorized client can fetch a credential:
vault read database/creds/orders-role
# Returns: username, password, lease_id, lease_durationTo verify the mechanism end to end, read a credential, then in PostgreSQL run SELECT usename FROM pg_roles WHERE usename LIKE 'v-%'; — the generated user should be there. Then run vault lease revoke <lease_id> and query again; the user should be gone. If it isn't, check your revocation statements and Vault's logs before assuming the engine works.
The engineering decision: how your app survives rotation
This is where most adoption efforts stumble. A process that reads a credential once at startup and opens a connection pool will, one hour later, be holding a pool full of connections authenticated as a user that no longer exists. Existing connections may keep working briefly (dropping a role doesn't necessarily kill its sessions — behavior varies by database), but reconnects will fail.
The workable patterns:
- Re-fetch and rebuild the pool. A background task (or Vault Agent rendering credentials to a file your app watches) fetches fresh credentials before expiry. Your app then builds a new connection pool with the new user and drains the old one — you replace the pool rather than mutating live connections. Most pool libraries support graceful drain; if yours doesn't, that's a blocker to fix first.
- Renew the lease instead.
vault lease renewextends the same credential's life up tomax_ttl. This avoids user churn but only delays the problem — at max_ttl you must re-fetch and rebuild anyway. - Use static roles for apps that can't do either. A static role makes Vault rotate the password of one pre-existing database account on a schedule. Your app still needs to pick up the new password, but no users are created or dropped, which suits legacy systems and databases where role churn is expensive.
Trade-offs worth pricing in
Availability coupling. Issuing a new credential requires Vault and the database to be reachable. If Vault is down when your pool needs to rotate, you need a grace strategy: renew early (don't wait until 99% of TTL has elapsed), alert on renewal failures, and size TTLs so a brief Vault outage isn't an outage for you. One-hour TTLs with renewal attempted at the halfway point is a reasonable starting posture.
User churn. Very short TTLs mean constant CREATE/DROP ROLE traffic. PostgreSQL handles this fine at moderate rates, but some databases (and some auditing setups) don't. Watch catalog bloat and auth logging volume before pushing TTLs below a few minutes.
The root credential. The admin account Vault uses to create users is a single point of failure. Rotate it with database/rotate-root right after configuration so only Vault knows it — and understand that losing Vault's access to it breaks the engine until reconfigured.
Where to start
Spin up a throwaway PostgreSQL container and a dev Vault server (vault server -dev), apply the config above, and run the pg_roles verification. Then point a small test app at it, force a re-fetch, and confirm your pool rebuilds without dropping requests. That last test — not the Vault configuration — is the part that determines whether dynamic secrets will work for your service. If the pool rebuild is painful, start with a static role and migrate to dynamic credentials once the app's connection lifecycle is under your control.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.