Skip to main content

Configuring dynamic secrets for a PostgreSQL and GitLab CI using HashiCorp Vault

Learn how to set up and issue short-lived credentials for a PostgreSQL database and in a GitLab CI pipeline with Vault, a secrets management platform.

Many teams begin with static, hardcoded credentials for databases — often buried in config files or environment variables. Getting your secret rotation automated on a fixed schedule (daily, weekly, monthly) is a good first step, but to truly minimize the risks of credential theft as much as possible requires adoption of “dynamic” secrets (sometimes called “ephemeral secrets” or “just-in-time secrets”). By issuing short-lived credentials that expire automatically — often within minutes or hours — organizations can drastically minimize the attack window if a secret becomes compromised.

In this follow-up to Why we need short-lived credentials and how to adopt them — a manager and architect-oriented post on this topic, I’ll walk through two practical scenarios: Issuing short-lived credentials for a PostgreSQL database and retrieving static vs. dynamic secrets in GitLab CI. Both examples use HashiCorp Vault to create ephemeral database users with a limited lifespan using dynamic secrets.

»Before and after dynamic secrets in PostgreSQL

The first example shows how the configurations and commands typically look starting with hardcoded credentials and migrating into dynamic credentials in a PostgreSQL database.

»Before: Hardcoded credentials

Developers and database administrators are familiar with how hardcoded credentials look. In this example below, the PostgreSQL database is using hardcoded credentials in environment variables.

#!/usr/bin/env bash
 
# Hardcoded credentials in environment variables
DB_HOST="my-postgres-host"
DB_USER="legacy_user"
DB_PASS="staticPasswordxyz"
DB_PORT=5432
 
# The script connects using these long-lived credentials
psql -h "$DB_HOST" -U "$DB_USER" -d my_database -c "SELECT * FROM user_schema;"

The goal is to move these credentials into a secrets manager like Vault and put leases on them that expunge the credentials after a reasonably short window and re-generate the credentials automatically so that no manual effort is required.

»After: Dynamic credentials

Vault uses secrets engines to manage dynamic secrets. In this example, you would enable the database secrets engine with the PostgreSQL plugin. This 4-step example will show how you manually implement dynamic secrets through the command line. In the second example later in the article, you’ll see how to make your application or CI/CD pipeline request these credentials via API calls or a Vault client library.

»1. Enable and configure the database secrets engine

In the command line, here’s how you enable and configure the database secrets engine for PostgreSQL in Vault:

# Enable the database secrets engine (if not already enabled)
vault secrets enable database
 
# Configure Vault to connect to your PostgreSQL database
vault write database/config/my-postgres-database \
plugin_name="postgresql-database-plugin" \
allowed_roles="my-db-role" \  connection_url="postgresql://{{username}}:{{password}}@my-postgres-host:5432/postgres?\
sslmode=disable" \
username="vault_admin" \
password="SuperSecurePassword123"
  • plugin_name: Identifies which database plugin to use (PostgreSQL in this case).
  • allowed_roles: Lists which Vault roles can be mapped to this database config.
  • connection_url: Contains the placeholder parameters for username and password, replaced by Vault.
  • username/password: The admin-level credentials Vault uses to dynamically create new roles in PostgreSQL.

»2. Create a role with a short lease

Next you create a role in the command line and give it a time-to-live (TTL):

vault write database/roles/my-db-role \
db_name="my-postgres-database" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="15m" \
max_ttl="30m"
  • creation_statements: A PostgreSQL command that defines what privileges the new user gets (e.g., SELECT on all tables).
  • default_ttl: The initial time-to-live (in this case 15 minutes).
  • max_ttl: The maximum extension if you renew (30 minutes max).

»3. Request dynamic secrets

Finally you use the command line to put the dynamic secrets variable into the configuration for PostgreSQL:

DB_CREDS=$(vault read -format=json database/creds/my-db-role)
DB_USER=$(echo "$DB_CREDS" | jq -r '.data.username')
DB_PASS=$(echo "$DB_CREDS" | jq -r '.data.password')
 
echo "Ephemeral username: $DB_USER"
# Avoid echoing DB_PASS in logs if possible
# Use them to connect to PostgreSQL, e.g.:
 
