Repository navigation
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