Local Secret Vaults & Rotation
Run a local secret vault with rotation that mirrors production behavior. Inject ephemeral credentials, renew leases, and validate parity without leaking secrets.
Eliminating environment drift requires secret management that mirrors production behavior without exposing sensitive material. This guide gives platform engineers a tactical path to provision, inject, rotate, and validate local secrets, working within the environment sync and CI parity baseline. If you are weighing tooling first, compare the options in Vault vs dotenv-vault vs SOPS for local secrets; if rotation is forcing container restarts, see rotating secrets without restarting containers.
Prerequisites
- Docker Engine 24+ with the Compose v2 plugin.
- The
vaultCLI andjqon PATH. direnvinstalled for shell-scoped credential injection.
Provision a Local Vault Instance
A lightweight HashiCorp Vault dev server gives you a single source of truth for development credentials, bound to the loopback interface.
- Run Vault in dev mode via Compose, mapped to
127.0.0.1:8200. - Bootstrap the root token and enable the KV v2 engine for versioned audit trails.
- Keep
VAULT_ADDRconsistent everywhere it appears.
# docker-compose.yml
services:
vault:
image: hashicorp/vault:1.15
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: "local-dev-token"
VAULT_ADDR: "http://127.0.0.1:8200"
cap_add:
- IPC_LOCK
volumes:
- ./vault-data:/vault/file
networks:
- vault-net
networks:
vault-net:
driver: bridge
Drift check — validate init state and VAULT_ADDR consistency:
#!/usr/bin/env bash
set -euo pipefail
vault status -format=json | jq '.initialized' # expect: true
grep -rn "VAULT_ADDR" .env* docker-compose.yml || true
echo "Vault provisioning check complete"
WSL2: The NAT layer can intercept
127.0.0.1. If host resolution fails, bind to0.0.0.0in the container but restrict access via Docker network policies. macOS (Docker Desktop): Raise allocated memory to 4GB+ to avoid OOM kills during KV v2 init. Apple Silicon (ARM64): Use thehashicorp/vault:1.15multi-arch manifest. If the host kernel rejectsIPC_LOCK, removecap_addand rely on Docker's default memory locking.
Inject Dynamic Secrets into Dev Containers
Static .env files cause drift and exposure. Mount the Vault CLI and token into the workspace and fetch short-lived credentials on directory entry with direnv. The static-fallback resolution rules are in dotenv and configuration management.
// .devcontainer/devcontainer.json
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"remoteEnv": {
"DB_PASSWORD": "${localEnv:VAULT_DB_PASS}"
},
"postCreateCommand": "command -v vault && vault kv get -field=password secret/dev/db > /tmp/.db_pass",
"features": {
"ghcr.io/devcontainers/features/vault:1": {}
}
}
Drift check — confirm injected variables resolve inside the container:
#!/usr/bin/env bash
set -euo pipefail
direnv export json | jq 'keys'
docker inspect "$(docker ps -q -f label=devcontainer.local_folder)" \
--format='{{json .Config.Env}}' | jq .
echo "Injection check complete"
WSL2: Store
.vault-tokenand.envrcinside native ext4 (/home/user/...);/mnt/cI/O latency causes direnv polling delays. macOS (Docker Desktop): Volume propagation can runpostCreateCommandbefore the CLI is mounted — gate it withcommand -v vaultas above.
Renew Leases on a Background Daemon
Local development needs predictable credential lifecycles. Restrict the dev role to read and renew, then run a renewal loop with exponential backoff and a fallback that prevents workspace lockout. Keep decrypted material off disk per managing local secrets without committing to git.
#!/usr/bin/env bash
# scripts/rotate-local-secrets.sh
set -euo pipefail
LEASE_PATH="secret/dev/api-key"
BACKOFF=10
MAX_BACKOFF=300
while true; do
if ! vault lease renew -increment=3600 "${LEASE_PATH}" 2>/dev/null; then
echo "Lease expired or unreachable; refreshing via kv get..."
vault kv get -format=json "${LEASE_PATH}" | jq -r '.data.data.value' > .env.local
BACKOFF=10
fi
sleep "${BACKOFF}"
BACKOFF=$(( BACKOFF * 2 > MAX_BACKOFF ? MAX_BACKOFF : BACKOFF * 2 ))
done
Drift check — alert when TTL drops below the rotation threshold:
#!/usr/bin/env bash
set -euo pipefail
TTL="$(vault lease lookup secret/dev/api-key -format=json | jq '.data.ttl')"
if [ "${TTL}" -lt 300 ]; then
echo "CRITICAL: lease TTL ${TTL}s below 300s threshold"
fi
echo "Lease TTL check complete"
WSL2: Background
cron/systemdmay not survive restarts. Usenohupwith PID tracking, or trigger the script via Windows Task Scheduler on login. Apple Silicon (ARM64): Ensure the devcontainer base image matches the host architecture; mismatched binaries fail duringpostCreateCommand.
Validate Secret Parity Against CI
Secret parity must be enforced programmatically. Export the local KV tree, normalize casing, and diff it against the CI-managed manifest. Wire the result into CI/CD pipeline parity checks so merges block on mismatch.
# Makefile
.PHONY: validate-secrets-parity
validate-secrets-parity:
@vault kv list -format=yaml secret/dev/ | yq eval '.[]' - | sort > /tmp/local_keys.txt
@curl -s "$(CI_SECRET_MANIFEST_URL)" | yq eval '.required_keys[]' - | sort > /tmp/ci_keys.txt
@diff -u /tmp/ci_keys.txt /tmp/local_keys.txt && echo "PARITY OK" || (echo "DRIFT DETECTED"; exit 1)
Drift check — run the parity target and act on the exit code:
#!/usr/bin/env bash
set -euo pipefail
make validate-secrets-parity
echo "Secret parity verified"
WSL2:
CRLFinMakefiletargets breaks execution. Rundos2unix Makefileor setcore.autocrlf=input. Apple Silicon (ARM64): Runyqviadocker run --rm -v "$(pwd):/work" mikefarah/yqfor consistent cross-platform behavior.
Rollback / recovery
If the renewal daemon corrupts .env.local or a lease is revoked mid-session, stop the daemon and re-fetch from Vault:
#!/usr/bin/env bash
set -euo pipefail
pkill -f rotate-local-secrets.sh || true
vault kv get -format=json secret/dev/api-key | jq -r '.data.data.value' > .env.local
echo "Local secret restored from Vault"