The Warehouse Password in Your Config File
Almost every data platform has the same weak spot. A warehouse password, a database connection string, and an API token for the object store all sit in an environment file, a Kubernetes Secret, or — worse — a checked-in config. They are long-lived, shared across the team, and nobody knows who has copied them.
HashiCorp Vault attacks that problem from a different angle. Instead of storing a static password more securely, it stops the static password from existing. A pipeline asks Vault for database access at run time, Vault creates a brand-new user scoped to that request, and the credential self-destructs when its lease expires.
This guide walks the parts a data team actually reaches for: the database secrets engine for dynamic credentials, static roles for accounts you cannot replace, the PKI engine for certificates, how leases make revocation automatic, and how a pipeline authenticates without a bootstrap secret of its own.
Note
Static Secrets vs Dynamic Secrets
Vault does two very different jobs, and conflating them is the most common early mistake. A static secret is a value you hand Vault to store and encrypt — the key-value engine is the canonical example. Vault protects it, audits access, and versions it, but the value itself does not change unless you change it.
A dynamic secret does not exist until something asks for it. Vault generates it on demand, hands it out attached to a lease, and revokes it when the lease ends. The docs are explicit that all dynamic secrets are required to have a lease, which forces every consumer to check in regularly rather than hold a credential forever.
For data teams the dynamic path is the prize. If your dbt run, your Airflow task, and your ad-hoc query each get a distinct database user that lives for an hour, a leaked credential is worthless within the hour and every action is attributable. That is a categorically stronger posture than one shared password rotated quarterly.
The Database Secrets Engine
The database secrets engine generates database credentials dynamically based on configured roles. You enable it, point it at a database with a privileged account Vault will use to create and drop users, and define roles that describe what a generated user is allowed to do.
# Enable the engine (mounts at "database/" by default)
vault secrets enable database
# Configure a PostgreSQL connection
vault write database/config/analytics-db \
plugin_name="postgresql-database-plugin" \
allowed_roles="reporting,dbt-runner" \
connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/analytics" \
username="vaultadmin" \
password="change-me-in-real-life"The account in username and password is what the docs call the root user for this connection — the identity Vault uses to manage others. HashiCorp recommends a dedicated Vault-only account here, and allowed_roles is the guardrail that lists which roles may use this connection.
Note
vault write -force database/rotate-root/analytics-db. The docs warn the original password is no longer accessible afterward — which is the point.Roles: Credentials Minted On Demand
A role is a template. It names the connection, holds the SQL that creates a user, and sets how long the resulting credential lives. The creation_statements use Vault’s templating tokens — {{name}}, {{password}}, and {{expiration}} — which Vault fills in for each request.
vault write database/roles/reporting \
db_name=analytics-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"Now anyone with the right policy asks for credentials and gets a fresh, read-only Postgres user valid for one hour. The VALID UNTIL '{{expiration}}' clause means the database itself enforces the deadline even if Vault were somehow unreachable.
vault read database/creds/reporting
# Key Value
# --- -----
# lease_id database/creds/reporting/xxxx
# lease_duration 1h
# lease_renewable true
# password A1a-generated-by-vault
# username v-token-reporting-9Qk2...The username is generated per request, so two pipelines never share an identity. This dovetails with the migration techniques in zero-downtime database migrations — a schema-change job can request a short-lived elevated role that vanishes the moment the migration finishes.
Static Roles: Rotating Accounts You Cannot Replace
Dynamic credentials are ideal when a throwaway user is acceptable, but some accounts are fixed. A BI tool configured with one service account, or a legacy job that cannot re-authenticate mid-run, needs a stable username whose password still rotates. That is what a static role does — the docs describe it as a one-to-one mapping of a Vault role to an existing database username.
# Rotate on a schedule (cron) — here, hourly on Saturdays
vault write database/static-roles/bi-service \
db_name=analytics-db \
username="bi_service" \
rotation_schedule="0 * * * SAT"
# Or a simple interval instead of a schedule
vault write database/static-roles/etl-service \
db_name=analytics-db \
username="etl_service" \
rotation_period="24h"The docs are firm that rotation_period and rotation_schedule are mutually exclusive — set exactly one. Consumers then read the current password rather than minting a new user.
vault read database/static-creds/bi-service
# Returns username, password, last_vault_rotation,
# ttl, and the rotation_period or rotation_schedule/rotation_windowNote
Leases: Why Revocation Is Automatic
Every dynamic secret comes with a lease — metadata that includes a TTL. Vault guarantees the secret is valid for that duration, and when the lease expires it revokes the underlying credential automatically. For the database engine that means Vault runs the revocation SQL and the generated user disappears.
A long-running job renews its lease to keep working; short jobs simply let it expire. And when something goes wrong, revocation is a first-class operation rather than a manual password reset across every system.
# Extend a lease to one hour from now
vault lease renew -increment=3600 database/creds/reporting/xxxx
# Kill a single credential immediately
vault lease revoke database/creds/reporting/xxxx
# Kill every credential from a mount at once — the "break glass" move
vault lease revoke -prefix database/creds/That last command is the incident-response superpower. If a host is compromised, one prefix revoke invalidates every database credential Vault ever issued from that path — no hunting through connection strings, no coordinating a fleet-wide password change.
The PKI Engine: Certificates on Demand
Data infrastructure runs on TLS — between brokers and clients, between services in a mesh, between a pipeline and the warehouse. The PKI secrets engine generates dynamic X.509 certificates so a service can obtain one without the manual key-CSR-sign-wait dance. Enable the engine, tune its max lease TTL, and generate (or import) a CA.
vault secrets enable pki
# Allow certificates valid up to one year
vault secrets tune -max-lease-ttl=8760h pki
# Generate a self-signed root CA
vault write pki/root/generate/internal \
common_name=my-website.com \
ttl=8760h
# Publish where the CA cert and CRL can be fetched
vault write pki/config/urls \
issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" \
crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl"A role constrains what certificates may be issued — which domains, whether subdomains are allowed, and the maximum lifetime. Issuing a certificate is then a single write.
vault write pki/roles/example-dot-com \
allowed_domains=my-website.com \
allow_subdomains=true \
max_ttl=72h
# Issue a leaf certificate — returns the cert, private key, and CA chain
vault write pki/issue/example-dot-com \
common_name=www.my-website.comBecause certificates are short-lived and issued from a role, you get the same lifecycle discipline as database credentials: no long-lived private keys sitting on disk, and rotation that is just re-issuing. In production most teams run an intermediate CA signed by an offline root rather than issuing directly from the root shown here.
The Chicken-and-Egg Problem: Auth Methods
All of this raises an obvious question: if Vault hands out the database password, what secret does the pipeline use to talk to Vault? Solving that with a static Vault token just moves the problem. The answer is an auth method that trusts the platform the workload already runs on.
For data pipelines on Kubernetes, the Kubernetes auth method lets a pod prove its identity with its own service-account token. Vault validates that token with the Kubernetes API, then issues a short-lived Vault token scoped by policy. No secret is provisioned into the pod at all.
vault auth enable kubernetes
vault write auth/kubernetes/config \
token_reviewer_jwt="<reviewer service account JWT>" \
kubernetes_host=https://192.168.99.100:8443 \
kubernetes_ca_cert=@ca.crt
# Bind a service account to a policy
vault write auth/kubernetes/role/dbt-runner \
bound_service_account_names=dbt \
bound_service_account_namespaces=analytics \
policies=reporting-read \
ttl=1hThe full chain now has no static secret anywhere: the pod authenticates with its Kubernetes identity, receives a one-hour Vault token, uses it to read database/creds/reporting, and gets a one-hour Postgres user. Every link expires on its own. This is the identity backbone behind the self-service platforms we describe in provisioning infrastructure with Pulumi, whose hashivault secrets provider can encrypt stack state with a Vault-managed key.
Policies Tie It Together
Everything above is gated by policy. A policy grants capabilities on specific paths, and Vault denies by default — an identity can touch only what it is explicitly allowed to. The policy for the reporting pipeline is tiny and readable.
# reporting-read.hcl
path "database/creds/reporting" {
capabilities = ["read"]
}Write it with vault policy write reporting-read reporting-read.hcl and attach it via the auth role, as above. Keeping policies narrow and named per workload is what makes an audit answerable: who could read this credential, and when did they. If you already run secrets scanning in your CI/CD pipeline, Vault removes the thing those scanners keep finding — plaintext credentials in code and config.
A Production Checklist
- Rotate the root connection. Run
rotate-rootright after configuring each database so the bootstrap password no longer exists anywhere. - Prefer dynamic over static. Reach for dynamic roles first; use static roles only for accounts that genuinely cannot be recreated per request.
- Set realistic TTLs. Short
default_ttlwith a boundedmax_ttllimits blast radius; renew for long jobs rather than issuing long leases. - Never rotate root via a static role. Vault cannot tell root from a managed user and will lock itself out.
- Authenticate by platform identity. Use Kubernetes, cloud IAM, or another workload-identity auth method so no bootstrap token is provisioned into pipelines.
- Scope policies tightly. Grant read on exactly the credential paths a workload needs, and enable audit devices so every access is logged.
The shift Vault asks for is conceptual before it is operational: stop thinking of a credential as a thing you store and protect, and start thinking of it as a thing you generate, use briefly, and discard. For data teams surrounded by warehouse passwords, connection strings, and API tokens, that shift turns the scariest part of the platform — the shared long-lived secret — into a short-lived, attributable, automatically revoked lease.
Your warehouse password lives in an environment file that half the team has copied, every pipeline shares one database account nobody can safely rotate, or a leaked connection string means a fleet-wide password change and no way to tell who touched what?
We design and implement HashiCorp Vault platforms for data teams — from a database secrets engine that mints a fresh, tightly-scoped database user per request with a short lease so a leaked credential is worthless within the hour and every action is attributable, through static roles that rotate the fixed accounts a BI tool or legacy job genuinely cannot replace on a schedule you choose and never pointed at the root credentials Vault needs to manage everything else, a PKI engine issuing short-lived X.509 certificates from constrained roles so no long-lived private keys sit on disk, leases wired so an incident is one prefix revoke rather than a manual reset across every system, and workload-identity auth on Kubernetes or cloud IAM so pipelines authenticate without any bootstrap secret provisioned into them at all — with deny-by-default policies scoped to exactly the credential paths each workload needs, audit devices logging every access, and the whole chain expiring on its own so the scariest part of your platform stops being a shared long-lived password. Let's talk.
Let's Talk