Dynamic secrets for databases and cloud
Credentials that expire faster than attackers can use them.
In short. Vault dynamic secrets for databases and cloud: credentials that expire before attackers reuse them.
A static database password in your app config is a hotel room key that never stops working — copy it once and you are inside forever, long after checkout. Vault's dynamic secrets flip that model: the app asks Vault at boot, Vault mints a brand-new credential on the spot with a stamped expiry, hands it over, and schedules its own deletion. Every workload gets a unique, disposable identity that dies in an hour whether or not anyone remembers to clean up. An attacker who scrapes the credential from a heap dump or a leaked env var wins a countdown, not a key.
The database secrets engine
Vault does not store your app's database password — it holds a privileged admin account and uses it to create and drop short-lived users on demand. You configure a connection (the credentials Vault itself logs in with) and a role that carries the exact SQL Vault runs to mint a user. The templated {{name}}, {{password}}, and {{expiration}} are filled per request. Define revocation_statements explicitly: the Postgres plugin ships a bare default that only revokes public-schema privileges and drops the role, so anything beyond that — owned objects, grants in other schemas, open sessions — leaks expired accounts into the database unless you spell out the teardown yourself. Keep allowed_roles tight so a compromised role config can't reach an unrelated connection.
# enable the database secrets enginevault secrets enable database# tell Vault how to reach Postgres, using ITS OWN admin login (not the app's)vault write database/config/app-postgres \plugin_name=postgresql-database-plugin \allowed_roles="app-readwrite" \connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/appdb?sslmode=require" \username="vault_admin" \password="$VAULT_DB_BOOTSTRAP_PW"# a role = the SQL Vault runs to create and later drop a throwaway uservault write database/roles/app-readwrite \db_name=app-postgres \creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \default_ttl="1h" \max_ttl="24h"# the app calls this at boot to get a unique 1-hour uservault read database/creds/app-readwrite
Cloud credentials on demand
The same pattern maps onto AWS: instead of baking a long-lived access key into a CI job or a pod, Vault issues one scoped to a single IAM policy and expires it. The iam_user type creates a real IAM user per request — simple, but IAM is eventually consistent, so the key may need a few seconds and a retry before it authenticates. For high-churn callers prefer assumed_role with STS, which returns temporary credentials instantly and never litters your account with users. GCP and Azure engines follow the identical config-plus-role shape, so once you internalize this you can rotate the cloud provider without relearning the workflow.
# enable and give Vault a set of AWS creds it can mint children fromvault secrets enable awsvault write aws/config/root \access_key=$AWS_VAULT_ACCESS_KEY \secret_key=$AWS_VAULT_SECRET_KEY \region=us-east-1# a role scoped to exactly the permissions the workload needsvault write aws/roles/s3-reader \credential_type=iam_user \policy_document=-<<'EOF'{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["s3:GetObject", "s3:ListBucket"],"Resource": ["arn:aws:s3:::app-artifacts", "arn:aws:s3:::app-artifacts/*"]}]}EOF# mint a short-lived AWS key pairvault read aws/creds/s3-reader
Leases, renewal, and the kill switch
Every dynamic secret comes wrapped in a lease — the accounting record that ties a credential to its expiry. A long-running service renews before the lease lapses to keep the same credential alive up to max_ttl; a batch job just lets it expire. The lease is also your incident kill switch: revoke by ID to burn one credential, or revoke by prefix to invalidate every credential a compromised role ever handed out, in one command. This is the payoff over static secrets — containment is a single API call, not a coordinated password reset across every consumer.
# inspect a specific lease's remaining TTLvault write sys/leases/lookup lease_id=database/creds/app-readwrite/<lease_id># extend it (capped at the role's max_ttl)vault lease renew database/creds/app-readwrite/<lease_id># kill one credential immediatelyvault lease revoke database/creds/app-readwrite/<lease_id># incident response: nuke EVERY credential from a role at oncevault lease revoke -prefix aws/creds/s3-reader
DROP ROLE refuses to run if the dynamic user still owns any object or holds any grant — so if your creation statement ever does a CREATE TABLE or the app leaves an open transaction, revocation errors out and the lease sticks in Vault while a live, over-privileged account lingers in the database. Watch for it by alerting on Vault's lease-revocation failures and by periodically diffing pg_roles against Vault's active leases. The fix is a fuller revocation statement that reassigns and drops owned objects and terminates live backends before the role goes: REASSIGN OWNED BY "{{name}}" TO vault_admin; DROP OWNED BY "{{name}}"; SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = '{{name}}'; DROP ROLE IF EXISTS "{{name}}";. Test revocation deliberately — never assume the happy path cleaned up.