psql -h "my-postgres-host" -U "$DB_USER" -d my_database -c "SELECT * FROM user_schema;"
  • vault read obtains unique credentials each time, valid only for default_ttl (15 minutes).
  • lease_duration indicates these credentials automatically expire in 15 minutes.
  • If leaked, the credentials are only useful for 15 minutes (default TTL). If they’re acquired or used after the TTL expiration, they won’t work.
  • lease_id can be used to manually revoke or renew the credential ahead of time if needed.

»4. Testing revocation

After the lease expires, Vault revokes the user from PostgreSQL automatically. Here’s the command you would type if you want to manually revoke the user at any time:

vault lease revoke database/creds/my-db-role/abc123

After the lease expires (or upon manual revoke), Vault removes the DB user automatically. You can try using the old credentials for subsequent login attempts and those credentials will fail. You’ve just created, used, and revoked a short-lived PostgreSQL credential.

In practice, your application or CI/CD pipeline would request these credentials via API calls or a Vault client library, ensuring they’re fetched securely each time they’re needed. Now that you’ve seen how Vault issues ephemeral DB credentials, let’s see how you can retrieve secrets — both static and dynamic — in a real CI/CD environment like GitLab.

»Retrieving secrets in GitLab

When integrating Vault into your CI pipelines, you can either pull static secrets from Vault’s KV store or generate short-lived dynamic secrets via one of Vault’s dynamic secrets engines. Below are two code snippets, each showing a different approach.

»Static KV-based secret retrieval

This example reads a static secret from Vault’s KV engine. While it demonstrates Vault integration, it’s not ephemeral: the secret remains valid until it’s manually rotated or overwritten:

.gitlab-ci.yml

variables:
  VAULT_SERVER_URL: "https://vault.example.com"
  VAULT_AUTH_ROLE: "gitlab-role"
  # Make sure to configure GitLab so it can authenticate to Vault (e.g., AppRole).
 
stages:
  - build
 
build_job:
  stage: build
  script:
    - echo "Fetching a static secret from Vault's KV store..."
    # If you're using KV v2, adjust the path to secret/data/myapp/settings
    - export STATIC_SECRET=$(vault kv get -field=value secret/myapp/settings)
    - echo "Got static secret: $STATIC_SECRET"
    # Proceed with your build steps
    - ./build-script.sh
  • This snippet retrieves a static secret from secret/myapp/settings. There’s no automatic TTL or revocation associated with it.
  • You must configure how GitLab obtains Vault credentials (AppRole or another auth method). If you’re on KV v2, your actual path may look like secret/data/myapp/settings.

»Dynamic secrets retrieval via the database secrets engine

Below is a truly ephemeral approach, using Vault’s database secrets engine to generate short-lived, just-in-time credentials for a PostgreSQL database. Each time the pipeline runs, it requests new credentials that automatically expire after a set TTL — reducing the blast radius/timeframe for stolen secrets.

.gitlab-ci.yml

variables:
  VAULT_SERVER_URL: "https://vault.example.com"
  VAULT_AUTH_ROLE: "gitlab-role"
  # Configure GitLab -> Vault auth just like in the KV example.
 
stages:
  - deploy
 
deploy_job:
  stage: deploy
  script:
    - echo "Fetching ephemeral DB credentials..."
    # This reads from the database secrets engine (e.g., database/creds/my-db-role)
    - DB_CREDS=$(vault read -format=json database/creds/my-db-role)
    - DB_USER=$(echo "$DB_CREDS" | jq -r '.data.username')
    - DB_PASS=$(echo "$DB_CREDS" | jq -r '.data.password')
 
    - echo "Ephemeral Username: $DB_USER"
    # Avoid echoing $DB_PASS in logs for security reasons
    # The credential automatically expires after its TTL (e.g. 15m)
 
    - ./deploy.sh --db-user="$DB_USER" --db-pass="$DB_PASS"

The database secrets engine’s role sets a default TTL (e.g. 15 minutes). Once that time passes, the credential is automatically revoked. Once the credential expires, the application or pipeline would need to request fresh credentials from Vault again. If in any worst-case scenario, these credentials are ever leaked, attackers have only a short window before they become invalid.

»Learn more

Short-lived dynamic credentials dramatically reduce risk compared to static secrets, minimizing the window in which attackers can exploit stolen credentials. By centralizing secrets in Vault, automating rotation, and gradually moving services to dynamic secrets, you align with zero trust principles and improve operational efficiency.

To learn more about dynamic secrets and HashiCorp Vault, visit:

Sign up for the latest HashiCorp news

By submitting this form, you acknowledge and agree that HashiCorp will process your personal information in accordance with the Privacy Policy.