Skip to content

Instantly share code, notes, and snippets.

@mohashari
Created March 14, 2026 23:30
Show Gist options
  • Select an option

  • Save mohashari/1b3535e93040e28aba679c2c8a00ee59 to your computer and use it in GitHub Desktop.

Select an option

Save mohashari/1b3535e93040e28aba679c2c8a00ee59 to your computer and use it in GitHub Desktop.
HashiCorp Vault: Secrets Management for Production Systems
# policies/myapp.hcl
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}
path "sys/leases/renew" {
capabilities = ["update"]
}
vault policy write myapp policies/myapp.hcl
-- Vault needs a privileged role to create/revoke users
CREATE ROLE vault_admin WITH LOGIN PASSWORD 'vault_admin_pass' CREATEROLE;
-- Grant necessary permissions
GRANT ALL PRIVILEGES ON DATABASE appdb TO vault_admin;
GRANT ALL ON SCHEMA public TO vault_admin;
-- Template Vault will use when creating dynamic users
-- Vault substitutes {{name}} and {{password}} at runtime
# Enable the database secrets engine
vault secrets enable database
# Configure the PostgreSQL connection
vault write database/config/appdb \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/appdb" \
allowed_roles="myapp-role" \
username="vault_admin" \
password="vault_admin_pass"
# Define a role: credentials expire after 1 hour, max lease 24 hours
vault write database/roles/myapp-role \
db_name=appdb \
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}}\";" \
default_ttl="1h" \
max_ttl="24h"
# Test it — Vault creates a real Postgres user
vault read database/creds/myapp-role
package secrets
import (
"context"
"database/sql"
"fmt"
vault "github.com/hashicorp/vault/api"
_ "github.com/lib/pq"
)
type DBCredentials struct {
Username string
Password string
}
func GetDynamicDBCredentials(ctx context.Context) (*DBCredentials, error) {
config := vault.DefaultConfig()
// VAULT_ADDR and VAULT_TOKEN are read from environment
client, err := vault.NewClient(config)
if err != nil {
return nil, fmt.Errorf("vault client init: %w", err)
}
secret, err := client.Logical().ReadWithContext(ctx, "database/creds/myapp-role")
if err != nil {
return nil, fmt.Errorf("reading db creds: %w", err)
}
if secret == nil {
return nil, fmt.Errorf("no secret returned from vault")
}
return &DBCredentials{
Username: secret.Data["username"].(string),
Password: secret.Data["password"].(string),
}, nil
}
func NewDBPool(ctx context.Context, host string) (*sql.DB, error) {
creds, err := GetDynamicDBCredentials(ctx)
if err != nil {
return nil, err
}
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=appdb sslmode=require",
host, creds.Username, creds.Password)
return sql.Open("postgres", dsn)
}
package auth
import (
"context"
"fmt"
vault "github.com/hashicorp/vault/api"
)
func AppRoleLogin(ctx context.Context, roleID, secretID string) (*vault.Client, error) {
config := vault.DefaultConfig()
client, err := vault.NewClient(config)
if err != nil {
return nil, fmt.Errorf("vault client: %w", err)
}
// Exchange roleID + secretID for a token
data := map[string]interface{}{
"role_id": roleID,
"secret_id": secretID,
}
secret, err := client.Logical().WriteWithContext(ctx, "auth/approle/login", data)
if err != nil {
return nil, fmt.Errorf("approle login: %w", err)
}
// Set the resulting token on the client
client.SetToken(secret.Auth.ClientToken)
// Start background token renewal
renewer, err := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
Secret: secret,
})
if err != nil {
return nil, fmt.Errorf("token renewer: %w", err)
}
go renewer.Start()
return client, nil
}
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server ./cmd/server
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
# VAULT_ADDR, VAULT_ROLE_ID, and VAULT_SECRET_ID are injected
# by the orchestrator at container start — never hardcoded here
ENV VAULT_ADDR=""
EXPOSE 8080
ENTRYPOINT ["./server"]
# Start Vault dev server (in a separate terminal)
vault server -dev -dev-root-token-id="root"
# In your working terminal
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='root'
# Enable KV v2 secrets engine
vault secrets enable -path=secret kv-v2
# Write your first secret
vault kv put secret/myapp/database \
username="app_user" \
password="s3cur3p@ss" \
host="postgres.internal:5432"
# Verify
vault kv get secret/myapp/database
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment