Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Vault Database: dynamic credentials for PostgreSQL, MySQL, MongoDB

By updated

Improve this page

HashiCorp Vault logo

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 modelWith Vault Database
One shared app_user accountOne account per workload, v-token-app-xyz
Password in code or environmentPassword generated on demand
Manual yearly rotationDynamic role: short-lived issuance + revocation
Compromise = everything exposedCompromise = 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.

Architecture of the Vault Database secrets engine: the command vault read database/creds/readonly asks for the readonly role (TTL, SQL), which goes through the postgres connection to run CREATE USER and GRANT in PostgreSQL, then returns username, password and lease_id.Vault: Database secrets engineCREATE USERv-token-readonly-xyzGRANT SELECT ON ALL TABLES...vault readdatabase/creds/readonlyRole readonly(TTL, SQL)Connectionpostgres (plugin)PostgreSQLCreates user v-token-xyzreturns username + password + lease_id
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

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.

Terminal window
vault secrets enable database

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

Terminal window
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"

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.

Terminal window
vault write -force database/rotate-root/postgres

Step 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_ttl for the lease granted by default and max_ttl for the ceiling it can be renewed up to
Terminal window
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"

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:

VariableDescription
{{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.

Terminal window
vault read database/creds/readonly

Output:

Key Value
--- -----
lease_id database/creds/readonly/abcd1234-5678-90ef
lease_duration 1h
lease_renewable true
password A1B2-c3D4-e5F6-g7H8
username v-token-readonly-xyz123abc

What happens:

  1. Vault connects to PostgreSQL with the admin account
  2. Vault runs the creation_statements with a generated username and password
  3. Vault returns the credentials and creates a lease
  4. The application uses these credentials
  5. 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 users
SELECT usename, valuntil FROM pg_user WHERE usename LIKE 'v-%';
-- Result
usename | valuntil
----------------------------+------------------------
v-token-readonly-xyz123abc | 2026-03-16 15:00:00+00

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

Terminal window
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.

Terminal window
vault read database/static-creds/legacy-app

Output:

Key Value
--- -----
last_vault_rotation 2026-03-16T10:00:00Z
password NewRotatedPass123
rotation_period 24h
ttl 23h45m
username legacy_app_user

Difference 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 caseChoiceReason
New applicationDynamic rolePer-workload traceability
Legacy app with an imposed fixed userStatic roleNo alternative
Need for precise audit per serviceDynamic roleUnique username = clear audit
Tool that cannot re-read credentialsStatic roleStable password between rotations
Short-lived connections (jobs, CI)Dynamic roleVery short TTL possible
Long-lived connection (pool)Dynamic role + renewalOr 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:

Terminal window
vault lease renew database/creds/readonly/abcd1234-5678-90ef

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

Terminal window
# Revoke one specific credential
vault lease revoke database/creds/readonly/abcd1234-5678-90ef
# Revoke all the credentials of a role
vault lease revoke -prefix database/creds/readonly

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

Terminal window
vault write -force database/rotate-role/legacy-app

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

policy-app-readonly.hcl
# Get readonly credentials
path "database/creds/readonly" {
capabilities = ["read"]
}
# Renew its own leases
path "sys/leases/renew" {
capabilities = ["update"]
}
Terminal window
vault policy write app-readonly policy-app-readonly.hcl

In 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 hvac
import psycopg2
from 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']
}
@contextmanager
def 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'])
# Usage
with 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:

agent-config.hcl
template {
contents = <<EOF
[database]
host = postgres.example.com
port = 5432
database = 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.

SymptomLikely causeSolution
permission deniedMissing policyCheck database/creds/<role>
connection refusedVault cannot reach the DBCheck network and firewall
authentication failedInvalid admin credentialsCheck the connection config
role not foundRole missing or misnamedvault list database/roles
Lease shorter than expected, no errorThe role's max_ttl: Vault caps the requested duration without any messageRead lease_duration, then adjust max_ttl or the renewal cycle
User not deleted after revocationWrong revocation_statementsCheck 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.

Terminal window
# Check the configuration
vault read database/config/postgres
# Test an issuance
vault read database/creds/readonly
# Check the active leases
vault list sys/leases/lookup/database/creds/readonly

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

ResponsibilityVaultYou
Create dynamic usersYes-
Revoke at expiryYes-
Store the credentialsNoYes (for the span of a lease)
Manage the connection poolNoYes
Migrate existing appsNoYes
Monitor expirationsNo*Yes
Back up the DBNoYes

*Vault exposes metrics, but monitoring is up to you.

Key points

  1. Dynamic role: a new credential per request, perfect traceability
  2. Static role: periodic rotation of an existing account (legacy only)
  3. Connection: a management account dedicated to Vault (not the DB superuser)
  4. rotate-root: after configuration, only Vault knows the management password
  5. Lease: identifier to renew or revoke a dynamic credential
  6. Revocation: a real action, the user is deleted from the DB
  7. Plugins: the creation_statements depend on the engine (PostgreSQL ≠ MongoDB)
  8. Integration: the app must handle renewal and connection reloading
  9. Proof expected by SOCLE: the default_ttl and max_ttl of the roles prove SOCLE-SEC-ROT-1 (in French); a vault lease revoke that deletes the user in the database, observed and recorded, proves SOCLE-SEC-ROT-3 (in French); the rotation_period of a static role and the rotate-root of 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

4 questions
4 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn