HashiCorp Vault — Project: App Secret Management
Build a complete application secret management pipeline — from static secrets to dynamic database credentials with automated rotation.
Project Overview
┌──────────┐ AppRole ┌───────┐ Dynamic Creds ┌──────────┐
│ Web App │──────────────►│ Vault │──────────────────►│ PostgreSQL│
└──────────┘ └───────┘ └──────────┘
▲ │
│ Vault Agent │ KV Secrets
│ (sidecar) │ (API keys, configs)
└─────────────────────────┘
What you’ll build:
- KV store for static secrets (API keys, config)
- Database secrets engine for PostgreSQL credentials
- AppRole authentication for the application
- Vault Agent for automatic secret injection
- Lease management and rotation
Step 1 — Store Static Secrets
# Enable KV v2
vault secrets enable -path=apps kv-v2
# Store application config
vault kv put apps/webapp/config \
app_name="my-webapp" \
environment="production" \
log_level="info"
# Store API keys
vault kv put apps/webapp/api-keys \
stripe_key="sk_live_abc123" \
sendgrid_key="SG.xyz789" \
aws_access_key="AKIA..." \
aws_secret_key="wJalr..."
# Store database connection (static, before enabling dynamic)
vault kv put apps/webapp/database \
host="db.example.com" \
port="5432" \
database="webapp" \
sslmode="require"
Step 2 — Configure Dynamic Database Secrets
# Enable database secrets engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/webapp-db \
plugin_name=postgresql-database-plugin \
allowed_roles="webapp-readonly,webapp-readwrite" \
connection_url="postgresql://:@db.example.com:5432/webapp?sslmode=require" \
username="vault_admin" \
password="initial-password"
# Rotate the root password (Vault takes ownership)
vault write -force database/rotate-root/webapp-db
# Create a read-only role
vault write database/roles/webapp-readonly \
db_name=webapp-db \
creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"\";" \
default_ttl="1h" \
max_ttl="24h"
# Create a read-write role
vault write database/roles/webapp-readwrite \
db_name=webapp-db \
creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"\";" \
default_ttl="1h" \
max_ttl="24h"
Step 3 — Set Up AppRole Authentication
# Enable AppRole
vault auth enable approle
# Create policy for the web app
vault policy write webapp - <<EOF
# Static secrets (read-only)
path "apps/data/webapp/*" {
capabilities = ["read"]
}
# Dynamic database credentials
path "database/creds/webapp-readonly" {
capabilities = ["read"]
}
# Renew leases
path "sys/leases/renew" {
capabilities = ["update"]
}
EOF
# Create AppRole
vault write auth/approle/role/webapp \
secret_id_ttl=720h \
token_ttl=15m \
token_max_ttl=1h \
token_policies=webapp
# Get role_id and secret_id
ROLE_ID=$(vault read -field=role_id auth/approle/role/webapp/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/webapp/secret-id)
echo "role_id=$ROLE_ID"
echo "secret_id=$SECRET_ID"
Step 4 — Configure Vault Agent
# vault-agent.hcl
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/etc/vault-agent/role-id"
secret_id_file_path = "/etc/vault-agent/secret-id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/tmp/vault-token"
}
}
}
template {
contents = <<-EOT
# Application Config
{{- with secret "apps/data/webapp/config" }}
APP_NAME={{ .Data.data.app_name }}
ENVIRONMENT={{ .Data.data.environment }}
LOG_LEVEL={{ .Data.data.log_level }}
{{- end }}
# API Keys
{{- with secret "apps/data/webapp/api-keys" }}
STRIPE_KEY={{ .Data.data.stripe_key }}
SENDGRID_KEY={{ .Data.data.sendgrid_key }}
{{- end }}
# Database Credentials (dynamic)
{{- with secret "database/creds/webapp-readonly" }}
DB_USERNAME={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
{{- end }}
EOT
destination = "/etc/webapp/env"
command = "systemctl reload webapp"
}
vault {
address = "https://vault.example.com:8200"
}
# Write role_id and secret_id files
echo "$ROLE_ID" > /etc/vault-agent/role-id
echo "$SECRET_ID" > /etc/vault-agent/secret-id
# Start Vault Agent
vault agent -config=vault-agent.hcl
Step 5 — Test the Pipeline
# Test AppRole login
vault write auth/approle/login \
role_id=$ROLE_ID \
secret_id=$SECRET_ID
# Read static secrets
vault kv get apps/webapp/config
vault kv get apps/webapp/api-keys
# Get dynamic database credentials
vault read database/creds/webapp-readonly
# Note: username and password are auto-generated
# Verify the credentials work
psql "postgresql://<generated-user>:<generated-pass>@db.example.com:5432/webapp"
Architecture Summary
| Component | Purpose |
|---|---|
KV v2 (apps/) |
Static secrets (API keys, config) |
| Database engine | Dynamic PostgreSQL credentials |
| AppRole | Machine authentication |
| Vault Agent | Auto-auth + template rendering |
| Policies | Least-privilege access control |