API Key Rotation Automation with HashiCorp Vault

Published March 9, 2026 · 12 min read · By SPUNK LLC

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:

# 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."
Production Checklist

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:

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