#!/usr/bin/env bash # creds-rotate.sh — guided or non-interactive rotation for a single net-kingdom credential. # # Usage: # SECRET= bash sso-mfa/bootstrap/creds-rotate.sh [secrets-dir] # make creds-rotate SECRET= # # # Agent / non-interactive mode (NK-WP-0005): # SECRET= bash sso-mfa/bootstrap/creds-rotate.sh --non-interactive [secrets-dir] # # 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 (interactive) or runs it # automatically (--non-interactive) # 5. After completion, updates creds-state.yaml set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" STATE_FILE="$SCRIPT_DIR/creds-state.yaml" K8S_DIR="$REPO_ROOT/sso-mfa/k8s" NON_INTERACTIVE=false POSITIONAL_ARGS=() for arg in "$@"; do if [[ "$arg" == "--non-interactive" ]]; then NON_INTERACTIVE=true else POSITIONAL_ARGS+=("$arg") fi done SECRETS_DIR="${POSITIONAL_ARGS[0]:-$SCRIPT_DIR/secrets}" 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?}" if [[ "$NON_INTERACTIVE" == true ]]; then echo " [non-interactive] auto-confirming: $prompt" return 0 fi echo "" read -rp "$prompt [y/N] " ans [[ "${ans,,}" == "y" ]] } header() { echo "" echo "════════════════════════════════════════════════════════" echo " Rotating: $SECRET$([ "$NON_INTERACTIVE" = true ] && echo " (non-interactive)" || true)" echo "════════════════════════════════════════════════════════" } update_last_rotated() { local key="$1" if [[ -f "$STATE_FILE" ]]; then # Update or append last_rotated_ in creds-state.yaml local ts ts="$(date -Iseconds)" if grep -qE "^last_rotated_${key}:" "$STATE_FILE"; then sed -i "s|^last_rotated_${key}: .*|last_rotated_${key}: \"${ts}\"|" "$STATE_FILE" else echo "last_rotated_${key}: \"${ts}\"" >> "$STATE_FILE" fi echo " [state] last_rotated_${key} → ${ts}" fi } post_rotation_reminder() { echo "" if [[ "$NON_INTERACTIVE" == false ]]; then echo "Post-rotation checklist:" echo " ✓ Update your personal password manager entry for this secret" echo " ✓ Run: make creds-bundle (refresh offsite backup)" echo " ✓ Run: make creds-verify (confirm cluster state)" else echo "Post-rotation (agent mode):" echo " ✓ creds-state.yaml updated with last_rotated timestamp" echo " Run: make creds-bundle (refresh offsite backup)" echo " Run: make creds-verify (confirm cluster state)" fi } # ── 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 update_last_rotated "PI_SECRET_KEY" 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 -- 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 update_last_rotated "PI_DB_PASSWORD" 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 update_last_rotated "LLDAP_JWT_SECRET" 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." if [[ "$NON_INTERACTIVE" == false ]]; then 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; } else echo " [non-interactive] Skipping LLDAP web UI step — update manually:" echo " Log in to https://lldap.coulomb.social and set admin password to: $NEW_VAL" fi 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 update_last_rotated "LLDAP_LDAP_USER_PASS" 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 update_last_rotated "AUTHELIA_SESSION_SECRET" 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 update_last_rotated "AUTHELIA_KEYCAPE_CLIENT_SECRET" 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 "" if [[ "$NON_INTERACTIVE" == false ]]; then echo " ✔ RSA key rotated. Store the new key in your password manager: net-kingdom/KeyCape/jwt-signing-key" else echo " ✔ RSA key rotated." fi update_last_rotated "KEYCAPE_RSA_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 "" if [[ "$NON_INTERACTIVE" == false ]]; then echo " Update your password manager: net-kingdom/Break-glass/break-glass" fi update_last_rotated "BREAKGLASS_PASSWORD" 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=" exit 1 ;; esac