The first time I audited a codebase for secrets, I found a database password in a comment explaining why it had been rotated, three commits after it had actually been rotated — the old one, still valid, still readable by anyone with clone access. Static secrets fail this way constantly: they end up in `.env` files, CI logs, Slack messages, and shell history, and none of those places get cleaned up when the secret is supposed to change. Vault's actual pitch has nothing to do with encryption at rest, which most secret stores already do fine. It's that it can stop handing out secrets that live forever and start handing out ones that expire on their own.
Static secrets vs dynamic secrets
A static secret is a password, API key, or certificate that exists whether or not anyone is using it right now, and stays valid until a human or a script explicitly rotates it. A dynamic secret is generated on demand, scoped to one consumer, and expires automatically after a lease period whether anyone remembers to revoke it or not. The difference sounds academic until you think through the incident case: with a static database password shared across twelve services, revoking it after a leak means coordinating twelve deploys at the same time. With dynamic credentials, you revoke the lease — or just let it expire — and the blast radius is whatever that one lease could touch, for however long it was valid, typically minutes to hours rather than the lifetime of the credential.
This is also why "secrets management" and "a place to put secrets" are different problems. A vault that just stores your existing static secrets more securely is strictly better than a `.env` file, but it doesn't change the failure mode — someone still has to remember to rotate the database password, and the credential is still good for as long as it takes someone to notice it leaked. Vault's secrets engines exist because generating the secret at request time, instead of storing one that was generated once, removes the human from that loop entirely.
The database secrets engine, in practice
Vault's database secrets engine connects to your database with a privileged admin credential — one that only Vault ever sees — and creates a new, narrowly-scoped user on demand every time an application asks for one. You configure a connection and a role once, and after that every request for a credential produces a brand new username and password pair with a TTL attached.
# enable the database secrets engine
vault secrets enable database
# tell Vault how to reach Postgres and manage the connection
vault write database/config/orders-db \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/orders" \
allowed_roles="orders-readonly,orders-readwrite" \
username="vault-admin" \
password="$(cat /run/secrets/vault_db_admin_pw)"
# define a role: what SQL runs to create/revoke a leased user
vault write database/roles/orders-readwrite \
db_name=orders-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' \
VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES \
IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# an application requests a credential like this
vault read database/creds/orders-readwrite
# Key Value
# --- -----
# lease_id database/creds/orders-readwrite/2xR9...
# lease_duration 1h
# username v-orders-readwrite-a1b2c3
# password A1b2C3d4E5f6...
Every application instance gets its own database user. When you're investigating unusual query activity, you're looking at a username tied to one specific lease and one specific pod, not a shared credential every service uses, which turns "which of our fifteen services did this" from a genuine question into a lookup.
Rotation and lease renewal
Every dynamic secret in Vault comes back with a lease: a duration after which the secret is no longer guaranteed valid. The consuming application is expected to renew the lease before it expires if it still needs the credential, and Vault will refuse to renew past `max_ttl` no matter how many times you ask — that ceiling is what actually forces rotation, rather than relying on a renewal loop that could in theory run forever. If nothing renews the lease, Vault revokes it, which for the database engine means it drops the Postgres role it created. No revocation script to maintain, no cron job that "handles cleanup," because the expiry is enforced by the same system that issued the credential.
Vault doesn't push renewed credentials to your app — your app (or Vault Agent, running as a sidecar) has to ask for renewal before the lease runs out, and handle the case where renewal is refused because max_ttl was hit. Design for "credential disappears and I need a new one," not "credential lives forever unless something goes wrong."
Vault Agent exists specifically to take this off the application's plate. It runs alongside your service, authenticates to Vault on its own, fetches and renews leases in the background, and writes the current credential to a file or template your app reads — so the app code doesn't need a Vault SDK at all, just the discipline to re-read a file periodically or on a signal. For services you don't control the code of, or don't want to instrument, this is usually the difference between "we could use Vault here" and "we actually will."
How services authenticate to Vault
Dynamic secrets only remove one hardcoded credential if getting them doesn't require a different hardcoded credential to authenticate to Vault in the first place. This is what the auth methods are for. Two show up constantly in production: AppRole, for services and CI systems that aren't running inside Kubernetes, and the Kubernetes auth method, for anything that is.
The Kubernetes auth method lets a pod authenticate using its own service account token — the one the kubelet already mounts into every pod — with no separate secret to distribute at all. Vault verifies that token against the Kubernetes API, checks it against a configured role, and issues a Vault token scoped to that role's policies.
# enable and point Vault at the cluster's API
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# bind a Kubernetes service account to a Vault policy
vault write auth/kubernetes/role/orders-api \
bound_service_account_names="orders-api" \
bound_service_account_namespaces="production" \
policies="orders-db-readwrite" \
ttl="1h"
# the policy the role above grants
vault policy write orders-db-readwrite - <
AppRole covers the case where there's no Kubernetes API to check against — a CI runner, a VM, an on-prem batch job. It splits authentication into a `role_id` (identifies which role you're claiming, not secret) and a `secret_id` (proves you're allowed to claim it, and can itself be short-lived or single-use). The `secret_id` still has to land somewhere at startup, which is the one part of this that never fully disappears — it just moves the problem from "the database password is static" to "one bootstrap credential, scoped to nothing but 'get a Vault token,' is static," which is a much smaller thing to get wrong.
Running Vault yourself vs a managed alternative
Vault itself is not the expensive part. The expensive part is everything around it: unsealing after a restart, storage backend choice and its own backup story, upgrade testing because Vault's own auth and secrets engines change behavior across major versions, and the fact that Vault becomes a single point of failure for every service that needs a database credential to start up. If Vault is down, nothing that leases short-lived credentials can start cleanly, which means Vault's own availability requirements are stricter than most of the things depending on it.
The managed options — HCP Vault, or AWS Secrets Manager and Google Secret Manager if you don't need Vault's full feature set — trade some of that operational surface for less flexibility and, usually, a narrower set of secrets engines. AWS Secrets Manager's RDS integration covers a real chunk of what people reach for the database secrets engine for, without a cluster to unseal. The honest tradeoff: self-hosted Vault is worth it once you need dynamic secrets across more than one cloud, more than one database engine, or auth methods a managed service doesn't offer. Below that line, the operational cost of running Vault well usually exceeds what it saves you over a managed secrets store plus disciplined manual rotation.
Vault starts sealed after every restart and refuses every request until it's unsealed with a quorum of unseal keys (or an auto-unseal mechanism backed by a cloud KMS). Auto-unseal via AWS KMS, GCP KMS, or Azure Key Vault removes the "someone needs to be on a call with a key share at 2am" scenario — set it up before you need it, not after the first restart during an incident.
Wrapping up
The point of dynamic secrets isn't that they're uncrackable — a leaked short-lived credential is still a leaked credential for as long as its lease lasts. The point is that "as long as its lease lasts" is now measured in an hour instead of "until someone notices and rotates it manually," which for most teams is the actual gap between a non-event and an incident. Start with the database secrets engine on your highest-value datastore, put Vault Agent or the Kubernetes auth method in front of it so no application code has to change, and only take on running Vault yourself once you've confirmed a managed alternative genuinely can't cover what you need.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.