generated from coulomb/repo-seed
feat(creds): implement NK-WP-0004 Credential Management Foundation
- .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>
This commit is contained in:
119
sso-mfa/bootstrap/creds-apply.sh
Executable file
119
sso-mfa/bootstrap/creds-apply.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# creds-apply.sh — run all create-secrets.sh scripts in dependency order.
|
||||
#
|
||||
# Usage:
|
||||
# bash sso-mfa/bootstrap/creds-apply.sh [secrets-dir]
|
||||
# make creds-apply
|
||||
#
|
||||
# Dependency order:
|
||||
# 1. postgresql (no dependencies)
|
||||
# 2. lldap (needs: secrets/lldap/secrets.env)
|
||||
# 3. authelia (needs: lldap/secrets.env → LLDAP_LDAP_USER_PASS)
|
||||
# 4. privacyidea (needs: secrets/privacyidea/secrets.env)
|
||||
# 5. keycape SKIPPED — requires PI_ADMIN_TOKEN from post-T04 bootstrap.
|
||||
# Run sso-mfa/k8s/keycape/create-pi-token.sh, then
|
||||
# sso-mfa/k8s/keycape/create-secrets.sh manually.
|
||||
#
|
||||
# After each successful component, creds-state.yaml is updated.
|
||||
|
||||
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"
|
||||
|
||||
# ── Preflight checks ──────────────────────────────────────────────────────────
|
||||
if [[ -z "${KUBECONFIG:-}" && ! -f "$HOME/.kube/config" ]]; then
|
||||
echo "ERROR: KUBECONFIG is not set and ~/.kube/config does not exist." >&2
|
||||
echo " Set KUBECONFIG or ensure cluster access before running creds-apply." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! kubectl cluster-info &>/dev/null; then
|
||||
echo "ERROR: Cannot reach the Kubernetes cluster." >&2
|
||||
echo " Ensure the cluster is running and KUBECONFIG points to the right context." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SECRETS_DIR" ]]; then
|
||||
echo "ERROR: secrets directory not found: $SECRETS_DIR" >&2
|
||||
echo " Run 'make creds-generate' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== creds-apply — net-kingdom SSO/MFA secrets ==="
|
||||
echo "Secrets dir : $SECRETS_DIR"
|
||||
echo "Cluster : $(kubectl config current-context 2>/dev/null || echo '(unknown)')"
|
||||
echo ""
|
||||
|
||||
# ── Helper: run a component's create-secrets.sh ───────────────────────────────
|
||||
run_component() {
|
||||
local component="$1"
|
||||
local script_dir="$K8S_DIR/$component"
|
||||
local script="$script_dir/create-secrets.sh"
|
||||
local state_key="$2"
|
||||
|
||||
echo "──────────────────────────────────────────────"
|
||||
echo "Applying: $component"
|
||||
|
||||
if [[ ! -f "$script" ]]; then
|
||||
echo "ERROR: $script not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(cd "$script_dir" && bash create-secrets.sh "$SECRETS_DIR")
|
||||
|
||||
# Update state file
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
sed -i "s|^ $state_key: .*| $state_key: true|" "$STATE_FILE"
|
||||
echo " [state] secrets_applied.$state_key → true"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Step 1: PostgreSQL ────────────────────────────────────────────────────────
|
||||
run_component "postgresql" "postgres"
|
||||
|
||||
# ── Step 2: LLDAP ────────────────────────────────────────────────────────────
|
||||
if [[ ! -f "$SECRETS_DIR/lldap/secrets.env" ]]; then
|
||||
echo "ERROR: $SECRETS_DIR/lldap/secrets.env not found — run creds-generate first." >&2
|
||||
exit 1
|
||||
fi
|
||||
run_component "lldap" "lldap"
|
||||
|
||||
# ── Step 3: Authelia (needs LLDAP bind password from lldap/secrets.env) ───────
|
||||
run_component "authelia" "authelia"
|
||||
|
||||
# ── Step 4: privacyIDEA ───────────────────────────────────────────────────────
|
||||
if [[ ! -f "$SECRETS_DIR/privacyidea/secrets.env" ]]; then
|
||||
echo "ERROR: $SECRETS_DIR/privacyidea/secrets.env not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
run_component "privacyidea" "privacyidea"
|
||||
|
||||
# ── Step 5: KeyCape — SKIPPED (requires PI_ADMIN_TOKEN) ──────────────────────
|
||||
echo "──────────────────────────────────────────────"
|
||||
echo "SKIPPED: keycape (requires PI_ADMIN_TOKEN from post-T04 bootstrap)"
|
||||
echo ""
|
||||
echo " After privacyIDEA is Running and bootstrapped:"
|
||||
echo " 1. TIME-SENSITIVE: run enckey-bootstrap.sh while pod is live:"
|
||||
echo " bash $K8S_DIR/privacyidea/enckey-bootstrap.sh"
|
||||
echo ""
|
||||
echo " 2. Create the pi-admin user:"
|
||||
echo " bash $K8S_DIR/privacyidea/bootstrap-admin.sh"
|
||||
echo ""
|
||||
echo " 3. Generate the PI admin token for KeyCape:"
|
||||
echo " bash $K8S_DIR/keycape/create-pi-token.sh"
|
||||
echo ""
|
||||
echo " 4. Apply KeyCape secrets:"
|
||||
echo " bash $K8S_DIR/keycape/create-secrets.sh $SECRETS_DIR"
|
||||
echo ""
|
||||
echo " 5. Update state:"
|
||||
echo " make creds-verify"
|
||||
echo ""
|
||||
|
||||
echo "=== creds-apply complete ==="
|
||||
echo "Run 'make creds-status' to review state."
|
||||
echo "Run 'make creds-verify' to confirm K8s secrets exist."
|
||||
296
sso-mfa/bootstrap/creds-rotate.sh
Executable file
296
sso-mfa/bootstrap/creds-rotate.sh
Executable file
@@ -0,0 +1,296 @@
|
||||
#!/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
|
||||
26
sso-mfa/bootstrap/creds-state.yaml
Normal file
26
sso-mfa/bootstrap/creds-state.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Credential state — net-kingdom SSO/MFA stack
|
||||
# This file is SAFE TO COMMIT. It contains no secrets.
|
||||
# Updated automatically by make creds-* targets and sso-mfa/bootstrap/creds-verify.sh.
|
||||
#
|
||||
# keepass_confirmed is the only field that requires manual operator intervention.
|
||||
# Set it to true after you have entered all generated secrets into KeePassXC.
|
||||
|
||||
generated_at: null
|
||||
bundle_at: null
|
||||
keepass_confirmed: false
|
||||
|
||||
secrets_applied:
|
||||
postgres: false
|
||||
lldap: false
|
||||
authelia: false
|
||||
privacyidea: false
|
||||
# keycape requires PI_ADMIN_TOKEN from post-privacyIDEA T04 bootstrap.
|
||||
# Run: sso-mfa/k8s/keycape/create-pi-token.sh, then re-run keycape/create-secrets.sh.
|
||||
keycape: false
|
||||
|
||||
# enckey_bootstrapped: set by sso-mfa/k8s/privacyidea/enckey-bootstrap.sh
|
||||
# This step is TIME-SENSITIVE — it must run while the privacyIDEA pod is live.
|
||||
enckey_bootstrapped: false
|
||||
|
||||
# pi_admin_created: set after sso-mfa/k8s/privacyidea/bootstrap-admin.sh completes
|
||||
pi_admin_created: false
|
||||
65
sso-mfa/bootstrap/creds-status.sh
Executable file
65
sso-mfa/bootstrap/creds-status.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# creds-status.sh — print a human-readable credential state table.
|
||||
#
|
||||
# Usage:
|
||||
# bash sso-mfa/bootstrap/creds-status.sh
|
||||
# make creds-status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
STATE_FILE="${1:-$SCRIPT_DIR/creds-state.yaml}"
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "ERROR: creds-state.yaml not found: $STATE_FILE" >&2
|
||||
echo " This file is created at repo init — check your working directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Simple key extractors (no yaml lib dependency)
|
||||
top_val() { grep -E "^$1:" "$STATE_FILE" | sed 's/^[^:]*: *//' | sed 's/ *#.*//' | tr -d '"'; }
|
||||
nested_val() { grep -E "^ $1:" "$STATE_FILE" | sed 's/^[^:]*: *//' | sed 's/ *#.*//' | tr -d '"'; }
|
||||
|
||||
status_icon() {
|
||||
case "$1" in
|
||||
true) echo "✔" ;;
|
||||
false) echo "✗" ;;
|
||||
null) echo "—" ;;
|
||||
*) echo "?" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "=== net-kingdom Credential State ==="
|
||||
echo ""
|
||||
|
||||
generated_at="$(top_val generated_at)"
|
||||
bundle_at="$(top_val bundle_at)"
|
||||
keepass_confirmed="$(top_val keepass_confirmed)"
|
||||
|
||||
printf " %-30s %s\n" "Generated at:" "${generated_at:-—}"
|
||||
printf " %-30s %s\n" "Bundle at:" "${bundle_at:-—}"
|
||||
printf " %-30s %s %s\n" "KeePassXC confirmed:" \
|
||||
"$(status_icon "$keepass_confirmed")" \
|
||||
"$([ "$keepass_confirmed" = "false" ] && echo "(set keepass_confirmed: true manually)" || true)"
|
||||
echo ""
|
||||
|
||||
echo " Secrets applied:"
|
||||
for component in postgres lldap authelia privacyidea keycape; do
|
||||
val="$(nested_val "$component")"
|
||||
note=""
|
||||
[[ "$component" == "keycape" && "$val" == "false" ]] && \
|
||||
note=" (requires PI_ADMIN_TOKEN — post-T04)"
|
||||
printf " %-28s %s%s\n" "$component" "$(status_icon "$val")" "$note"
|
||||
done
|
||||
echo ""
|
||||
|
||||
enckey="$(top_val enckey_bootstrapped)"
|
||||
pi_admin="$(top_val pi_admin_created)"
|
||||
|
||||
printf " %-30s %s%s\n" "enckey bootstrapped:" \
|
||||
"$(status_icon "$enckey")" \
|
||||
"$([ "$enckey" = "false" ] && echo " ← TIME-SENSITIVE once pod is live" || true)"
|
||||
printf " %-30s %s\n" "pi-admin created:" "$(status_icon "$pi_admin")"
|
||||
|
||||
echo ""
|
||||
echo "Run 'make creds-verify' to refresh state from the live cluster."
|
||||
104
sso-mfa/bootstrap/creds-verify.sh
Executable file
104
sso-mfa/bootstrap/creds-verify.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# creds-verify.sh — check all expected K8s secrets exist and update creds-state.yaml.
|
||||
#
|
||||
# Usage:
|
||||
# bash sso-mfa/bootstrap/creds-verify.sh
|
||||
# make creds-verify
|
||||
#
|
||||
# Checks the following K8s secrets:
|
||||
# databases/net-kingdom-pg-privacyidea-app → secrets_applied.postgres
|
||||
# sso/lldap-secrets → secrets_applied.lldap
|
||||
# sso/authelia-secrets → secrets_applied.authelia
|
||||
# mfa/privacyidea-config → secrets_applied.privacyidea
|
||||
# sso/keycape-config → secrets_applied.keycape
|
||||
# mfa/privacyidea-enckey → enckey_bootstrapped
|
||||
# sso/keycape-pi-token → pi_admin_created
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
STATE_FILE="$SCRIPT_DIR/creds-state.yaml"
|
||||
|
||||
if ! kubectl cluster-info &>/dev/null; then
|
||||
echo "ERROR: Cannot reach the Kubernetes cluster. Verify KUBECONFIG." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Check helper ──────────────────────────────────────────────────────────────
|
||||
secret_exists() {
|
||||
local ns="$1" name="$2"
|
||||
kubectl get secret "$name" --namespace="$ns" --ignore-not-found -o name 2>/dev/null | grep -q .
|
||||
}
|
||||
|
||||
update_state_top() {
|
||||
local key="$1" value="$2"
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
sed -i "s|^$key: .*|$key: $value|" "$STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
update_state_nested() {
|
||||
local key="$1" value="$2"
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
sed -i "s|^ $key: .*| $key: $value|" "$STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Results table ─────────────────────────────────────────────────────────────
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
local label="$1" ns="$2" secret="$3" state_fn="$4" state_key="$5"
|
||||
if secret_exists "$ns" "$secret"; then
|
||||
printf " %-40s ✔ exists\n" "$label"
|
||||
"$state_fn" "$state_key" "true"
|
||||
((pass++)) || true
|
||||
else
|
||||
printf " %-40s ✗ missing (ns: %s, secret: %s)\n" "$label" "$ns" "$secret"
|
||||
"$state_fn" "$state_key" "false"
|
||||
((fail++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== creds-verify — net-kingdom SSO/MFA secrets ==="
|
||||
echo ""
|
||||
|
||||
check "postgres (net-kingdom-pg-privacyidea-app)" \
|
||||
databases net-kingdom-pg-privacyidea-app \
|
||||
update_state_nested postgres
|
||||
|
||||
check "lldap (lldap-secrets)" \
|
||||
sso lldap-secrets \
|
||||
update_state_nested lldap
|
||||
|
||||
check "authelia (authelia-secrets)" \
|
||||
sso authelia-secrets \
|
||||
update_state_nested authelia
|
||||
|
||||
check "privacyidea (privacyidea-config)" \
|
||||
mfa privacyidea-config \
|
||||
update_state_nested privacyidea
|
||||
|
||||
check "keycape (keycape-config)" \
|
||||
sso keycape-config \
|
||||
update_state_nested keycape
|
||||
|
||||
echo ""
|
||||
|
||||
check "enckey (privacyidea-enckey)" \
|
||||
mfa privacyidea-enckey \
|
||||
update_state_top enckey_bootstrapped
|
||||
|
||||
check "pi-admin token (keycape-pi-token)" \
|
||||
sso keycape-pi-token \
|
||||
update_state_top pi_admin_created
|
||||
|
||||
echo ""
|
||||
echo "Results: $pass present, $fail missing"
|
||||
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
echo "State file updated: $STATE_FILE"
|
||||
fi
|
||||
|
||||
[[ "$fail" -eq 0 ]]
|
||||
Reference in New Issue
Block a user