In this episode we'll cover Dynamic Secrets — the paradigm where database credentials are no longer permanent, but generated on-demand with short TTLs and automatic destruction. We'll integrate Vault with PostgreSQL, write roles, and read dynamic credentials.

After covering the KV Secrets Engine in episode 4 — how to store static secrets with versioning and recovery — this episode moves one level higher: the Dynamic Database Secrets Engine. This is one of the features that most distinguishes Vault from just a "password storage place," and the main reason many companies choose Vault over other static secret managers.
Why does this topic matter in the real world? Because static database credentials are one of the biggest security risks of the modern era. A database password created once and used for years — scattered across config files, spreadsheets, or even Git commits — is a nightmare for security teams. When one employee leaves or one server is hacked, those credentials must be manually rotated across all systems. Often, that never happens.
Dynamic secrets completely change this paradigm: credentials are no longer "stored," but "borrowed." Imagine the difference between giving everyone in the office a permanent ID card versus providing a visitor badge that automatically expires at the end of the day. The second is clearly safer — even if it leaks, the badge is already useless. Let's dissect how Vault makes this happen.
Dynamic secrets are credentials generated by Vault on-demand, given to applications when needed, and automatically destroyed (revoked) when their lifetime (TTL) ends.
The flow goes like this:
vault read database/creds/my-role).This means your database is populated with temporary users that are born and die automatically — without human intervention. Its biggest advantages:
| Aspect | Static Credentials | Dynamic Secrets |
|---|---|---|
| Lifetime | Years / permanent | Minutes to hours (per TTL) |
| When leaked | Dangerous — needs emergency manual rotation | Nearly harmless — auto-expires |
| Rotation | Manual, often forgotten | Automatic, every time the lease ends |
| Visibility | Scattered across many files/systems | Centralized in Vault + audit log |
| User management | DBAs create users manually | Vault creates & deletes automatically |
Important
The key philosophical difference: KV stores the secret value, while the Database engine creates secrets. Vault never stores dynamic credentials — it stores the recipe for creating them, and every request produces a unique new user.
For this episode's practice, make sure there's a PostgreSQL (or MySQL) instance reachable by Vault. If using Docker:
docker run -d --name vault-demo-db \
-e POSTGRES_USER=vault_admin \
-e POSTGRES_PASSWORD=vault_admin_pass \
-e POSTGRES_DB=appdb \
-p 5432:5432 \
postgres:16Caution
Vault needs a user with high privileges (usually a superuser or one with GRANT OPTION) to be able to create/delete other users. On PostgreSQL, the user configured in Vault must have the CREATEROLE and CREATE DATABASE rights (or access to the target database), because Vault will run SQL statements like CREATE ROLE ... LOGIN PASSWORD '...'.
vault secrets enable databaseOutput:
Success! Enabled the database secrets engine at: database/Next, tell Vault how to connect to our database. This is called a database connection config:
vault write database/config/postgres \
plugin_name="postgresql-database-plugin" \
allowed_roles="my-role" \
connection_url="postgresql://{{username}}:{{password}}@127.0.0.1:5432/appdb" \
username="vault_admin" \
password="vault_admin_pass"Output:
Success! Data written to: database/config/postgresFor MySQL, the command is nearly identical, only the plugin and connection URL differ:
vault write database/config/mysql \
plugin_name="mysql-database-plugin" \
allowed_roles="my-role" \
connection_url="{{username}}:{{password}}@tcp(127.0.0.1:3306)/appdb" \
username="root" \
password="root_pass"Tip
Notice the {{username}} and {{password}} placeholders in the connection_url. Vault will automatically fill those placeholders with the admin credentials you provide as the username and password arguments — so you don't need to write the admin credentials literally twice inside the URL string.
A connection config alone isn't enough — Vault needs a role that defines what kind of credentials to create and how long they last. A role contains creation statements (the SQL executed when creating the user) and a default TTL.
vault write database/roles/my-role \
db_name="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}}\";" \
default_ttl="1h" \
max_ttl="24h"Output:
Success! Data written to: database/roles/my-roleBehind the scenes, Vault substitutes the placeholders in the creation statements:
| Placeholder | Replaced with | Example result |
|---|---|---|
{{name}} | Random username generated by Vault | v-token-my-role-1aB2c3D4 |
{{password}} | Random password generated by Vault | e2Jkf9sLpQx... |
{{expiration}} | User expiration time (ISO) | 2026-08-02T09:15:00+00:00 |
Warning
In PostgreSQL, the VALID UNTIL '{{expiration}}' statement isn't just decoration — it makes that user expire at the database level at the same time as the Vault lease. This is a double security layer: even if the Vault lease somehow fails to be revoked, the database itself will reject the user's login after the expiration time. Always include the {{expiration}} placeholder in your creation statements.
This is the most satisfying moment. Request credentials from Vault:
vault read database/creds/my-roleOutput:
Key Value
--- -----
lease_id database/creds/my-role/7k3x0qWXy9LpQz8VfR2nA4dE
lease_duration 1h
lease_renewable true
password e2Jkf9sLpQxZ7mN4vB8cW1tY6uI3oP5q
username v-token-my-role-1aB2c3D4Notice three important things:
lease_id — the unique identity of this credentials "borrowing." This lease is what lets Vault track and revoke the associated credentials.lease_duration: 1h — the lifetime according to the default_ttl we set in the role.username: v-token-my-role-... — a new user genuinely created in our PostgreSQL database.To prove the user actually exists, check in PostgreSQL:
SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolname LIKE 'v-token%';Output:
rolname | rolvaliduntil
----------------------------------+----------------------------
v-token-my-role-1aB2c3D4 | 2026-08-02T09:15:00+00:00The rolvaliduntil column shows that this user does have a time limit — real proof of the short-lived credentials concept.
The most convincing proof is to actually connect to the database with them:
PGPASSWORD="e2Jkf9sLpQxZ7mN4vB8cW1tY6uI3oP5q" \
psql -h 127.0.0.1 -U v-token-my-role-1aB2c3D4 -d appdb -c "SELECT current_user;"Output:
current_user
--------------
v-token-my-role-1aB2c3D4This is what happens in real applications every time a service needs database access: request credentials from Vault, use them to open a connection, then let them die along with the lease. One thing you should note well: never write these dynamic credentials to config files or persistent environment variables — they are designed to be temporary. If an application stores them, you've just turned a dynamic secret into a static one.
When the lease reaches the end of its life, Vault performs revocation — it runs the deletion statements defined during setup. Vault automatically generates default revocation statements based on the plugin, which essentially executes DROP ROLE "username" (PostgreSQL) or DROP USER ... (MySQL).
You can also revoke manually sooner if a leak occurs:
vault lease revoke database/creds/my-role/7k3x0qWXy9LpQz8VfR2nA4dEOr revoke all credentials from a specific role at once:
vault lease revoke -prefix database/creds/my-roleImportant
The lease concept will be covered thoroughly in episode 12, but one thing you must understand now: dynamic credentials are not eternal. Applications using them must be able to request new credentials periodically (renewal) or restart their connections before the TTL expires. This is what's called connection lifecycle management — one of the hardest aspects of adopting dynamic secrets.
This is the classic trap that makes teams give up on dynamic secrets: an already-established TCP connection is not automatically cut when the user is revoked. In PostgreSQL, once a connection is authenticated, it stays alive even if the role is DROPped. This means an application can keep using a "dead user" as long as its connection isn't closed.
The impact is real: applications using a connection pool with long-lived connections will keep running with revoked credentials — while also leaving "zombie" users in the database, because Vault can't delete users that still have active connections (PostgreSQL rejects DROP ROLE if the role is still in use).
Common strategies:
| Strategy | How It Works | Best for |
|---|---|---|
| Short TTL + reconnect | The application requests new credentials at short intervals, the pool is refreshed | Modern applications with retry logic |
max_conn_lifetime in the pool | Limit the maximum connection age in the connection pool (e.g. 50% of TTL) | Go/Python/Node driver pools |
| Connection pooling layer (PgBouncer) | Pooling at the middleware layer, credentials refreshed there | Legacy architectures that are hard to change |
| Static roles | User stays created but password rotated by Vault | Applications that can't restart connections |
Sometimes there are applications that can't use dynamic credentials — for example, legacy applications that connect once at startup and have no reconnect mechanism. For this case Vault provides static roles: the database user is created permanently, but its password is rotated by Vault periodically and automatically.
vault write database/static-roles/legacy-app \
db_name="postgres" \
username="legacy_app_user" \
rotation_statements="ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';" \
rotation_period="4h"The difference from a dynamic role: the username stays fixed (legacy_app_user), and Vault generates a new password every rotation_period. Applications using these credentials must get the latest password through another mechanism (for example, Vault Agent templates, which we'll cover in episodes 14-15).
One security practice that's often forgotten: the admin credentials we gave Vault in the config step (vault_admin/vault_admin_pass) should also be rotated — because now Vault holds them, and Vault can rotate its own database root credentials:
vault write -f database/rotate-root/postgresOutput:
Success! Data written to: database/rotate-root/postgresAfter this command, the vault_admin password in PostgreSQL changes to a new random password known only to Vault. Humans and other systems no longer hold the database admin credentials — only Vault can manage them. This is a perfect example of the least privilege and single source of truth principles.
| Mistake | Symptom | Solution |
|---|---|---|
Forgetting the {{expiration}} placeholder | User doesn't expire at the DB level | Always include VALID UNTIL '{{expiration}}' |
allowed_roles doesn't list the role | vault write database/roles/... is rejected | Make sure allowed_roles contains every role to be created |
| Vault admin user lacks CREATEROLE | Permission error when creating a user | Give the admin user sufficient rights in the database |
| TTL longer than needed | "Junk" users accumulate in the DB | Set default_ttl as short as possible (e.g. 1h) |
| Application doesn't handle lease renewal | Connections drop suddenly when the TTL ends | Apply reconnect logic / shorten the TTL to match the cycle |
| Connection URL has wrong host/port | Connection refused error | Test connectivity from the Vault server with psql/mysql first |
Missing plugin_name | Role creation fails with "unable to initialize connection" | Specify the right plugin_name (postgresql-database-plugin / mysql-database-plugin) |
In episode 5, we've covered the core concept of Dynamic Secrets: credentials generated on-demand, with short TTLs, that auto-destroy. You've practiced the entire flow — enabling the database secrets engine, configuring the PostgreSQL/MySQL connection, writing roles with creation_statements, reading dynamic credentials with their leases, and understanding the revocation lifecycle and the trap of connections that don't die on their own. We also covered static roles as a complement for legacy applications.
The essence of this episode: dynamic credentials shift the security strategy from "protecting secrets" to "making leaked secrets useless." This is a completely different mindset from simply storing passwords in a safe.
In episode 6, we'll cover the secrets engine that protects a deeper layer: the Transit Secrets Engine (Encryption-as-a-Service) — where Vault encrypts and decrypts sensitive data like credit card numbers and national IDs without ever storing the data. Keep your enthusiasm up!