HashiCorp Vault centralizes secret management with dynamic credentials that expire automatically — no more long-lived database passwords rotated manually. The dynamic secrets engine creates database users on-demand with customizable TTLs. PKI manages internal certificate authorities. Vault Agent delivers secrets to applications via templates without code changes. Transit provides encryption-as-a-service so applications never store plaintext keys. Claude Code generates Vault policy HCL, dynamic secrets configuration, PKI setup, Vault Agent configurations, and the Kubernetes auth integration for pod-based workflows.
CLAUDE.md for Vault Projects
## Vault Stack
- Version: Vault 1.16+ (Enterprise or OSS)
- Auth methods: Kubernetes (pods), AppRole (CI/CD), userpass (developers)
- Secrets engines: Dynamic DB (postgres, mysql, redis), AWS, PKI, KV v2
- Vault Agent: sidecar for secret injection + template rendering
- Policies: least-privilege — one policy per application
- Token TTL: 1h default, max 24h; secrets TTL: 15m for prod
- Audit: file audit device enabled on all clusters
- HA: Raft integrated storage (3-node minimum for production)
Dynamic PostgreSQL Credentials
# vault-config/database-engine.hcl
# Enable database secrets engine
path "sys/mounts/database" {
capabilities = ["create", "update"]
}
# Configure PostgreSQL connection
# vault write database/config/myapp-postgres ...
# Initial setup (run once by platform team)
vault secrets enable database
# Configure the database connection
vault write database/config/myapp-postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="readonly,readwrite,admin" \
connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/myapp?sslmode=require" \
username="vault-root" \
password="$VAULT_PG_ROOT_PASSWORD" \
password_authentication="scram-sha-256"
# Create readonly role — credentials expire after 15m in prod
vault write database/roles/readonly \
db_name=myapp-postgres \
creation_statements="
CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO \"{{name}}\";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"{{name}}\";
" \
revocation_statements="
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\";
DROP ROLE IF EXISTS \"{{name}}\";
" \
default_ttl="15m" \
max_ttl="1h"
# Create readwrite role for application use
vault write database/roles/readwrite \
db_name=myapp-postgres \
creation_statements="
CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO \"{{name}}\";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO \"{{name}}\";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO \"{{name}}\";
" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="15m" \
max_ttl="1h"
Vault Policies
# policies/app-api-server.hcl — policy for the API server application
# Dynamic database credentials
path "database/creds/readwrite" {
capabilities = ["read"]
}
# KV secrets for application config
path "secret/data/myapp/production/*" {
capabilities = ["read"]
}
# Renew own token
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Look up own token info
path "auth/token/lookup-self" {
capabilities = ["read"]
}
# policies/ci-cd-pipeline.hcl — policy for GitHub Actions / CI
# Read static secrets for deployment
path "secret/data/myapp/deploy/*" {
capabilities = ["read"]
}
# Read AWS credentials for deployment
path "aws/creds/deploy-role" {
capabilities = ["read"]
}
# Cannot access production database credentials
path "database/creds/*" {
capabilities = ["deny"]
}
Kubernetes Auth Integration
# vault-config/kubernetes-auth.hcl
# Enable Kubernetes auth method
# vault auth enable kubernetes
# Configure Kubernetes auth
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
# Bind Kubernetes service account to Vault policy
vault write auth/kubernetes/role/api-server \
bound_service_account_names=api-server \
bound_service_account_namespaces=production \
policies=app-api-server \
ttl=1h \
max_ttl=24h
# kubernetes/api-server-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
template:
metadata:
annotations:
# Vault Agent sidecar injection
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "api-server"
# Inject dynamic DB credentials as file
vault.hashicorp.com/agent-inject-secret-db: "database/creds/readwrite"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "database/creds/readwrite" -}}
DATABASE_URL=postgresql://{{ .Data.username }}:{{ .Data.password }}@postgres.internal:5432/myapp
{{- end }}
# Inject KV secret
vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/production/config"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/myapp/production/config" -}}
STRIPE_SECRET_KEY={{ .Data.data.stripe_secret_key }}
SENDGRID_API_KEY={{ .Data.data.sendgrid_api_key }}
{{- end }}
# Renew secrets before expiry
vault.hashicorp.com/agent-pre-populate-only: "false"
spec:
serviceAccountName: api-server
containers:
- name: api
image: myapp/api:latest
command: ["/bin/sh", "-c"]
args:
- |
# Load secrets from files injected by Vault Agent
export $(grep -v '^#' /vault/secrets/db | xargs)
export $(grep -v '^#' /vault/secrets/config | xargs)
exec /app/server
Vault Agent Standalone Configuration
# vault-agent/config.hcl — for non-Kubernetes environments
vault {
address = "https://vault.internal:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault/role-id"
secret_id_file_path = "/etc/vault/secret-id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/tmp/vault-token"
mode = 0640
}
}
}
cache {
use_auto_auth_token = true
}
template {
source = "/etc/vault/templates/database.ctmpl"
destination = "/app/secrets/database.env"
perms = 0640
# Restart the app when secrets rotate
command {
command = "systemctl reload myapp"
timeout = "30s"
}
}
template {
source = "/etc/vault/templates/app-config.ctmpl"
destination = "/app/secrets/config.env"
perms = 0640
}
{{/* /etc/vault/templates/database.ctmpl */}}
{{- with secret "database/creds/readwrite" -}}
DB_HOST=postgres.internal
DB_PORT=5432
DB_NAME=myapp
DB_USERNAME={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
DATABASE_URL=postgresql://{{ .Data.username }}:{{ .Data.password }}@postgres.internal:5432/myapp
{{- end }}
PKI Certificate Authority
# Set up internal CA for mTLS between services
vault secrets enable -path=pki pki
vault secrets tune -max-lease-ttl=87600h pki # 10 years
# Generate root CA
vault write -field=certificate pki/root/generate/internal \
common_name="MyApp Internal CA" \
issuer_name="root-2026" \
ttl=87600h > root-cert.pem
# Set URLs
vault write pki/config/urls \
issuing_certificates="https://vault.internal:8200/v1/pki/ca" \
crl_distribution_points="https://vault.internal:8200/v1/pki/crl"
# Create intermediate CA
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int # 5 years
vault write -format=json pki_int/intermediate/generate/internal \
common_name="MyApp Intermediate CA" | jq -r '.data.csr' > int.csr
vault write -format=json pki/root/sign-intermediate \
issuer_ref="root-2026" [email protected] format=pem_bundle \
ttl=43800h | jq -r '.data.certificate' > int-cert.pem
vault write pki_int/intermediate/set-signed [email protected]
# Create role for service certificates
vault write pki_int/roles/internal-services \
issuer_ref="default" \
allowed_domains="internal,svc.cluster.local" \
allow_subdomains=true \
allow_glob_domains=false \
max_ttl=2160h # 90 days
generate_lease=true
# Issue a certificate for api-server
vault write pki_int/issue/internal-services \
common_name="api-server.internal" \
alt_names="api-server.production.svc.cluster.local" \
ttl=720h # 30 days
Transit Encryption-as-a-Service
// lib/vault-transit.ts — encrypt sensitive data via Vault Transit
import axios from 'axios'
const VAULT_ADDR = process.env.VAULT_ADDR!
const VAULT_TOKEN = process.env.VAULT_TOKEN! // Injected by Vault Agent
interface TransitEncryptResponse {
data: { ciphertext: string }
}
export async function encryptField(keyName: string, plaintext: string): Promise<string> {
const encoded = Buffer.from(plaintext).toString('base64')
const response = await axios.post<TransitEncryptResponse>(
`${VAULT_ADDR}/v1/transit/encrypt/${keyName}`,
{ plaintext: encoded },
{ headers: { 'X-Vault-Token': VAULT_TOKEN } }
)
return response.data.data.ciphertext
}
export async function decryptField(keyName: string, ciphertext: string): Promise<string> {
const response = await axios.post(
`${VAULT_ADDR}/v1/transit/decrypt/${keyName}`,
{ ciphertext },
{ headers: { 'X-Vault-Token': VAULT_TOKEN } }
)
const decoded = Buffer.from(response.data.data.plaintext, 'base64').toString()
return decoded
}
// Usage: encrypt PII before storing in database
export async function encryptUserPII(user: { ssn: string; creditCard: string }) {
return {
ssn: await encryptField('user-pii', user.ssn),
creditCard: await encryptField('payment-data', user.creditCard),
}
}
# Create transit encryption key (cannot be exported — Vault holds it)
vault write transit/keys/user-pii type=aes256-gcm96
vault write transit/keys/payment-data \
type=aes256-gcm96 \
allow_rotation=true \
min_decryption_version=1
# Rotate key periodically (old ciphertext still decryptable)
vault write transit/keys/payment-data/rotate
AppRole Auth for CI/CD
# Set up AppRole for GitHub Actions
vault auth enable approle
vault write auth/approle/role/github-actions \
role_id="github-actions-production" \
secret_id_ttl="30m" \
token_ttl="1h" \
token_max_ttl="4h" \
token_policies="ci-cd-pipeline" \
secret_id_num_uses=1 # Single-use secret IDs (for security)
# In GitHub Actions workflow:
# 1. Fetch secret ID (never cached)
# 2. Login to get token
# 3. Use token for deployment
# 4. Token expires automatically
# .github/workflows/deploy.yml — Vault integration
- name: Get Vault token
id: vault
run: |
ROLE_ID=$(vault read -field=role_id auth/approle/role/github-actions/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/github-actions/secret-id)
VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID")
echo "::add-mask::$VAULT_TOKEN"
echo "vault_token=$VAULT_TOKEN" >> $GITHUB_OUTPUT
env:
VAULT_ADDR: ${{ secrets.VAULT_ADDR }}
VAULT_TOKEN: ${{ secrets.VAULT_BOOTSTRAP_TOKEN }} # Limited token just for getting app tokens
- name: Read deploy secrets
run: |
export AWS_ACCESS_KEY_ID=$(vault read -field=access_key aws/creds/deploy-role)
export AWS_SECRET_ACCESS_KEY=$(vault read -field=secret_key aws/creds/deploy-role)
# Deploy using dynamic AWS credentials
./deploy.sh
env:
VAULT_ADDR: ${{ secrets.VAULT_ADDR }}
VAULT_TOKEN: ${{ steps.vault.outputs.vault_token }}
For the Kubernetes infrastructure that uses Vault’s Kubernetes auth method and sidecar injection, see the Kubernetes guide for pod deployment patterns. For the CI/CD pipeline that integrates Vault’s AppRole for dynamic credentials during deployment, the GitHub Actions Advanced guide covers the pipeline automation layer. The Claude Skills 360 bundle includes Vault skill sets covering dynamic secrets, PKI, and Kubernetes integration. Start with the free tier to try Vault policy generation.