-
-
Notifications
You must be signed in to change notification settings - Fork 0
Vault Integration
HashiCorp Vault provides centralized secrets management and Public Key Infrastructure (PKI) for services. Instead of storing passwords in .env files, services fetch credentials from Vault at startup.
Benefits:
- ✅ Centralized secrets management
- ✅ Dynamic certificate generation
- ✅ Automatic certificate rotation
- ✅ Audit trail of secret access
- ✅ Optional SSL/TLS for encrypted connections
- ✅ No plaintext passwords in configuration files
Architecture:
Vault PKI Hierarchy
├── Root CA (10-year validity)
│ └── Intermediate CA (5-year validity)
│ └── Service Certificates (1-year validity)
│ ├── PostgreSQL
│ ├── MySQL
│ ├── Redis
│ └── Other services
ALL services use Vault integration with AppRole authentication for credentials management. AppRole migration completed November 2025 (100% of services).
Integrated Services (All using AppRole):
- ✅ PostgreSQL (
configs/postgres/scripts/init-approle.sh) - AppRole authentication - ✅ MySQL (
configs/mysql/scripts/init-approle.sh) - AppRole authentication - ✅ Redis Cluster (3 nodes) (
configs/redis/scripts/init-approle.sh) - AppRole authentication - ✅ RabbitMQ (
configs/rabbitmq/scripts/init-approle.sh) - AppRole authentication - ✅ MongoDB (
configs/mongodb/scripts/init-approle.sh) - AppRole authentication - ✅ Forgejo (
configs/forgejo/scripts/init-approle.sh) - AppRole authentication - ✅ Reference API - FastAPI (
reference-apps/fastapi/app/services/vault.py) - AppRole authentication
Security Improvement: Zero root token usage in containers. All services use role-based access control with least-privilege principle.
How It Works (using PostgreSQL as example with AppRole):
-
Container Startup → AppRole wrapper script (
/init/init-approle.sh) executes - Wait for Vault → Script waits for Vault to be unsealed and ready
-
AppRole Authentication → Login to Vault using role-id and secret-id from
/vault-approles/postgres/ - Obtain Service Token → Receive short-lived service token (hvs.CAESIE... prefix)
-
Fetch Credentials & TLS Setting → GET
/v1/secret/data/postgresusing service token (includestls_enabledfield) - Validate Certificates → Check pre-generated certificates exist if TLS enabled
- Configure PostgreSQL → Injects credentials and TLS configuration
- Start Service → PostgreSQL starts with Vault-managed credentials
Wrapper Script (configs/postgres/scripts/init-approle.sh):
#!/bin/bash
# PostgreSQL initialization with Vault integration
# 1. Wait for Vault to be ready
wait_for_vault()
# 2. Fetch credentials AND tls_enabled from Vault
export POSTGRES_USER=$(vault_api | jq -r '.data.data.user')
export POSTGRES_PASSWORD=$(vault_api | jq -r '.data.data.password')
export POSTGRES_DB=$(vault_api | jq -r '.data.data.database')
export ENABLE_TLS=$(vault_api | jq -r '.data.data.tls_enabled // "false"')
# 3. If TLS enabled, validate pre-generated certificates
if [ "$ENABLE_TLS" = "true" ]; then
validate_certificates # Check certs exist in mounted volume
configure_tls # Configure PostgreSQL SSL
fi
# 4. Start PostgreSQL with injected credentials
exec docker-entrypoint.sh postgresFetching PostgreSQL Password:
# Via management script
./devstack vault-show-password postgres
# Via Vault CLI
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=$(cat ~/.config/vault/root-token)
vault kv get -field=password secret/postgres
# Via curl
curl -H "X-Vault-Token: $VAULT_TOKEN" \
http://localhost:8200/v1/secret/data/postgres \
| jq -r '.data.data.password'Environment Variables:
# In .env file
VAULT_ADDR=http://vault:8200
VAULT_TOKEN=hvs.xxxxxxxxxxxxx # From ~/.config/vault/root-token
# NOTE: TLS settings are now in Vault, not .envNote: ALL service passwords and TLS settings have been removed from .env. All credentials and TLS configuration are now managed entirely by Vault.
Overview: The reference-api demonstrates AppRole authentication, HashiCorp's recommended method for applications to authenticate to Vault. This is more secure than using root tokens as it provides:
- Limited, application-specific permissions
- Renewable and revocable tokens
- Audit trail of application authentication
- No root token exposure in environment variables
How It Works:
-
AppRole Credentials Mounted → Credentials stored at
~/.config/vault/approles/reference-api/-
role-id: Identifies the AppRole (similar to username) -
secret-id: Secret credential (similar to password)
-
-
Container Startup → Docker mounts credentials into container
volumes: - ${HOME}/.config/vault/approles/reference-api:/vault-approles/reference-api:ro
-
Application Initialization → VaultClient reads credentials and authenticates
# reference-apps/fastapi/app/services/vault.py def _login_with_approle(self) -> str: # Read role-id and secret-id from filesystem role_id_path = os.path.join(settings.VAULT_APPROLE_DIR, "role-id") secret_id_path = os.path.join(settings.VAULT_APPROLE_DIR, "secret-id") with open(role_id_path, 'r') as f: role_id = f.read().strip() with open(secret_id_path, 'r') as f: secret_id = f.read().strip() # Exchange credentials for Vault token url = urljoin(f"{self.vault_addr}/", "v1/auth/approle/login") payload = {"role_id": role_id, "secret_id": secret_id} response = client.post(url, json=payload, timeout=5.0) data = response.json() client_token = data.get("auth", {}).get("client_token") return client_token # Token has "hvs." prefix (not root token)
-
Fallback Mechanism → If AppRole authentication fails, falls back to token-based auth
def __init__(self): if settings.VAULT_APPROLE_DIR and os.path.exists(settings.VAULT_APPROLE_DIR): try: self.vault_token = self._login_with_approle() logger.info("Successfully authenticated to Vault using AppRole") except Exception as e: logger.warning(f"AppRole authentication failed: {e}, falling back to token-based auth") self.vault_token = settings.VAULT_TOKEN else: logger.info("Using token-based authentication (AppRole directory not found)") self.vault_token = settings.VAULT_TOKEN
Verifying AppRole Authentication:
# 1. Check AppRole credentials exist
ls -la ~/.config/vault/approles/reference-api/
# Should show: role-id, secret-id
# 2. Verify container has no VAULT_TOKEN
docker exec dev-reference-api env | grep VAULT_TOKEN
# Should be empty (no output)
# 3. Check container logs for authentication
docker logs dev-reference-api 2>&1 | grep -i "approle"
# Should show: "Successfully authenticated to Vault using AppRole"
# 4. Verify token type
docker exec dev-reference-api python -c "from app.services.vault import vault_client; print(vault_client.vault_token[:4])"
# Should show: "hvs." (AppRole token, not root token starting with "hvs.")AppRole Setup (for reference):
AppRole credentials are created during ./devstack vault-bootstrap:
# Create AppRole
vault auth enable approle
vault write auth/approle/role/reference-api \
token_policies="reference-api-policy" \
token_ttl=1h \
token_max_ttl=4h
# Generate credentials
vault read -field=role_id auth/approle/role/reference-api/role-id > ~/.config/vault/approles/reference-api/role-id
vault write -f -field=secret_id auth/approle/role/reference-api/secret-id > ~/.config/vault/approles/reference-api/secret-idBenefits:
- Security: Application never has root token
- Audit: All API calls logged with AppRole identity
- Rotation: secret-id can be rotated without code changes
- Least Privilege: Policy limits what secrets application can access
- Production-Ready: Recommended by HashiCorp for application authentication
See Also:
- Reference API documentation:
reference-apps/fastapi/README.md - AppRole implementation:
reference-apps/fastapi/app/services/vault.py - Docker configuration:
docker-compose.yml(line 879)
TLS Implementation: Pre-Generated Certificates with Vault-Based Configuration
The system uses a modern, production-ready TLS architecture where:
- ✅ TLS settings are stored in Vault (not environment variables)
- ✅ Certificates are pre-generated and validated before service startup
- ✅ Runtime enable/disable without container rebuilds
- ✅ All 8 services support TLS (PostgreSQL, MySQL, Redis cluster, RabbitMQ, MongoDB, FastAPI reference app)
- ✅ Dual-mode operation (accepts both SSL and non-SSL connections)
One-Time Certificate Generation:
# 1. Ensure Vault is running and bootstrapped
docker compose up -d vault
sleep 10
# 2. Bootstrap Vault (creates secrets with tls_enabled field)
VAULT_ADDR=http://localhost:8200 \
VAULT_TOKEN=$(cat ~/.config/vault/root-token) \
bash configs/vault/scripts/vault-bootstrap.sh
# 3. Generate all certificates (stored in ~/.config/vault/certs/)
VAULT_ADDR=http://localhost:8200 \
VAULT_TOKEN=$(cat ~/.config/vault/root-token) \
bash scripts/generate-certificates.shEnabling TLS for a Service (Runtime Configuration):
# 1. Set tls_enabled=true in Vault
TOKEN=$(cat ~/.config/vault/root-token)
curl -sf -X POST \
-H "X-Vault-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"data":{"tls_enabled":true}}' \
http://localhost:8200/v1/secret/data/postgres
# 2. Restart the service (picks up new setting)
docker restart dev-postgres
# 3. Verify TLS is enabled
docker logs dev-postgres | grep "tls_enabled"
# Should show: tls_enabled=trueDisabling TLS:
# Set tls_enabled=false and restart
TOKEN=$(cat ~/.config/vault/root-token)
curl -sf -X POST \
-H "X-Vault-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"data":{"tls_enabled":false}}' \
http://localhost:8200/v1/secret/data/postgres
docker restart dev-postgresCertificate Rotation:
# 1. Delete old certificates for a service
rm -rf ~/.config/vault/certs/postgres/
# 2. Regenerate certificates
VAULT_ADDR=http://localhost:8200 \
VAULT_TOKEN=$(cat ~/.config/vault/root-token) \
bash scripts/generate-certificates.sh
# 3. Restart service to pick up new certificates
docker restart dev-postgresCertificate Details:
- Validity: 1 year (8760 hours)
-
Storage:
~/.config/vault/certs/{service}/ - Mount: Read-only bind mounts into containers
- Format: Service-specific (e.g., MySQL uses .pem, MongoDB uses combined cert+key)
Testing TLS Connections:
All services are configured for dual-mode TLS (accepting both encrypted and unencrypted connections).
PostgreSQL:
# Get password from Vault
export PGPASSWORD=$(python3 scripts/read-vault-secret.py postgres password)
# SSL connection (with certificate verification)
psql "postgresql://dev_admin@localhost:5432/dev_database?sslmode=require"
# Non-SSL connection (dual-mode allows this)
psql "postgresql://dev_admin@localhost:5432/dev_database?sslmode=disable"
# Verify SSL is enabled
docker exec dev-postgres psql -U dev_admin -d dev_database -c "SHOW ssl;"MySQL:
# Get password from Vault
MYSQL_PASS=$(export VAULT_TOKEN=$(cat ~/.config/vault/root-token); python3 scripts/read-vault-secret.py mysql password)
# SSL connection
mysql -h localhost -u dev_admin -p$MYSQL_PASS --ssl-mode=REQUIRED dev_database
# Non-SSL connection
mysql -h localhost -u dev_admin -p$MYSQL_PASS --ssl-mode=DISABLED dev_database
# Verify TLS is configured
docker logs dev-mysql | grep "Channel mysql_main configured to support TLS"Redis:
# Get password from Vault
REDIS_PASS=$(export VAULT_TOKEN=$(cat ~/.config/vault/root-token); python3 scripts/read-vault-secret.py redis-1 password)
# SSL connection (TLS port 6380)
redis-cli -h localhost -p 6380 --tls \
--cacert ~/.config/vault/certs/redis-1/ca.crt \
--cert ~/.config/vault/certs/redis-1/redis.crt \
--key ~/.config/vault/certs/redis-1/redis.key \
-a $REDIS_PASS PING
# Non-SSL connection (standard port 6379)
redis-cli -h localhost -p 6379 -a $REDIS_PASS PING
# Verify dual ports
docker logs dev-redis-1 | grep "Ready to accept connections"RabbitMQ:
# SSL port: 5671
# Non-SSL port: 5672 (management UI also available on 15672)
# Test management API
curl -u admin:$(export VAULT_TOKEN=$(cat ~/.config/vault/root-token); python3 scripts/read-vault-secret.py rabbitmq password) \
http://localhost:15672/api/overviewMongoDB:
# Get credentials from Vault
MONGO_USER=$(export VAULT_TOKEN=$(cat ~/.config/vault/root-token); python3 scripts/read-vault-secret.py mongodb user)
MONGO_PASS=$(export VAULT_TOKEN=$(cat ~/.config/vault/root-token); python3 scripts/read-vault-secret.py mongodb password)
# SSL connection (if TLS is enabled)
mongosh "mongodb://$MONGO_USER:$MONGO_PASS@localhost:27017/dev_database?tls=true&tlsCAFile=$HOME/.config/vault/certs/mongodb/ca.pem"
# Non-SSL connection
mongosh "mongodb://$MONGO_USER:$MONGO_PASS@localhost:27017/dev_database"SSL/TLS Modes:
-
PostgreSQL SSL Modes:
-
disable- No SSL -
allow- Try SSL, fallback to plain -
prefer- Prefer SSL, fallback to plain -
require- Require SSL (no cert verification) -
verify-ca- Require SSL + verify CA certificate -
verify-full- Require SSL + verify CA + hostname matching
-
-
MySQL SSL Modes:
-
DISABLED- No SSL -
PREFERRED- Use SSL if available -
REQUIRED- Require SSL -
VERIFY_CA- Verify CA certificate -
VERIFY_IDENTITY- Verify CA + hostname
-
Vault Management Script Commands:
# Initialize Vault (first time only)
./devstack vault-init
# Check Vault status
./devstack vault-status
# Get root token
./devstack vault-token
# Unseal Vault manually (if needed)
./devstack vault-unseal
# Bootstrap PKI and service credentials
./devstack vault-bootstrap
# Export CA certificates
./devstack vault-ca-cert
# Show service password
./devstack vault-show-password postgres
./devstack vault-show-password mysqlVault Bootstrap Process:
The vault-bootstrap command sets up the complete PKI infrastructure:
- Generate Root CA (if not exists)
- Generate Intermediate CA CSR
- Sign Intermediate CA with Root CA
- Install Intermediate CA certificate
- Create PKI roles for each service (postgres-role, mysql-role, etc.)
- Generate and store service credentials (user, password, database)
-
Export CA certificates to
~/.config/vault/ca/
Manual Vault Operations:
# Set environment
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=$(cat ~/.config/vault/root-token)
# List secret paths
vault kv list secret/
# Get PostgreSQL credentials
vault kv get secret/postgres
# Update password (manual rotation)
vault kv put secret/postgres \
user=dev_admin \
password=new_generated_password \
database=dev_database
# Issue certificate manually
vault write pki_int/issue/postgres-role \
common_name=postgres.dev-services.local \
ttl=8760h
# View PKI role configuration
vault read pki_int/roles/postgres-rolePKI Certificate Paths:
Vault PKI Endpoints:
├── /v1/pki/ca/pem # Root CA certificate
├── /v1/pki_int/ca/pem # Intermediate CA certificate
├── /v1/pki_int/roles/postgres-role # PostgreSQL certificate role
├── /v1/pki_int/issue/postgres-role # Issue PostgreSQL certificate
└── /v1/secret/data/postgres # PostgreSQL credentials
Credential Loading for Non-Container Services:
For services that need Vault credentials but aren't containerized (e.g., PgBouncer, Forgejo), credentials are loaded via environment variables:
Script: scripts/load-vault-env.sh
This script loads credentials from Vault into environment variables for docker-compose:
#!/bin/bash
# Load credentials from Vault and export as environment variables
# 1. Wait for Vault to be ready
# 2. Read VAULT_TOKEN from ~/.config/vault/root-token
# 3. Fetch PostgreSQL password: secret/postgres
# 4. Export POSTGRES_PASSWORD for docker-compose
export POSTGRES_PASSWORD=$(python3 scripts/read-vault-secret.py postgres password)Script: scripts/read-vault-secret.py
Python helper to read secrets from Vault KV v2 API:
#!/usr/bin/env python3
# Usage: read-vault-secret.py <path> <field>
# Example: read-vault-secret.py postgres password
import sys, json, urllib.request, os
vault_addr = os.getenv('VAULT_ADDR', 'http://localhost:8200')
vault_token = os.getenv('VAULT_TOKEN')
url = f"{vault_addr}/v1/secret/data/{sys.argv[1]}"
req = urllib.request.Request(url)
req.add_header('X-Vault-Token', vault_token)
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
print(data['data']['data'][sys.argv[2]])When Credentials Are Loaded:
The devstack.py script automatically loads credentials during startup:
- Start Vault container
- Wait 5 seconds for Vault to be ready
- Run
source scripts/load-vault-env.sh - Export credentials as environment variables
- Start remaining services with injected credentials
All Services Are Now Vault-Integrated:
✅ All database services (PostgreSQL, MySQL, MongoDB) ✅ All caching services (Redis Cluster) ✅ All message queue services (RabbitMQ) ✅ All connection pooling services (PgBouncer)
No migration needed - the infrastructure is complete!
Vault runs in two concurrent processes within the container:
Container: dev-vault
├── Process 1: vault server
│ - Listens on 0.0.0.0:8200
│ - Uses file storage: /vault/data
│ - Config: /vault/config/vault.hcl
│
└── Process 2: vault-auto-unseal.sh
- Waits for Vault to be ready
- Unseals using saved keys
- Sleeps indefinitely (no CPU overhead)
Entrypoint (docker-compose.yml:360-366):
entrypoint: >
sh -c "
chown -R vault:vault /vault/data &&
docker-entrypoint.sh server &
/usr/local/bin/vault-auto-unseal.sh &
wait -n
"Process Flow:
- Fix
/vault/datapermissions (chown) - Start Vault server in background (
&) - Start auto-unseal script in background (
&) - Wait for either process to exit (
wait -n)
First-Time Initialization:
./configs/vault/scripts/vault-init.sh
# Or
./devstack vault-initWhat Happens:
- Waits for Vault to be ready (max 30 seconds)
- Checks if already initialized
- If not initialized:
- POSTs to
/v1/sys/initwith{"secret_shares": 5, "secret_threshold": 3} - Receives 5 unseal keys + root token
- Saves to
~/.config/vault/keys.json(chmod 600) - Saves root token to
~/.config/vault/root-token(chmod 600)
- POSTs to
- Unseals Vault using 3 of 5 keys
- Displays status and root token
Shamir Secret Sharing:
- 5 keys generated
- Any 3 keys can unseal Vault
- Designed for distributed trust (give keys to different people/systems)
- Lost keys = cannot unseal (data is encrypted and unrecoverable)
Script (configs/vault/scripts/vault-auto-unseal.sh):
# 1. Wait for Vault API (max 30 attempts, 1s each)
wget --spider http://127.0.0.1:8200/v1/sys/health?uninitcode=200&sealedcode=200
# 2. Check seal status
wget -qO- http://127.0.0.1:8200/v1/sys/seal-status
# → {"sealed": true}
# 3. Read unseal keys from mounted volume
cat /vault-keys/keys.json | extract 3 keys
# 4. POST each key to unseal endpoint
for key in key1 key2 key3; do
wget --post-data='{"key":"'$key'"}' http://127.0.0.1:8200/v1/sys/unseal
done
# 5. Verify unsealed
wget -qO- http://127.0.0.1:8200/v1/sys/seal-status
# → {"sealed": false}
# 6. Sleep indefinitely (no monitoring overhead)
while true; do sleep 3600; doneWhy Not Continuous Monitoring?
- Original design had 10-second checks (360 API calls/hour)
- Optimized to single unseal + sleep
- Saves 99% of API calls and CPU cycles
- Trade-off: Won't auto-reseal if manually sealed (must restart container)
Check Vault Status:
./devstack vault-status
# Or directly
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=$(cat ~/.config/vault/root-token)
vault statusManually Unseal:
./devstack vault-unseal
# Or using vault CLI
vault operator unseal # Repeat 3 times with different keysSeal Vault:
vault operator seal
# Note: Won't auto-reseal until container restartsRotate Root Token:
vault token create -policy=root
# Save new token to ~/.config/vault/root-tokenBackup Unseal Keys:
# Encrypt and backup
tar czf vault-keys-$(date +%Y%m%d).tar.gz ~/.config/vault/
gpg -c vault-keys-*.tar.gz
# Store encrypted file in secure location (1Password, etc.)| Certificate Type | Validity Period | Typical Issue Date | Expiration Date | Renewal Window |
|---|---|---|---|---|
| Root CA (pki) | 10 years | 2025-01-15 | 2035-01-15 | 9 years notice |
| Intermediate CA (pki_int) | 5 years | 2025-01-15 | 2030-01-15 | 4.5 years notice |
| Service Certificates | 1 year | 2025-01-15 | 2026-01-15 | 30 days notice |
Critical: Service certificates must be renewed annually. Set calendar reminders for 30 days before expiration.
Check all service certificates:
# Quick check all services
for service in postgres mysql redis-1 redis-2 redis-3 rabbitmq mongodb; do
echo "=== $service ==="
openssl x509 -in ~/.config/vault/certs/$service/cert.pem -noout -enddate
echo ""
done
# With days until expiry
for service in postgres mysql redis-1 redis-2 redis-3 rabbitmq mongodb; do
EXPIRY=$(openssl x509 -in ~/.config/vault/certs/$service/cert.pem -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s 2>/dev/null || date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_UNTIL=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
echo "$service: $DAYS_UNTIL days until expiry"
doneCheck Root CA:
openssl x509 -in ~/.config/vault/ca/ca.pem -noout -enddate -subjectCheck Intermediate CA:
vault read pki_int/ca/pem | openssl x509 -noout -enddate -subjectWhen to Renew: 30 days before expiration (or sooner)
Automated Renewal Script: scripts/renew-certificates.sh
#!/bin/bash
# Renew all service certificates from Vault PKI
# Run annually or when certificates are approaching expiration
set -e
VAULT_ADDR="${VAULT_ADDR:-http://localhost:8200}"
VAULT_TOKEN="${VAULT_TOKEN:-$(cat ~/.config/vault/root-token)}"
echo "🔄 Renewing service certificates..."
echo "Vault: $VAULT_ADDR"
# Check Vault is unsealed
if ! vault status > /dev/null 2>&1; then
echo "❌ Error: Vault is not accessible or is sealed"
echo "Run: ./devstack vault-unseal"
exit 1
fi
# Backup existing certificates
echo "📦 Backing up existing certificates..."
BACKUP_DIR=~/.config/vault/certs-backup-$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
cp -r ~/.config/vault/certs/* "$BACKUP_DIR/"
echo " Backup created: $BACKUP_DIR"
# Generate new certificates
echo "🔐 Generating new certificates..."
export VAULT_ADDR VAULT_TOKEN
./scripts/generate-certificates.sh
# Verify new certificates
echo "✅ Verifying new certificates..."
for service in postgres mysql redis-1 redis-2 redis-3 rabbitmq mongodb; do
if [ -f ~/.config/vault/certs/$service/cert.pem ]; then
EXPIRY=$(openssl x509 -in ~/.config/vault/certs/$service/cert.pem -noout -enddate | cut -d= -f2)
echo " $service: Valid until $EXPIRY"
fi
done
echo ""
echo "🔄 Restarting services to load new certificates..."
./devstack restart
echo ""
echo "✅ Certificate renewal complete!"
echo " Old certificates backed up to: $BACKUP_DIR"
echo " New certificates expire in ~365 days"
echo ""
echo "📅 Set reminder to renew again in 11 months"Make executable and run:
chmod +x scripts/renew-certificates.sh
./scripts/renew-certificates.shManual Renewal (if script fails):
# 1. Set environment
export VAULT_TOKEN=$(cat ~/.config/vault/root-token)
export VAULT_ADDR=http://localhost:8200
# 2. Backup existing certificates
cp -r ~/.config/vault/certs ~/.config/vault/certs-backup-$(date +%Y%m%d)
# 3. Regenerate certificates
./scripts/generate-certificates.sh
# 4. Restart services
./devstack restart
# 5. Verify new certificates
for service in postgres mysql redis-1; do
openssl x509 -in ~/.config/vault/certs/$service/cert.pem -noout -dates
doneWhen to Renew: 60 days before expiration (5-year cert, so plan at 4 years 10 months)
Renewal Procedure:
# 1. Set environment
export VAULT_TOKEN=$(cat ~/.config/vault/root-token)
export VAULT_ADDR=http://localhost:8200
# 2. Check current intermediate CA expiration
vault read pki_int/ca/pem | openssl x509 -noout -enddate
# 3. Generate new intermediate CSR
vault write -format=json pki_int/intermediate/generate/internal \
common_name="DevStack Core Intermediate CA v2" \
ttl="43800h" \
key_type="rsa" \
key_bits="4096" > pki_int_csr_v2.json
CSR=$(jq -r '.data.csr' < pki_int_csr_v2.json)
# 4. Sign with Root CA
vault write -format=json pki/root/sign-intermediate \
csr="$CSR" \
format=pem_bundle \
ttl="43800h" > pki_int_cert_v2.json
CERT=$(jq -r '.data.certificate' < pki_int_cert_v2.json)
# 5. Import signed certificate
vault write pki_int/intermediate/set-signed certificate="$CERT"
# 6. Verify new intermediate CA
vault read pki_int/ca/pem | openssl x509 -noout -text | grep "Not After"
# 7. Regenerate ALL service certificates
./scripts/generate-certificates.sh
# 8. Restart all services
./devstack restart
# 9. Verify everything works
./devstack healthPost-Renewal Verification:
# Check Intermediate CA is active
vault list pki_int/roles
# Test certificate issuance
vault write pki_int/issue/postgres-role \
common_name=test.postgres.local \
ttl=1h
# Verify service health
./devstack healthWhen to Renew: 6-12 months before expiration (10-year cert, plan at 9 years)
- Coordinated rollover period (dual root CA trust)
- New intermediate CA must be issued
- All service certificates must be regenerated
- Distribution of new root CA to all clients
- Testing in non-production environment first
DO NOT attempt Root CA renewal without:
- Full backup of current PKI
- Test environment validation
- Communication plan for dependent systems
- Rollback procedure
Recommendation: Create a dedicated ROOT_CA_RENEWAL.md runbook 6 months before expiration with detailed step-by-step procedures specific to your environment.
Emergency Root CA Renewal (if Root CA is compromised):
See DISASTER_RECOVERY.md - "Vault Data Loss" section.
Daily Cron Job:
# Add to crontab: crontab -e
# Check certificate expiration daily at 9 AM
0 9 * * * /Users/gator/devstack-core/scripts/check-cert-expiry.sh 2>&1 | mail -s "Certificate Expiry Report" admin@example.comMonitoring Script: scripts/check-cert-expiry.sh
#!/bin/bash
# Check certificate expiration and alert if approaching expiry
WARN_DAYS=30
CRIT_DAYS=7
FOUND_WARNING=0
FOUND_CRITICAL=0
echo "Certificate Expiration Report - $(date)"
echo "==========================================="
echo ""
# Check service certificates
echo "Service Certificates:"
for cert_path in ~/.config/vault/certs/*/cert.pem; do
if [ ! -f "$cert_path" ]; then
continue
fi
service=$(basename $(dirname "$cert_path"))
expiry=$(openssl x509 -in "$cert_path" -noout -enddate | cut -d= -f2)
# Calculate days until expiry (macOS compatible)
if date -j -f "%b %d %T %Y %Z" "$expiry" +%s > /dev/null 2>&1; then
expiry_epoch=$(date -j -f "%b %d %T %Y %Z" "$expiry" +%s)
else
expiry_epoch=$(date -d "$expiry" +%s)
fi
now_epoch=$(date +%s)
days_until=$(( ($expiry_epoch - $now_epoch) / 86400 ))
if [ $days_until -lt $CRIT_DAYS ]; then
echo "❌ CRITICAL: $service certificate expires in $days_until days ($expiry)"
FOUND_CRITICAL=1
elif [ $days_until -lt $WARN_DAYS ]; then
echo "⚠️ WARNING: $service certificate expires in $days_until days ($expiry)"
FOUND_WARNING=1
else
echo "✅ OK: $service certificate valid for $days_until days"
fi
done
echo ""
echo "Root and Intermediate CAs:"
# Check Root CA
if [ -f ~/.config/vault/ca/ca.pem ]; then
root_expiry=$(openssl x509 -in ~/.config/vault/ca/ca.pem -noout -enddate | cut -d= -f2)
echo " Root CA expires: $root_expiry"
fi
# Check Intermediate CA
if command -v vault > /dev/null 2>&1; then
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=$(cat ~/.config/vault/root-token 2>/dev/null)
if [ -n "$VAULT_TOKEN" ]; then
int_expiry=$(vault read pki_int/ca/pem 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null)
if [ -n "$int_expiry" ]; then
echo " Intermediate CA expires: $int_expiry"
fi
fi
fi
echo ""
echo "==========================================="
# Exit codes for monitoring systems
if [ $FOUND_CRITICAL -eq 1 ]; then
exit 2
elif [ $FOUND_WARNING -eq 1 ]; then
exit 1
else
exit 0
fiMake executable:
chmod +x scripts/check-cert-expiry.sh
# Test run
./scripts/check-cert-expiry.shWhen to Revoke: Certificate compromise, key exposure, or service decommissioning
Revocation Procedure:
# 1. Get certificate serial number
openssl x509 -in ~/.config/vault/certs/postgres/cert.pem -noout -serial
# Example output: serial=3A:5F:2B:...
# 2. Revoke certificate
vault write pki_int/revoke serial_number="3A:5F:2B:..."
# 3. Generate new certificate immediately
vault write pki_int/issue/postgres-role \
common_name="postgres" \
ttl="8760h" \
format=pem_bundle > postgres_new.json
# 4. Extract and install new certificate
jq -r '.data.certificate' < postgres_new.json > ~/.config/vault/certs/postgres/cert.pem
jq -r '.data.private_key' < postgres_new.json > ~/.config/vault/certs/postgres/key.pem
jq -r '.data.ca_chain[]' < postgres_new.json > ~/.config/vault/certs/postgres/ca.pem
# 5. Restart affected service
docker compose restart postgres
# 6. Update Certificate Revocation List (CRL)
vault read pki_int/crl-
Calendar Reminders:
- Root CA: Set reminder 9 years from issue date
- Intermediate CA: Set reminder 4 years 6 months from issue date
- Service Certificates: Set reminders every 11 months
-
Automate Renewal:
-
Service certificates: Run
renew-certificates.shmonthly (automated) - Intermediate CA: Manual process with advance planning
- Root CA: Treat as major project, plan 6-12 months ahead
-
Service certificates: Run
-
Test Renewal Process:
- Practice renewal in test environment quarterly
- Document any issues encountered
- Update renewal scripts based on lessons learned
-
Monitor Continuously:
- Daily automated expiration checks (
check-cert-expiry.sh) - Alert if certificates < 30 days from expiry
- Escalate if certificates < 7 days from expiry
- Daily automated expiration checks (
-
Backup Before Renewal:
- Always backup
~/.config/vault/before renewal - Store backups securely off-system
- Test restoration from backup
- Always backup
-
Verify After Renewal:
- Check certificate dates with
openssl x509 - Verify services restart successfully
- Run health checks:
./devstack health - Test TLS connections to services
- Check certificate dates with
Problem: Service won't start after certificate renewal
Solution:
# Check certificate permissions
ls -la ~/.config/vault/certs/postgres/
# Should be readable (644 for certs, 600 for keys)
chmod 644 ~/.config/vault/certs/postgres/cert.pem
chmod 644 ~/.config/vault/certs/postgres/ca.pem
chmod 600 ~/.config/vault/certs/postgres/key.pem
# Check certificate validity
openssl verify -CAfile ~/.config/vault/certs/postgres/ca.pem \
~/.config/vault/certs/postgres/cert.pem
# Check service logs
docker compose logs postgres | grep -i tlsProblem: Certificate verification failed
Solution:
# Verify certificate chain
openssl verify -verbose -CAfile ~/.config/vault/ca/ca.pem \
-untrusted ~/.config/vault/ca/ca-chain.pem \
~/.config/vault/certs/postgres/cert.pem
# If chain is broken, regenerate
./scripts/generate-certificates.shProblem: Vault won't issue new certificates
Solution:
# Check Vault PKI role exists
vault list pki_int/roles
# If missing, re-run vault-bootstrap
./devstack vault-bootstrap
# Check intermediate CA is configured
vault read pki_int/cert/ca30 Days Before Expiry:
- Run
check-cert-expiry.shto confirm expiry dates - Schedule maintenance window (service restart required)
- Notify stakeholders of planned renewal
- Backup current certificates
- Test renewal script in non-production environment
Renewal Day:
- Verify Vault is healthy and unsealed
- Backup
~/.config/vault/directory - Run
renew-certificates.shscript - Verify new certificate dates
- Restart services:
./devstack restart - Verify services healthy:
./devstack health - Test TLS connections
- Monitor service logs for TLS errors
Post-Renewal (24 hours later):
- Confirm no TLS errors in logs
- Verify applications connecting successfully
- Update renewal tracking spreadsheet
- Set reminder for next renewal (11 months)
- Document any issues encountered