Learn Vault - Dynamic Database Secrets Engine (Short-Lived Credentials)
Episode 5 of 26

Learn Vault - Dynamic Database Secrets Engine (Short-Lived Credentials)

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.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

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.

Main Discussion

The Dynamic Secrets Concept

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:

  1. The application requests credentials from Vault (vault read database/creds/my-role).
  2. Vault calls the database API, creating a new user + random password in PostgreSQL/MySQL.
  3. Vault returns that username/password along with a lease (a usage contract with a time limit).
  4. The application uses those credentials to connect to the database.
  5. When the TTL expires, Vault automatically deletes that user from the database.

This means your database is populated with temporary users that are born and die automatically — without human intervention. Its biggest advantages:

AspectStatic CredentialsDynamic Secrets
LifetimeYears / permanentMinutes to hours (per TTL)
When leakedDangerous — needs emergency manual rotationNearly harmless — auto-expires
RotationManual, often forgottenAutomatic, every time the lease ends
VisibilityScattered across many files/systemsCentralized in Vault + audit log
User managementDBAs create users manuallyVault 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.

Prerequisite: A Running PostgreSQL/MySQL Database

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:16

Caution

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 '...'.

Step 1: Enable the Secrets Engine

Enable the database secrets engine
vault secrets enable database

Output:

vault secrets enable database output
Success! Enabled the database secrets engine at: database/

Step 2: Configure the Database Connection

Next, tell Vault how to connect to our database. This is called a database connection config:

Configure the PostgreSQL connection in Vault
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:

vault write database/config output
Success! Data written to: database/config/postgres

For MySQL, the command is nearly identical, only the plugin and connection URL differ:

Configure the MySQL connection in Vault
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.

Step 3: Write Database Roles

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.

Write a dynamic credentials role
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:

vault write database/roles output
Success! Data written to: database/roles/my-role

Behind the scenes, Vault substitutes the placeholders in the creation statements:

PlaceholderReplaced withExample result
{{name}}Random username generated by Vaultv-token-my-role-1aB2c3D4
{{password}}Random password generated by Vaulte2Jkf9sLpQx...
{{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.

Step 4: Read Dynamic Credentials

This is the most satisfying moment. Request credentials from Vault:

Read dynamic credentials
vault read database/creds/my-role

Output:

vault read database/creds/my-role output
Key                Value
---                -----
lease_id           database/creds/my-role/7k3x0qWXy9LpQz8VfR2nA4dE
lease_duration     1h
lease_renewable    true
 
password           e2Jkf9sLpQxZ7mN4vB8cW1tY6uI3oP5q
username           v-token-my-role-1aB2c3D4

Notice three important things:

  1. lease_id — the unique identity of this credentials "borrowing." This lease is what lets Vault track and revoke the associated credentials.
  2. lease_duration: 1h — the lifetime according to the default_ttl we set in the role.
  3. username: v-token-my-role-... — a new user genuinely created in our PostgreSQL database.

To prove the user actually exists, check in PostgreSQL:

Check the dynamic user in PostgreSQL
SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolname LIKE 'v-token%';

Output:

psql output
             rolname              |       rolvaliduntil
----------------------------------+----------------------------
 v-token-my-role-1aB2c3D4         | 2026-08-02T09:15:00+00:00

The 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:

Connect using dynamic credentials
PGPASSWORD="e2Jkf9sLpQxZ7mN4vB8cW1tY6uI3oP5q" \
  psql -h 127.0.0.1 -U v-token-my-role-1aB2c3D4 -d appdb -c "SELECT current_user;"

Output:

psql output with dynamic credentials
 current_user
--------------
 v-token-my-role-1aB2c3D4

This 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.

The Lifecycle: What Happens When the Lease Expires?

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:

Manually revoke dynamic credentials
vault lease revoke database/creds/my-role/7k3x0qWXy9LpQz8VfR2nA4dE

Or revoke all credentials from a specific role at once:

Revoke by prefix
vault lease revoke -prefix database/creds/my-role

Important

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.

Pitfall: Old Database Connections Don't Die on Their Own

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:

StrategyHow It WorksBest for
Short TTL + reconnectThe application requests new credentials at short intervals, the pool is refreshedModern applications with retry logic
max_conn_lifetime in the poolLimit 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 thereLegacy architectures that are hard to change
Static rolesUser stays created but password rotated by VaultApplications that can't restart connections

Complement: Static Roles

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.

Write a static role
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).

Bonus: Database Root Credential Rotation

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:

Rotate the database root credentials
vault write -f database/rotate-root/postgres

Output:

vault write -f database/rotate-root output
Success! Data written to: database/rotate-root/postgres

After 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.

Common Dynamic Database Secrets Mistakes

MistakeSymptomSolution
Forgetting the {{expiration}} placeholderUser doesn't expire at the DB levelAlways include VALID UNTIL '{{expiration}}'
allowed_roles doesn't list the rolevault write database/roles/... is rejectedMake sure allowed_roles contains every role to be created
Vault admin user lacks CREATEROLEPermission error when creating a userGive the admin user sufficient rights in the database
TTL longer than needed"Junk" users accumulate in the DBSet default_ttl as short as possible (e.g. 1h)
Application doesn't handle lease renewalConnections drop suddenly when the TTL endsApply reconnect logic / shorten the TTL to match the cycle
Connection URL has wrong host/portConnection refused errorTest connectivity from the Vault server with psql/mysql first
Missing plugin_nameRole creation fails with "unable to initialize connection"Specify the right plugin_name (postgresql-database-plugin / mysql-database-plugin)

Conclusion

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!

Learn Vault - Dynamic Database Secrets Engine (Short-Lived Credentials) | Learn Secret Management with HashiCorp Vault