generated from coulomb/repo-seed
- .sops.yaml + keys/age.pub: SOPS age encryption for all secrets/ paths - .gitignore: broad secrets/ catch-all (any depth) - .githooks/pre-commit: blocks unencrypted secrets/, *.env outside bootstrap/, and known plaintext patterns (PI_SECRET_KEY=, LLDAP_JWT_SECRET=, etc.) - Makefile: full credential lifecycle (creds-init/generate/bundle/apply/verify/ status/rotate) + SOPS helpers (sops-setup/edit/encrypt/decrypt/rotate/check-secrets) + hooks/hooks-test - creds-apply.sh: runs create-secrets.sh in dependency order (postgresql → lldap → authelia → privacyidea), skips keycape with printed instructions, updates state - creds-verify.sh: checks all K8s secrets exist, updates creds-state.yaml - creds-status.sh: human-readable state table from creds-state.yaml - creds-rotate.sh: guided rotation for all 9 secret types with impact descriptions and atomic multi-component update sequences - creds-state.yaml: committable state file tracking generation, bundle, KeePassXC confirmation, per-component apply status, enckey and pi-admin bootstrap flags NK-WP-0003-T01 unblocked. /creds-bootstrap skill registered separately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
297 lines
12 KiB
Bash
Executable File
297 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# creds-rotate.sh — guided rotation for a single net-kingdom credential.
|
|
#
|
|
# Usage:
|
|
# SECRET=<name> bash sso-mfa/bootstrap/creds-rotate.sh [secrets-dir]
|
|
# make creds-rotate SECRET=<name>
|
|
#
|
|
# The script:
|
|
# 1. Validates the secret name
|
|
# 2. Prints rotation impact and required coordination steps
|
|
# 3. Generates a new value (same entropy as original)
|
|
# 4. Guides through the atomic update sequence
|
|
# 5. After confirmation, updates creds-state.yaml and reminds to re-bundle
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
SECRETS_DIR="${1:-$SCRIPT_DIR/secrets}"
|
|
STATE_FILE="$SCRIPT_DIR/creds-state.yaml"
|
|
K8S_DIR="$REPO_ROOT/sso-mfa/k8s"
|
|
|
|
SECRET="${SECRET:-}"
|
|
|
|
rnd_hex() { openssl rand -hex "$1"; }
|
|
rnd_b64() { openssl rand -base64 "$1" | tr -d '\n/+=' | head -c "$2"; }
|
|
|
|
confirm() {
|
|
local prompt="${1:-Continue?}"
|
|
echo ""
|
|
read -rp "$prompt [y/N] " ans
|
|
[[ "${ans,,}" == "y" ]]
|
|
}
|
|
|
|
header() {
|
|
echo ""
|
|
echo "════════════════════════════════════════════════════════"
|
|
echo " Rotating: $SECRET"
|
|
echo "════════════════════════════════════════════════════════"
|
|
}
|
|
|
|
post_rotation_reminder() {
|
|
echo ""
|
|
echo "Post-rotation checklist:"
|
|
echo " ✓ Update KeePassXC entry for this secret"
|
|
echo " ✓ Run: make creds-bundle (refresh offsite backup)"
|
|
echo " ✓ Run: make creds-verify (confirm cluster state)"
|
|
}
|
|
|
|
# ── Dispatch ──────────────────────────────────────────────────────────────────
|
|
case "$SECRET" in
|
|
|
|
PI_SECRET_KEY)
|
|
header
|
|
echo ""
|
|
echo "Impact: Flask/privacyIDEA app secret — rotates all active PI sessions."
|
|
echo " All privacyIDEA users will be logged out."
|
|
echo ""
|
|
echo "New value:"
|
|
NEW_VAL="$(rnd_hex 32)"
|
|
echo " PI_SECRET_KEY=$NEW_VAL"
|
|
confirm "Apply rotation?" || exit 0
|
|
echo ""
|
|
ENV_FILE="$SECRETS_DIR/privacyidea/secrets.env"
|
|
[[ -f "$ENV_FILE" ]] && sed -i "s|^PI_SECRET_KEY=.*|PI_SECRET_KEY=$NEW_VAL|" "$ENV_FILE"
|
|
echo " 1. Updating K8s Secret privacyidea-config (namespace: mfa)..."
|
|
(cd "$K8S_DIR/privacyidea" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo " 2. Restarting privacyIDEA pod..."
|
|
kubectl rollout restart deployment privacyidea -n mfa
|
|
kubectl rollout status deployment privacyidea -n mfa
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
PI_PEPPER)
|
|
header
|
|
echo ""
|
|
echo " ⚠ WARNING: PI_PEPPER CANNOT BE ROTATED without re-hashing all privacyIDEA"
|
|
echo " user passwords. This is a major destructive operation."
|
|
echo ""
|
|
echo " Treat PI_PEPPER as permanent. If it is compromised:"
|
|
echo " 1. Rotate all user credentials in privacyIDEA"
|
|
echo " 2. Re-enroll all TOTP tokens"
|
|
echo " 3. Contact affected users"
|
|
echo ""
|
|
echo " This script will NOT automate PI_PEPPER rotation."
|
|
exit 1
|
|
;;
|
|
|
|
PI_DB_PASSWORD)
|
|
header
|
|
echo ""
|
|
echo "Impact: privacyIDEA database password — must be updated atomically in:"
|
|
echo " 1. PostgreSQL (CNPG) — ALTER USER privacyidea WITH PASSWORD '...';"
|
|
echo " 2. K8s Secret privacyidea-config (PI_SQLALCHEMY_DATABASE_URI)"
|
|
echo " Privacyidea pod must be restarted after both are updated."
|
|
echo ""
|
|
echo "New value:"
|
|
NEW_VAL="$(rnd_b64 32 40)"
|
|
echo " PI_DB_PASSWORD=$NEW_VAL"
|
|
confirm "Apply rotation?" || exit 0
|
|
echo ""
|
|
ENV_FILE="$SECRETS_DIR/privacyidea/secrets.env"
|
|
[[ -f "$ENV_FILE" ]] && sed -i "s|^PI_DB_PASSWORD=.*|PI_DB_PASSWORD=$NEW_VAL|" "$ENV_FILE"
|
|
echo " 1. Updating PostgreSQL password..."
|
|
PG_POD=$(kubectl get pod -n databases -l postgresql=net-kingdom-pg -o name 2>/dev/null | head -1)
|
|
if [[ -n "$PG_POD" ]]; then
|
|
kubectl exec -n databases "$PG_POD" -- \
|
|
psql -U postgres -c "ALTER USER privacyidea WITH PASSWORD '$NEW_VAL';"
|
|
echo " ✔ PostgreSQL password updated"
|
|
else
|
|
echo " WARN: Could not find PostgreSQL pod — update manually:"
|
|
echo " kubectl exec -n databases <pg-pod> -- psql -U postgres -c \"ALTER USER privacyidea WITH PASSWORD '$NEW_VAL';\""
|
|
fi
|
|
echo ""
|
|
echo " 2. Updating K8s Secret privacyidea-config..."
|
|
(cd "$K8S_DIR/privacyidea" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo ""
|
|
echo " 3. Restarting privacyIDEA pod..."
|
|
kubectl rollout restart deployment privacyidea -n mfa
|
|
kubectl rollout status deployment privacyidea -n mfa
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
LLDAP_JWT_SECRET)
|
|
header
|
|
echo ""
|
|
echo "Impact: LLDAP JWT signing key — all LLDAP sessions will be invalidated."
|
|
echo ""
|
|
echo "New value:"
|
|
NEW_VAL="$(rnd_hex 32)"
|
|
echo " LLDAP_JWT_SECRET=$NEW_VAL"
|
|
confirm "Apply rotation?" || exit 0
|
|
ENV_FILE="$SECRETS_DIR/lldap/secrets.env"
|
|
[[ -f "$ENV_FILE" ]] && sed -i "s|^LLDAP_JWT_SECRET=.*|LLDAP_JWT_SECRET=$NEW_VAL|" "$ENV_FILE"
|
|
echo " Updating K8s Secret lldap-secrets (namespace: sso)..."
|
|
(cd "$K8S_DIR/lldap" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo " Restarting LLDAP pod..."
|
|
kubectl rollout restart deployment lldap -n sso
|
|
kubectl rollout status deployment lldap -n sso
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
LLDAP_LDAP_USER_PASS)
|
|
header
|
|
echo ""
|
|
echo "Impact: LLDAP admin + LDAP bind password — used by Authelia and KeyCape."
|
|
echo " All three must be updated atomically:"
|
|
echo " 1. LLDAP admin password (via LLDAP web UI or API)"
|
|
echo " 2. Authelia K8s Secret (ldap_password)"
|
|
echo " 3. KeyCape K8s Secret (config.yaml lldap.bindPW)"
|
|
echo " Restart all three pods after updating."
|
|
echo ""
|
|
echo "New value:"
|
|
NEW_VAL="$(rnd_b64 32 40)"
|
|
echo " LLDAP_LDAP_USER_PASS=$NEW_VAL"
|
|
confirm "Apply rotation?" || exit 0
|
|
ENV_FILE="$SECRETS_DIR/lldap/secrets.env"
|
|
[[ -f "$ENV_FILE" ]] && sed -i "s|^LLDAP_LDAP_USER_PASS=.*|LLDAP_LDAP_USER_PASS=$NEW_VAL|" "$ENV_FILE"
|
|
echo " 1. Updating LLDAP admin password via API..."
|
|
echo " WARN: Automated LLDAP password update not implemented."
|
|
echo " Log in to https://lldap.coulomb.social and change the admin password manually."
|
|
confirm "Confirm you have updated the LLDAP admin password?" || { echo "Aborting."; exit 1; }
|
|
echo " 2. Updating Authelia secrets..."
|
|
(cd "$K8S_DIR/authelia" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo " 3. Updating KeyCape secrets..."
|
|
(cd "$K8S_DIR/keycape" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo " 4. Restarting Authelia and KeyCape pods..."
|
|
kubectl rollout restart deployment authelia -n sso
|
|
kubectl rollout restart deployment keycape -n sso
|
|
kubectl rollout status deployment authelia -n sso
|
|
kubectl rollout status deployment keycape -n sso
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
AUTHELIA_SESSION_SECRET)
|
|
header
|
|
echo ""
|
|
echo "Impact: Authelia session cookie encryption key."
|
|
echo " All active Authelia sessions will be invalidated (users re-prompted)."
|
|
echo ""
|
|
echo "New value:"
|
|
NEW_VAL="$(rnd_hex 32)"
|
|
echo " AUTHELIA_SESSION_SECRET=$NEW_VAL"
|
|
confirm "Apply rotation?" || exit 0
|
|
ENV_FILE="$SECRETS_DIR/authelia/secrets.env"
|
|
[[ -f "$ENV_FILE" ]] && sed -i "s|^AUTHELIA_SESSION_SECRET=.*|AUTHELIA_SESSION_SECRET=$NEW_VAL|" "$ENV_FILE"
|
|
(cd "$K8S_DIR/authelia" && bash create-secrets.sh "$SECRETS_DIR")
|
|
kubectl rollout restart deployment authelia -n sso
|
|
kubectl rollout status deployment authelia -n sso
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
AUTHELIA_KEYCAPE_CLIENT_SECRET)
|
|
header
|
|
echo ""
|
|
echo "Impact: Authelia↔KeyCape OIDC client secret."
|
|
echo " Must be updated atomically in:"
|
|
echo " 1. Authelia K8s Secret (bcrypt hash of the new plaintext)"
|
|
echo " 2. KeyCape K8s Secret (plaintext, stored as authelia.clientSecret)"
|
|
echo " Both pods must be restarted before the new secret takes effect."
|
|
echo ""
|
|
echo "New value:"
|
|
NEW_VAL="$(rnd_b64 32 40)"
|
|
echo " AUTHELIA_KEYCAPE_CLIENT_SECRET=$NEW_VAL"
|
|
confirm "Apply rotation?" || exit 0
|
|
ENV_FILE="$SECRETS_DIR/authelia/secrets.env"
|
|
[[ -f "$ENV_FILE" ]] && sed -i "s|^AUTHELIA_KEYCAPE_CLIENT_SECRET=.*|AUTHELIA_KEYCAPE_CLIENT_SECRET=$NEW_VAL|" "$ENV_FILE"
|
|
echo " Updating Authelia secrets (bcrypt hash)..."
|
|
(cd "$K8S_DIR/authelia" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo " Updating KeyCape secrets (plaintext)..."
|
|
(cd "$K8S_DIR/keycape" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo " Restarting Authelia and KeyCape..."
|
|
kubectl rollout restart deployment authelia -n sso
|
|
kubectl rollout restart deployment keycape -n sso
|
|
kubectl rollout status deployment authelia -n sso
|
|
kubectl rollout status deployment keycape -n sso
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
KEYCAPE_RSA_KEY)
|
|
header
|
|
echo ""
|
|
echo " ⚠ WARNING: Rotating the KeyCape RSA signing key immediately invalidates"
|
|
echo " ALL issued JWT tokens. This causes a brief authentication outage."
|
|
echo " All downstream applications will reject existing tokens until they"
|
|
echo " refresh against the new JWKS endpoint."
|
|
echo ""
|
|
echo " Coordination required:"
|
|
echo " 1. Notify downstream application owners of the planned outage window."
|
|
echo " 2. Delete secrets/keycape/key.pem to trigger regeneration."
|
|
echo " 3. Re-run keycape/create-secrets.sh."
|
|
echo " 4. Restart KeyCape pod."
|
|
echo " 5. Confirm downstream apps recover (they should auto-refresh JWKS)."
|
|
echo ""
|
|
confirm "Proceed with RSA key rotation? (causes auth outage)" || exit 0
|
|
KEY_FILE="$SECRETS_DIR/keycape/key.pem"
|
|
if [[ -f "$KEY_FILE" ]]; then
|
|
shred -u "$KEY_FILE"
|
|
echo " Old key shredded."
|
|
fi
|
|
echo " Regenerating RSA-2048 signing key..."
|
|
mkdir -p "$(dirname "$KEY_FILE")"
|
|
openssl genrsa -out "$KEY_FILE" 2048 2>/dev/null
|
|
chmod 600 "$KEY_FILE"
|
|
echo " Updating KeyCape secrets..."
|
|
(cd "$K8S_DIR/keycape" && bash create-secrets.sh "$SECRETS_DIR")
|
|
echo " Restarting KeyCape pod..."
|
|
kubectl rollout restart deployment keycape -n sso
|
|
kubectl rollout status deployment keycape -n sso
|
|
echo ""
|
|
echo " ✔ RSA key rotated. Store the new key in KeePassXC: net-kingdom/KeyCape/jwt-signing-key"
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
BREAKGLASS_PASSWORD)
|
|
header
|
|
echo ""
|
|
echo "Impact: Break-glass emergency access password. Low blast radius."
|
|
echo " Rotate freely — no downstream dependencies."
|
|
echo ""
|
|
echo "New value:"
|
|
NEW_VAL="$(rnd_b64 32 40)"
|
|
echo " BREAKGLASS_PASSWORD=$NEW_VAL"
|
|
confirm "Apply rotation?" || exit 0
|
|
ENV_FILE="$SECRETS_DIR/breakglass/secrets.env"
|
|
[[ -f "$ENV_FILE" ]] && sed -i "s|^BREAKGLASS_PASSWORD=.*|BREAKGLASS_PASSWORD=$NEW_VAL|" "$ENV_FILE"
|
|
echo " Updating break-glass K8s Secret (namespace: sso)..."
|
|
BG_SECRET="break-glass"
|
|
kubectl create secret generic "$BG_SECRET" \
|
|
--namespace=sso \
|
|
--from-literal=BREAKGLASS_PASSWORD="$NEW_VAL" \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
echo ""
|
|
echo " Update KeePassXC entry: net-kingdom/Break-glass/break-glass"
|
|
post_rotation_reminder
|
|
;;
|
|
|
|
*)
|
|
echo "ERROR: Unknown secret: '$SECRET'"
|
|
echo ""
|
|
echo "Known secrets:"
|
|
echo " PI_SECRET_KEY Flask/PI app secret (session rotation)"
|
|
echo " PI_PEPPER Password hashing pepper (PERMANENT — read help)"
|
|
echo " PI_DB_PASSWORD privacyIDEA database password"
|
|
echo " LLDAP_JWT_SECRET LLDAP JWT signing key"
|
|
echo " LLDAP_LDAP_USER_PASS LLDAP admin + LDAP bind password (3-way coordinated)"
|
|
echo " AUTHELIA_SESSION_SECRET Authelia session cookie key"
|
|
echo " AUTHELIA_KEYCAPE_CLIENT_SECRET Authelia↔KeyCape OIDC client secret"
|
|
echo " KEYCAPE_RSA_KEY KeyCape JWT signing key (causes auth outage)"
|
|
echo " BREAKGLASS_PASSWORD Break-glass emergency password"
|
|
echo ""
|
|
echo "Usage: make creds-rotate SECRET=<name>"
|
|
exit 1
|
|
;;
|
|
|
|
esac
|