The Vault Database secrets engine generates database credentials on demand, with a limited lifetime. Each service gets its own user and password, and each one can be revoked individually.
This guide covers PostgreSQL, MySQL and MongoDB, but the principle applies to every database plugin (the component that teaches Vault how to talk to one type of database) that Vault supports. It teaches the gestures specific to this engine: connection, role, root rotation, static role. The general lease mechanism is explained elsewhere: the concept is compared with static secrets in static vs dynamic secrets, and leases themselves are detailed in the lesson linked in the prerequisites.
What you will learn
By the end of this guide you will be able to:
- Configure a database connection in Vault with a dedicated management account
- Create a dynamic role whose TTL and SQL statements limit the rights granted
- Issue and verify unique credentials for a service
- Revoke one credential or a whole role, and choose between a dynamic role and a static role
- SOCLE anchoring: this guide implements SOCLE-SEC-ROT-1 (in French), a TTL defined per role, SOCLE-SEC-ROT-3 (in French), the emergency revocation of a database account, and SOCLE-SEC-ROT-5 (in French), the automatic rotation of the static role and of the management account (
rotate-root)
Why use dynamic credentials for databases?
A shared service account causes two problems that manual rotation does not solve. First the blast radius: a leaked password gives access to the database for every service that uses it, and changing it means redeploying each of them. Then accountability: the database logs show a single user name, so you cannot attribute a query to a given service. The table below compares the two models on these points.
| Classic model | With Vault Database |
|---|---|
One shared app_user account | One account per workload, v-token-app-xyz |
| Password in code or environment | Password generated on demand |
| Manual yearly rotation | Dynamic role: short-lived issuance + revocation |
| Compromise = everything exposed | Compromise = a single workload |
| Audit: "app_user did X" | Audit: "v-token-svc-abc did X" |
Prerequisites
Before you start, you need three notions, defined here in one line each. A lease is the record Vault keeps for every credential it issues, with its expiry date. Its lease ID is the identifier that you pass to renew or revoke it. The TTL (time to live) is the lifetime granted, and the max TTL is the ceiling that renewals cannot exceed. Revocation means ending a lease before its expiry, and for a database it deletes the user. The lesson on leases, dynamic roles and static roles goes further.
You also need to be able to write a policy, a set of rules that says which paths a client may read or write, to control who can request these credentials.
- Vault installed and started
- Admin access to configure the engine
- A PostgreSQL, MySQL or MongoDB database reachable from Vault
- A dedicated management account for Vault, to create and delete users
How is the Database engine built?
The engine rests on three distinct objects that you must be able to name
before typing any command, because they appear as-is in the API paths. The
connection describes how to reach the database, the role describes
which user to create and with which rights, and the lease is the trace of
each credential issued. The diagram shows how they chain on a request: the
command asks for the role, which goes through the connection to create the
user in PostgreSQL, then Vault returns the credentials and the lease_id
that will be used for revocation.
Components:
- Connection: how to access the database (URL, admin credentials)
- Role: SQL template + TTL for the generated credentials
- Lease: reference to the issued credential, for renewal and revocation
Step 1: enable the engine and configure the connection
This first step is done with an administrator token and only concerns the Vault operator. It mounts the engine on a path, then gives it what it needs to open a session on the database. Until it is finished, no application can request credentials.
Enable the engine
Without an argument, the engine is mounted on the database/ path: this is
the prefix you will find in all the commands that follow. Add -path= if you
need to manage several fleets of databases separately.
vault secrets enable databaseConfigure the PostgreSQL connection
The allowed_roles parameter is the most important access control in this
block: it lists the roles allowed to use this connection, and a role missing
from the list is refused even if it exists. The {{username}} and
{{password}} markers in connection_url (the URL Vault uses to reach the
database) are filled in by Vault with the management account's credentials,
which avoids writing them twice.
vault write database/config/postgres \ plugin_name="postgresql-database-plugin" \ allowed_roles="readonly,readwrite" \ connection_url="postgresql://{{username}}:{{password}}@postgres.example.com:5432/myapp?sslmode=require" \ username="vault_admin" \ password="VaultAdminPassword123"vault write database/config/mysql \ plugin_name="mysql-database-plugin" \ allowed_roles="readonly,readwrite" \ connection_url="{{username}}:{{password}}@tcp(mysql.example.com:3306)/myapp" \ username="vault_admin" \ password="VaultAdminPassword123"vault write database/config/mongodb \ plugin_name="mongodb-database-plugin" \ allowed_roles="readonly,readwrite" \ connection_url="mongodb://{{username}}:{{password}}@mongo.example.com:27017/admin?tls=true" \ username="vault_admin" \ password="VaultAdminPassword123"Rotate the admin password
After configuration, renew the admin password so that only Vault knows it.
The command is called rotate-root: this is HashiCorp's name for rotating
the management account's password, and it is a command name, so it stays as
is.
vault write -force database/rotate-root/postgresStep 2: create a dynamic role
A role is the contract between Vault and the database: it describes the user to create, what it is allowed to do and how long it lives. It is the only place where you apply least privilege, since an issued credential inherits exactly the rights written here. Plan one role per access level rather than a single, very permissive role.
A role defines:
- The SQL template used to create the user
- The permissions granted
- The TTL (lifetime), with
default_ttlfor the lease granted by default andmax_ttlfor the ceiling it can be renewed up to
vault write database/roles/readonly \ db_name="postgres" \ creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \ revocation_statements="REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\"; \ DROP ROLE IF EXISTS \"{{name}}\";" \ default_ttl="1h" \ max_ttl="24h"vault write database/roles/readwrite \ 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}}\";" \ revocation_statements="REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\"; \ DROP ROLE IF EXISTS \"{{name}}\";" \ default_ttl="1h" \ max_ttl="8h"vault write database/roles/readonly \ db_name="mysql" \ creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \ GRANT SELECT ON myapp.* TO '{{name}}'@'%';" \ revocation_statements="DROP USER IF EXISTS '{{name}}'@'%';" \ default_ttl="1h" \ max_ttl="24h"The Database engine also works for MongoDB, with the same principle but a
different syntax: the creation_statements there are not SQL, because
MongoDB uses JSON documents to define roles and privileges. Refer to the
examples specific to the MongoDB plugin.
Available variables:
| Variable | Description |
|---|---|
{{name}} | Username generated by Vault |
{{password}} | Password generated by Vault |
{{expiration}} | Expiration date (PostgreSQL VALID UNTIL) |
Step 3: issue credentials
This is the only command that the application really runs. Each call creates a new user in the database and returns an identifier and password pair that will exist nowhere else: Vault does not keep them, it only keeps the lease that will allow revoking them. Running the command again therefore does not return the same values, it generates new ones.
vault read database/creds/readonlyOutput:
Key Value--- -----lease_id database/creds/readonly/abcd1234-5678-90eflease_duration 1hlease_renewable truepassword A1B2-c3D4-e5F6-g7H8username v-token-readonly-xyz123abcWhat happens:
- Vault connects to PostgreSQL with the admin account
- Vault runs the
creation_statementswith a generated username and password - Vault returns the credentials and creates a lease
- The application uses these credentials
- At expiry (or revocation), Vault runs the
revocation_statements
How do you check on the PostgreSQL side?
The proof that the mechanism works is read in the database itself: the users
created by Vault have a name prefixed with v- and an expiration date in the
valuntil column.
-- List the Vault usersSELECT usename, valuntil FROM pg_user WHERE usename LIKE 'v-%';
-- Result usename | valuntil----------------------------+------------------------ v-token-readonly-xyz123abc | 2026-03-16 15:00:00+00What is a static role?
A static role lets Vault manage an existing account and renew its password automatically. It is meant for legacy applications that do not support dynamic credentials.
Create a static role
The account targeted by username must already exist in the database:
Vault does not create it, it only changes its password every
rotation_period. The first rotation happens as soon as the role is created.
vault write database/static-roles/legacy-app \ db_name="postgres" \ username="legacy_app_user" \ rotation_statements="ALTER ROLE \"{{name}}\" WITH PASSWORD '{{password}}';" \ rotation_period="24h"Read the credentials
Reading is done on static-creds and not on creds: it generates nothing, it
returns the account's current password. The ttl field shows the time left
before the next rotation, not the lifetime of a credential.
vault read database/static-creds/legacy-appOutput:
Key Value--- -----last_vault_rotation 2026-03-16T10:00:00Zpassword NewRotatedPass123rotation_period 24httl 23h45musername legacy_app_userDifference with a dynamic role:
- The username is always the same (
legacy_app_user) - The password changes automatically every
rotation_period - No lease to renew, but the password changes
The static role remains a compromise for apps that do not support dynamic credentials. You keep automatic rotation, but lose per-workload traceability. Prefer dynamic roles for new applications.
Dynamic role or static role: which one to choose?
The decision criterion is not security but the application constraint. If the application accepts receiving a different identifier at each start, the dynamic role is always preferable. The static role is only justified when a fixed user name is imposed, by a third-party tool or by rights granted manually on that precise account.
| Use case | Choice | Reason |
|---|---|---|
| New application | Dynamic role | Per-workload traceability |
| Legacy app with an imposed fixed user | Static role | No alternative |
| Need for precise audit per service | Dynamic role | Unique username = clear audit |
| Tool that cannot re-read credentials | Static role | Stable password between rotations |
| Short-lived connections (jobs, CI) | Dynamic role | Very short TTL possible |
| Long-lived connection (pool) | Dynamic role + renewal | Or static role if legacy |
How do you revoke or renew database credentials?
A dynamic credential disappears either because its lease reaches its end,
or because you revoke it explicitly. Both paths trigger the same
revocation_statements, hence the actual deletion of the user in the
database. Knowing how to drive them by hand is essential on incident day, when
you must cut a service's access without waiting for natural expiry.
Renew a lease
Before expiry, the application can extend it:
vault lease renew database/creds/readonly/abcd1234-5678-90efRenewal is limited by the role's max_ttl: beyond it, Vault returns no
error, it simply grants the ceiling. Always read the duration actually
granted in the lease_duration of the response (see
dynamic secrets).
Revoke a lease
Revocation is immediate and final: connections already open with that
account will drop as soon as the database requires it again. The -prefix
option cuts all the credentials issued by a role at once, and it is the
gesture to know when you suspect a leak.
# Revoke one specific credentialvault lease revoke database/creds/readonly/abcd1234-5678-90ef
# Revoke all the credentials of a rolevault lease revoke -prefix database/creds/readonlyReal action: Vault runs the revocation_statements and deletes the user
from the database. Check it by running the pg_user query from step 3 again: the
revoked user must no longer appear. This recorded observation is what
makes a revocation a successful emergency test.
Force a rotation (static role)
This command changes the account's password without waiting for the end of the
rotation_period and resets the counter. The application will have to read
static-creds again to get valid access.
vault write -force database/rotate-role/legacy-appWhich policy gives access to database credentials?
A Vault policy allows operations on paths. This one gives the strict
minimum to a read-only application: request credentials on its own role, and
extend its leases. Note that it mentions neither database/config/ nor
database/roles/: the application has no reason to read the engine's
configuration or to create roles.
# Get readonly credentialspath "database/creds/readonly" { capabilities = ["read"]}
# Renew its own leasespath "sys/leases/renew" { capabilities = ["update"]}vault policy write app-readonly policy-app-readonly.hclIn the name of the principle of least privilege, each application must reach only the role it needs, with one policy per profile:
- Read service →
database/creds/readonly - Write service →
database/creds/readwrite - Admin →
database/creds/admin(with a very short TTL)
How does an application use these credentials?
Moving to dynamic credentials changes a development habit: the connection configuration is no longer read once at startup, it has an expiry date. Two approaches exist: either the application queries Vault itself, or a local agent writes a configuration file in its place. The first requires code, the second applies to software you cannot modify.
Python example with psycopg2
The hvac library is the official Python client for Vault. The important
point of this example is the order of operations: you first authenticate to
Vault with a durable identity, here an AppRole (an authentication method
for applications, based on a role_id and a secret_id), and it is this
identity that grants the right to the short-lived database credentials.
import hvacimport psycopg2from contextlib import contextmanager
def get_db_credentials(): """Get dynamic credentials from Vault.""" client = hvac.Client(url='https://vault.example.com') # Auth via AppRole, Kubernetes, etc. client.auth.approle.login( role_id='app-role-id', secret_id='app-secret-id' )
# Read the dynamic credentials creds = client.secrets.database.generate_credentials( name='readonly', mount_point='database' )
return { 'user': creds['data']['username'], 'password': creds['data']['password'], 'lease_id': creds['lease_id'], 'lease_duration': creds['lease_duration'] }
@contextmanagerdef get_db_connection(): """Context manager with dynamic credentials.""" creds = get_db_credentials()
conn = psycopg2.connect( host='postgres.example.com', database='myapp', user=creds['user'], password=creds['password'] )
try: yield conn finally: conn.close() # Optional: revoke immediately if the TTL is short # client.sys.revoke_lease(creds['lease_id'])
# Usagewith get_db_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM users LIMIT 10") rows = cursor.fetchall()Vault Agent with a template
For applications that cannot call Vault directly, Vault Agent is a local process that authenticates to Vault, fetches the secrets and writes them into files from a template:
template { contents = <<EOF[database]host = postgres.example.comport = 5432database = myapp{{ with secret "database/creds/readonly" }}username = {{ .Data.username }}password = {{ .Data.password }}{{ end }}EOF destination = "/etc/myapp/db.conf" perms = "0600" command = "systemctl reload myapp"}What if it does not work?
The Database engine's error messages come from two different worlds, which makes diagnosis confusing: some are emitted by Vault (policy, role, TTL), others bubble up unchanged from the database (connection, authentication, SQL syntax). Identifying the emitter points straight to the right configuration to fix.
| Symptom | Likely cause | Solution |
|---|---|---|
permission denied | Missing policy | Check database/creds/<role> |
connection refused | Vault cannot reach the DB | Check network and firewall |
authentication failed | Invalid admin credentials | Check the connection config |
role not found | Role missing or misnamed | vault list database/roles |
| Lease shorter than expected, no error | The role's max_ttl: Vault caps the requested duration without any message | Read lease_duration, then adjust max_ttl or the renewal cycle |
| User not deleted after revocation | Wrong revocation_statements | Check the SQL syntax |
How do you debug the connection?
These three commands run in order and isolate the failure. The first never shows the management password, only the URL and the allowed roles; the third lists the leases still active, useful for spotting credentials that should have been revoked.
# Check the configurationvault read database/config/postgres
# Test an issuancevault read database/creds/readonly
# Check the active leasesvault list sys/leases/lookup/database/creds/readonlyWhat does the Database engine not do?
Vault manages the accounts' lifecycle, nothing else. The split of responsibilities below avoids the classic production disappointment: an application that crashes at expiry because nobody wrote the renewal code. Everything related to the connection pool and to monitoring expirations remains your responsibility.
| Responsibility | Vault | You |
|---|---|---|
| Create dynamic users | Yes | - |
| Revoke at expiry | Yes | - |
| Store the credentials | No | Yes (for the span of a lease) |
| Manage the connection pool | No | Yes |
| Migrate existing apps | No | Yes |
| Monitor expirations | No* | Yes |
| Back up the DB | No | Yes |
*Vault exposes metrics, but monitoring is up to you.
Key points
- Dynamic role: a new credential per request, perfect traceability
- Static role: periodic rotation of an existing account (legacy only)
- Connection: a management account dedicated to Vault (not the DB superuser)
rotate-root: after configuration, only Vault knows the management password- Lease: identifier to renew or revoke a dynamic credential
- Revocation: a real action, the user is deleted from the DB
- Plugins: the
creation_statementsdepend on the engine (PostgreSQL ≠ MongoDB) - Integration: the app must handle renewal and connection reloading
- Proof expected by SOCLE: the
default_ttlandmax_ttlof the roles prove SOCLE-SEC-ROT-1 (in French); avault lease revokethat deletes the user in the database, observed and recorded, proves SOCLE-SEC-ROT-3 (in French); therotation_periodof a static role and therotate-rootof the management account prove SOCLE-SEC-ROT-5 (in French)
Knowledge check
Check that the essentials of this lesson are in place. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Vault AWS: dynamic IAM credentials
A new full quiz with random questions
Work again only on the questions you missed