API Key Rotation Automation with HashiCorp Vault
Manual key rotation works at small scale, but it breaks down as your infrastructure grows. Forgotten rotations, missed services, and human error during the process create exactly the kind of security gaps rotation is supposed to prevent. HashiCorp Vault solves this with two complementary features: dynamic secrets that generate short-lived credentials on demand, and automated rotation for static secrets that cannot be made dynamic. This guide walks through both approaches with production-ready Terraform and HCL configuration.
Dynamic Secrets: Eliminating Rotation Entirely
The most powerful approach to key rotation is to make it unnecessary. Vault's dynamic secrets engines generate unique, short-lived credentials for each request. When the credentials expire, they are automatically revoked. There is nothing to rotate because nothing lives long enough to become a security risk.
Setting Up Dynamic AWS Credentials
This is the most common dynamic secrets use case. Instead of sharing a single AWS access key across your team, each developer and each CI/CD run gets their own temporary credentials.
# Enable the AWS secrets engine
vault secrets enable aws
# Configure the root credentials Vault uses to generate dynamic keys
vault write aws/config/root \
access_key=$VAULT_AWS_ACCESS_KEY \
secret_key=$VAULT_AWS_SECRET_KEY \
region=us-east-1
# Define a role that generates credentials with specific permissions
vault write aws/roles/deploy-service \
credential_type=iam_user \
policy_arns=arn:aws:iam::123456789012:policy/DeployAccess \
default_ttl=1h \
max_ttl=4h
Now any authenticated client can request temporary credentials:
# Request credentials (returns unique access_key and secret_key)
vault read aws/creds/deploy-service
# Output:
# Key Value
# --- -----
# lease_id aws/creds/deploy-service/abc123
# lease_duration 1h
# access_key AKIA_TEMP_KEY_12345
# secret_key temp_secret_key_value
# security_token FwoGZX...
After one hour, Vault automatically deletes the IAM user and its access key. No rotation needed.
Dynamic Database Credentials
Database passwords are the second most common dynamic secret. Instead of a shared production password, each application instance gets its own credentials.
# Enable the database secrets engine
vault secrets enable database
# Configure the PostgreSQL connection
vault write database/config/production \
plugin_name=postgresql-database-plugin \
allowed_roles="app-readonly,app-readwrite" \
connection_url="postgresql://{{username}}:{{password}}@db.example.com:5432/myapp" \
username="vault_admin" \
password="vault_admin_password"
# Create a read-only role with 30-minute credentials
vault write database/roles/app-readonly \
db_name=production \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl=30m \
max_ttl=2h
# Application requests database credentials
vault read database/creds/app-readonly
# Output:
# Key Value
# --- -----
# lease_id database/creds/app-readonly/xyz789
# lease_duration 30m
# username v-app-readonly-abc123
# password A1b2C3d4E5f6G7h8
Terraform Configuration for Vault
Infrastructure as code ensures your Vault configuration is reproducible, version-controlled, and reviewable. Here is a complete Terraform module for setting up dynamic secrets.
# vault-secrets/main.tf
terraform {
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 3.0"
}
}
}
# AWS dynamic secrets
resource "vault_aws_secret_backend" "aws" {
path = "aws"
access_key = var.vault_aws_access_key
secret_key = var.vault_aws_secret_key
region = "us-east-1"
default_lease_ttl_seconds = 3600 # 1 hour
max_lease_ttl_seconds = 14400 # 4 hours
}
resource "vault_aws_secret_backend_role" "deploy" {
backend = vault_aws_secret_backend.aws.path
name = "deploy-service"
credential_type = "iam_user"
policy_arns = [
"arn:aws:iam::123456789012:policy/DeployAccess"
]
}
# Database dynamic secrets
resource "vault_mount" "database" {
path = "database"
type = "database"
}
resource "vault_database_secret_backend_connection" "postgres" {
backend = vault_mount.database.path
name = "production"
allowed_roles = ["app-readonly", "app-readwrite"]
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@db.example.com:5432/myapp"
username = var.db_admin_username
password = var.db_admin_password
}
}
resource "vault_database_secret_backend_role" "readonly" {
backend = vault_mount.database.path
name = "app-readonly"
db_name = vault_database_secret_backend_connection.postgres.name
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
]
revocation_statements = [
"DROP ROLE IF EXISTS \"{{name}}\";"
]
default_ttl = 1800 # 30 minutes
max_ttl = 7200 # 2 hours
}
# Policy granting access to the deploy role
resource "vault_policy" "deploy_policy" {
name = "deploy-service"
policy = <<-EOT
path "aws/creds/deploy-service" {
capabilities = ["read"]
}
path "database/creds/app-readonly" {
capabilities = ["read"]
}
EOT
}
Rotating Static Secrets with Vault
Not all secrets can be dynamic. Third-party API keys (Stripe, Twilio, OpenAI) are issued by external providers and cannot be generated on the fly. For these, Vault provides the KV secrets engine with versioning and a rotation workflow you automate externally.
Setting Up Versioned Static Secrets
# Enable KV v2 (versioned) secrets engine
vault secrets enable -version=2 -path=static kv
# Store a third-party API key
vault kv put static/stripe/production \
api_key=sk_live_current_key \
rotated_at="2026-03-09T00:00:00Z" \
next_rotation="2026-04-08T00:00:00Z"
# Read the current version
vault kv get static/stripe/production
# After rotation, store the new key (old version preserved)
vault kv put static/stripe/production \
api_key=sk_live_new_rotated_key \
rotated_at="2026-04-08T00:00:00Z" \
next_rotation="2026-05-08T00:00:00Z"
# Roll back to previous version if needed
vault kv rollback -version=1 static/stripe/production
Automating Static Rotation with a Cron Job
Combine Vault's API with your provider's key management API to build a fully automated rotation pipeline.
#!/bin/bash
# rotate-stripe-key.sh — runs on a 30-day cron schedule
set -euo pipefail
VAULT_ADDR="https://vault.example.com:8200"
VAULT_TOKEN="$(cat /run/secrets/vault-token)"
# Step 1: Create a new Stripe API key via Stripe's API
NEW_KEY=$(curl -s -u "$STRIPE_SECRET_KEY:" \
-X POST https://api.stripe.com/v1/api_keys \
| jq -r '.secret')
# Step 2: Store the new key in Vault
vault kv put static/stripe/production \
api_key="$NEW_KEY" \
rotated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
next_rotation="$(date -u -d '+30 days' +%Y-%m-%dT%H:%M:%SZ)"
# Step 3: Trigger application restart to pick up new key
kubectl rollout restart deployment/payment-service
# Step 4: Wait for rollout and verify
kubectl rollout status deployment/payment-service --timeout=300s
# Step 5: Revoke old key after verification
# (Keep old key ID in Vault metadata for audit trail)
echo "Rotation complete. Old key can be revoked after 48h verification window."
Vault Agent for Automatic Secret Injection
Vault Agent runs as a sidecar alongside your application. It authenticates with Vault, fetches secrets, renders them into template files, and automatically re-renders when secrets change or leases expire.
# vault-agent-config.hcl
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "my-app"
}
}
sink "file" {
config = {
path = "/tmp/vault-token"
}
}
}
template {
source = "/etc/vault-agent/templates/env.tpl"
destination = "/app/config/.env"
perms = 0600
# Re-render when the lease is 80% expired
command = "kill -HUP $(cat /app/app.pid)"
}
# Renew leases automatically
vault {
address = "https://vault.example.com:8200"
}
# /etc/vault-agent/templates/env.tpl
{{ with secret "database/creds/app-readonly" }}
DATABASE_USERNAME={{ .Data.username }}
DATABASE_PASSWORD={{ .Data.password }}
{{ end }}
{{ with secret "static/stripe/production" }}
STRIPE_SECRET_KEY={{ .Data.data.api_key }}
{{ end }}
When the database credentials are 80% through their TTL, Vault Agent automatically requests new credentials, re-renders the template, and sends a HUP signal to your application to reload its configuration. Zero human intervention required.
Monitoring and Alerting
Automated rotation is only as good as your visibility into it. Configure these monitoring checks:
- Lease expiration alerts. Query
vault list sys/leases/lookup/database/creds/app-readonlyand alert when any lease is within 5 minutes of expiration without a renewal. - Rotation schedule compliance. Check the
next_rotationmetadata field on static secrets daily. Alert if the current date exceeds the next rotation date. - Vault audit log monitoring. Parse Vault's audit logs for
authfailures,permission deniedresponses, and unusual access patterns. Ship these to your SIEM. - Token expiration tracking. Vault tokens used by applications and Vault Agent have their own TTLs. Monitor these separately from secret leases.
# Prometheus alert rule for Vault lease expiration
groups:
- name: vault_alerts
rules:
- alert: VaultLeaseExpiringSoon
expr: vault_secret_lease_remaining_seconds < 300
for: 1m
labels:
severity: warning
annotations:
summary: "Vault lease expiring in less than 5 minutes"
description: "Lease {{ $labels.lease_id }} expires soon."
- alert: VaultRotationOverdue
expr: vault_static_secret_days_since_rotation > 35
for: 1h
labels:
severity: critical
annotations:
summary: "Static secret rotation overdue"
description: "Secret {{ $labels.path }} has not been rotated in 35+ days."
Before enabling automated rotation in production: (1) test the full rotation cycle in staging at least three times, (2) verify rollback procedures work by intentionally reverting to a previous secret version, (3) confirm monitoring alerts fire correctly by simulating a lease expiration, and (4) document the rotation architecture for your on-call team.
Choosing TTL Values
Shorter TTLs mean better security but more operational complexity. Use these guidelines to select appropriate values:
- Database credentials: 15-60 minutes. Short enough that leaked credentials are useless quickly. Long enough that connection pools are not constantly being recycled.
- AWS IAM credentials: 1-4 hours. Matches typical CI/CD job durations. Shorter for production workloads, longer for batch processing.
- Static API keys: 30 days for payment and financial APIs, 60 days for standard third-party services, 90 days for low-risk internal services.
- Vault tokens: Match the TTL to the workload lifetime. A web server might use a 24-hour token with periodic renewal. A CI/CD job might use a 30-minute token with no renewal.
Automated key rotation with Vault transforms credential management from a manual chore into an invisible infrastructure concern. Start with dynamic secrets for databases and cloud providers where the impact is immediate, then layer in static secret rotation for third-party API keys. The combination of Terraform for configuration, Vault Agent for injection, and Prometheus for monitoring gives you a complete rotation pipeline that runs without human intervention.
Recommended Resources
- Real-World Cryptography — understand the cryptographic foundations behind dynamic secrets, key derivation, and encrypted transit.
- The Web Application Hacker's Handbook — learn how attackers exploit credential misconfigurations that rotation prevents.
- YubiKey 5 NFC — Hardware Security Key — protect your Vault admin access and cloud consoles with phishing-resistant 2FA.