HashiCorp Vault — Project: App Secret Management

2 minute read

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

Next: PKI Infrastructure Project