Vault dynamic database credentials in Python

Vault’s database secrets engine issues a brand-new database user on every request, valid only for its lease, then deletes it. There is no static password to leak. This page consumes those credentials from Python, extending the HashiCorp Vault Python SDK cluster.

Problem 1: a static DB password in config

# ANTI-PATTERN: one long-lived password, unbounded blast radius
DATABASE_URL = "postgresql://app:staticPassword@db/app"   # leaks forever

A static credential is valid until someone notices and rotates it.

Problem 2: ignoring the lease

# ANTI-PATTERN: uses dynamic creds but never renews the lease
creds = client.secrets.database.generate_credentials(name="app")
# ... the lease expires and the database user is deleted mid-connection

Dynamic credentials are temporary; the connection must be rebuilt before the lease ends.

Secure implementation

# db/vault_creds.py
import time
import hvac
from pydantic import SecretStr
from sqlalchemy import create_engine

class VaultDB:
    def __init__(self, client: hvac.Client, role: str, host: str, db: str):
        self._c, self._role, self._host, self._db = client, role, host, db
        self._engine = None
        self._renew_at = 0.0

    def engine(self):
        if self._engine is None or time.monotonic() > self._renew_at:
            lease = self._c.secrets.database.generate_credentials(name=self._role)
            d, ttl = lease["data"], lease["lease_duration"]
            pw = SecretStr(d["password"]).get_secret_value()
            self._engine = create_engine(
                f"postgresql://{d['username']}:{pw}@{self._host}/{self._db}",
                pool_pre_ping=True,
            )
            self._renew_at = time.monotonic() + ttl * 0.7   # rebuild before expiry
        return self._engine

A fresh credential is generated and the engine rebuilt at 70% of the lease TTL, so connections always use a live, short-lived user. The password is handled as SecretStr.

Gotchas & version-specific behaviour

  • Read lease_duration from the response; do not hard-code the TTL.
  • Rebuild the engine before the lease ends (e.g. at 70% TTL) — an expired user disappears from the database.
  • pool_pre_ping=True drops connections whose user was revoked.
  • The Vault DB role defines the SQL grants — scope it to least privilege.

Production parity checklist

  • The app uses dynamic credentials, not a static DB password.
  • The engine is rebuilt before the lease expires.
  • The connection pool drops revoked connections.
  • The Vault database role is least-privilege.
  • Passwords are SecretStr-wrapped and never logged.

Conclusion

Vault’s database engine plus a lease-aware engine factory means every database connection uses a short-lived user that cannot outlive its lease. Authenticate the client first with Vault AppRole Auth in Python.