A five-person team with no dedicated SRE function usually shares one database admin password. It lives in a password manager entry, a CI secret, and someone’s shell history. The credential is convenient and it is also the reason a single offboarding takes a week, a leaked log line becomes a full-database incident, and nobody can answer “who ran that migration” without guessing. This is a field note on replacing that shared credential with individual, short-lived Vault-issued logins over a weekend, and on the parts that realistically need a staged rollout.
What the weekend can actually deliver
The realistic weekend scope is: enable Vault’s database secrets engine against PostgreSQL, define one or two roles that map to the privileges the team already uses, wire human authentication through an auth method, and cut over interactive access. Automation cutover, break-glass design, and audit review are usually the following week. Trying to do all of it in 48 hours is how teams end up with a half-migrated credential and two sources of truth.
Vault’s PostgreSQL database secrets engine generates credentials dynamically based on configured roles, and it also supports static roles. The plugin is named postgresql-database-plugin, and the documented setup is a vault secrets enable database followed by a vault write database/config/... with a connection URL and a privileged username. The role definition carries the SQL that Vault executes to create the credential, for example CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; plus whatever grants the role needs. Default and max TTL are set per role; the documentation example uses default_ttl="1h" and max_ttl="24h". That is the whole mechanism. It is not exotic, and it does not require a platform team.
Two details from the same documentation matter for a small team. First, the plugin supports password_authentication="scram-sha-256", which is the setting you want on any PostgreSQL 14+ instance rather than the older md5 default. Second, the rootless static-role workflow is Vault Enterprise only, and it explicitly does not support dynamic roles. If you are on Vault Community, plan on a single privileged connection user for the database secrets engine and treat that user as a high-value credential in its own right.
Human authentication: keep it boring
For five engineers, the userpass auth method is sufficient and auditable. It allows users to authenticate with a username and password configured directly on the auth method, and it lowercases all submitted usernames, so Mary and mary are the same entry. The documented enablement is vault auth enable userpass, then vault write auth/userpass/users/<name> password=... policies=.... User lockout is enabled by default from Vault 1.13 onward, with a default threshold of 5 failed attempts, a 15-minute lockout duration, and a 15-minute counter reset. That default is a reasonable starting point; it also means a fat-fingered engineer during an incident will be locked out for 15 minutes, so decide in advance whether the on-call policy needs a second path.
If your team already has an identity provider, an OIDC or LDAP auth method is a better long-term choice than userpass because offboarding happens in the IdP rather than in Vault. The weekend tradeoff is that userpass is one command per person and OIDC is a configuration project. Either is defensible; the failure mode to avoid is leaving the shared password in place “until we finish the OIDC work.”
PostgreSQL roles: separate the human from the machine
The mistake that makes this migration painful is conflating the human login with the application login. PostgreSQL’s CREATE USER is an alias for CREATE ROLE with LOGIN assumed by default, and the privilege model is role-based: a role holds privileges directly, through role membership, and through anything granted to PUBLIC. That means you can build a small number of privilege-bearing roles and hand out membership, rather than granting table privileges to every human individually.
A workable shape for a five-person team:
app_rw— the role the application connects as. Owned by the application’s service account, not by humans.oncall_read—CONNECTon the database,USAGEon the relevant schemas,SELECTon the tables the team actually needs to inspect during an incident.oncall_write— the narrower set of write privileges the team has historically needed for hotfixes, granted explicitly rather than viaALL PRIVILEGES.migration— the role that owns schema changes, used by the migration tool and by humans only during a planned change window.
Vault roles then map to these: a readonly role whose creation statement grants oncall_read, and a breakglass role whose creation statement grants oncall_write with a short TTL. The GRANT documentation is explicit that granting a privilege on a table does not extend to sequences used by that table, including sequences tied to SERIAL columns, so any role that inserts into a table with a serial primary key needs USAGE on the sequence as well. This is the kind of detail that turns a “read-only” role into a broken role at 02:00.
One more note from the GRANT documentation worth internalizing: superusers can access all objects regardless of object privilege settings, and the documentation compares this to root on a Unix system and advises against operating as a superuser except when absolutely necessary. The shared admin password was almost certainly a superuser. The point of this migration is to make superuser access rare, named, and time-bound.
Break-glass without reintroducing the shared password
The objection that stalls this migration is always the same: “what happens when Vault is down and we need the database?” The answer is not a shared password in a sealed envelope. It is a separate, deliberately awkward path that is still individual and still logged.
OWASP’s Secrets Management Cheat Sheet has a section on downtime, break-glass, backup and restore, and its general guidance is that secrets should be centralized, access-controlled on a least-privilege basis, and audited. The cheat sheet’s auditing section lists what should be captured at minimum: who requested a secret and for what system and role, whether the request was approved or rejected, when the secret was used and by whom, when it expired, whether there were attempts to reuse expired secrets, authentication and authorization errors, and administrative actions. That list is a good specification for what your break-glass path must produce.
A design that satisfies it without a shared password:
- A dedicated PostgreSQL role,
breakglass, withLOGINand a password stored in a separate secrets manager that is not Vault — for example, the cloud provider’s native secret store, or a hardware-backed password manager entry that requires a second approver to reveal. - The password is rotated after every use, and the rotation is a documented runbook step, not a memory.
- Every use is announced in the incident channel before the credential is revealed, and the reveal is logged by the secrets manager.
- PostgreSQL’s own connection logging captures the login, so the database side has an independent record.
The tradeoff is real: this path is slower than typing a shared password. That is the point. A break-glass path that is as convenient as normal access will become normal access.
What to stage rather than rush
Interactive human access is the safe first cutover because a broken login is immediately visible and the blast radius is one engineer. The following should be staged:
- Application credentials. Moving an application from a static password to Vault dynamic credentials requires the application to fetch and refresh credentials, or a sidecar to do it. OWASP describes the sidecar pattern — a Vault Agent sidecar authenticating with a Kubernetes service account, writing the secret to a shared in-memory volume, and refreshing it periodically — as a way to decouple the application from the secrets manager. That is a code and deployment change, not a weekend change.
- CI and migration tooling. These often hold the longest-lived credentials and are the hardest to rotate because a failed rotation breaks deploys. Give them their own Vault role with a TTL that matches the deploy cadence, and test the rotation in a staging environment first.
- Monitoring and alerting. If your synthetic checks or backup jobs connect to PostgreSQL, they need their own role. A backup job that fails silently because its credential expired is a worse outcome than the shared password you started with.
This is the same discipline as writing the recovery checklist before you need it — the migration is a rehearsal, and the parts you cannot rehearse in a weekend should not be cut over in a weekend. See Write the Recovery Checklist Before You Need It for the pattern of documenting the path before the incident forces you to improvise it.
Audit evidence that the shared credential is actually retired
Retiring the shared password is not the same as deleting it from the password manager. The credential may still exist in CI variables, in a Terraform state file, in a developer’s ~/.pgpass, or in a runbook. The verification step is to look for evidence of use, not evidence of deletion.
Three sources, in order of usefulness:
- PostgreSQL connection logs. If
log_connectionsis on, every successful login is recorded with the role name. Query the logs for the old shared username over a two-week window after cutover. Any hit is a system you missed. - Vault audit logs. Vault’s audit device records every request, including credential issuance. The OWASP auditing guidance applies here: you want to see who requested which role, when, and whether the request succeeded. If your audit device is not enabled before the migration, enable it first — a migration with no audit trail is a migration you cannot verify.
- Access review. A short written review, dated, listing each engineer, their Vault entity, their policies, and the PostgreSQL roles their Vault roles map to. This is the artifact that answers “who can reach production data” without a meeting.
NIST SP 800-53 Rev. 5 is the reference catalog if your organization needs a control mapping; the Access Control, Audit and Accountability, and Identification and Authentication families are the relevant ones. For a five-person team, the practical value is the vocabulary, not the full control set.
Onboarding and offboarding after the change
The measurable win of this migration is that offboarding becomes a single action. Before, removing someone meant finding every place the shared password was copied. After, it means disabling their Vault entity and, if you use an IdP-backed auth method, disabling their IdP account. Their dynamic credentials expire on their own; there is nothing to rotate.
Onboarding is similarly short: create the Vault entity, attach the policy that allows reading the appropriate database role, and confirm the engineer can issue a credential and connect. The PostgreSQL side needs no per-person role creation because the Vault role’s creation statement handles it. The one thing that does not disappear is the access review — a new engineer with oncall_write is a decision worth recording.
The honest tradeoff: dynamic credentials mean that a long-running interactive session will be interrupted when its TTL expires. Set the default TTL to something that matches how your team actually works — an hour is a reasonable starting point for interactive access, and the documentation example uses exactly that — and document the renewal command in the runbook. An engineer who does not know how to renew a lease during an incident will reach for the break-glass path, which defeats the design.
Questions that come up
Do we need Vault Enterprise? No, for dynamic PostgreSQL credentials. The rootless static-role workflow is Enterprise-only and does not support dynamic roles, so Community users configure the database secrets engine with a privileged connection user. That user becomes a credential to protect carefully.
What TTL should we use? The documentation example uses default_ttl="1h" and max_ttl="24h". For interactive human access, an hour is a reasonable default; for CI, match the deploy cadence; for break-glass, shorter than either. These are starting points, not findings.
Can we keep the shared password as a fallback? Only if it is rotated, stored separately from Vault, and its use is logged and reviewed. A fallback that is never rotated is the original problem with a new name.
What if Vault is down? The break-glass path above is the answer. Test it once before you need it, and record the test in the runbook.
How do we know the migration worked? The old shared username stops appearing in PostgreSQL connection logs, Vault audit logs show individual credential issuance, and the access review lists every engineer by name. Three artifacts, all checkable.