BlogSecrets

Dynamic database credentials for Jenkins with Vault

Stop pasting Postgres passwords into Jenkins credentials. Vault mints a fresh user per build and revokes it when the job ends.

Mar 24, 2026·5 min readIntermediate·By the SecOpsLog team · command-tested

A static Postgres password pasted into Jenkins credentials is a secret with no expiry, shared by every build on every branch, visible to anyone with Job/Configure permission, and sitting in a heap dump the first time a pipeline crashes with a core file. Rotating it is a quarterly fire drill that breaks in-flight jobs. Vault's database secrets engine replaces the pattern entirely: Jenkins asks Vault for credentials at job start, Vault runs CREATE ROLE against Postgres with a short TTL, the build uses a unique username, and Vault drops the role when the lease expires — or when you explicitly revoke it on job completion.

The admin connection Vault uses to mint users is powerful — treat it like a break-glass credential with allowed_roles restricting which roles it can create. Keep TTL just longer than your slowest migration job, not hours. Pair with the HashiCorp Vault Jenkins plugin so credentials never touch the Jenkins credential store or disk. The Vault from dev to production track covers the database engine, policies, and CI integration patterns.

Credential lifecycle per build

A crashed build still holds a lease until TTL expires. Revoke on completion or keep TTL aggressively short.

1Build startsJenkins authenticates to Vault2Read roledatabase/creds/jenkins-ci3Vault → DBCREATE ROLE, grant perms4Build runsDB_USER / DB_PASS in stage only5Job endsrevoke lease or wait TTL6Vault → DBDROP ROLE automatically7Auditno static cred in Jenkins store

Enable the database secrets engine

Point Vault at your database with a dedicated admin user Vault uses only to create and drop application roles. The allowed_roles list on the connection config is the guardrail — this connection cannot mint arbitrary roles outside that list even if someone misconfigures a policy.

vault-setup.sh
vault secrets enable database
vault write database/config/appdb \
plugin_name=postgresql-database-plugin \
allowed_roles="jenkins-ci" \
connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/app?sslmode=require" \
username="vault_admin" \
password="$ADMIN_PW"

A role that mints short-lived users

The role defines the SQL Vault executes to create a user and the TTL before automatic revocation. Grant only what migrations need — SELECT, INSERT, UPDATE, DELETE on application tables, not SUPERUSER. Use VALID UNTIL in creation statements so Postgres enforces expiry even if Vault's revocation path fails.

vault-role.sh
vault write database/roles/jenkins-ci \
db_name=appdb \
default_ttl="10m" max_ttl="20m" \
creation_statements="\
CREATE ROLE \"{{name}}\" LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"

Read the role once from a test shell and confirm Vault returns a unique username, password, and lease metadata before you wire Jenkins.

bash — vault read database/credslive
vault read database/creds/jenkins-ci
Key Value
--- -----
lease_id database/creds/jenkins-ci/8Kx2..
lease_duration 10m
username v-jenkins-ci-6Yh0
password A1a-9f3Kp2mZ-qX7
user auto-dropped when the 10m lease expires

Wire it into the pipeline

The HashiCorp Vault plugin's withVault block injects secrets as environment variables scoped to a single stage. They never enter the Jenkins credential store, never appear in job config XML, and disappear when the block exits. Authenticate Jenkins to Vault with AppRole or JWT — not a root token in an environment variable on the controller.

Jenkinsfile
withVault(vaultSecrets: [[
path: 'database/creds/jenkins-ci',
secretValues: [
[envVar: 'DB_USER', vaultKey: 'username'],
[envVar: 'DB_PASS', vaultKey: 'password'],
]
]]) {
sh './run-migrations.sh' // DB_USER / DB_PASS live only in this block
}
Revoke on failure, too
A crashed build still holds a database lease until TTL expires — an attacker with Jenkins log access may find credentials in env dumps. Call vault lease revoke in a post { always } block, or keep TTL short enough that the window is acceptable. Never extend max_ttl to hours just because one slow job exists; fix the job or use a dedicated long-TTL role with tighter grants.

Least-privilege Vault policy for Jenkins

Jenkins needs read on exactly one path — database/creds/jenkins-ci — and optionally update on that path if you use renew. It does not need access to KV, PKI, or other database roles. Separate AppRoles per Jenkins controller or per folder so a compromised dev job cannot read production credentials.

jenkins-policy.hcl
path "database/creds/jenkins-ci" {
capabilities = ["read"]
}
# no path "database/creds/*" — scope to one role

Where this goes next

The same pattern extends to Vault's AWS secrets engine for deployment jobs, PKI for short-lived TLS in integration tests, and SSH OTP for ephemeral bastion access. Once builds stop carrying long-lived secrets, credential rotation becomes continuous instead of quarterly. The Vault from dev to production path covers dynamic secrets, auth methods for CI, and operating Vault in production.

Go deeper in a courseVault from dev to productionDatabase engine, dynamic secrets, CI auth, and rotation without fire drills.View course

Related posts