#!/usr/bin/env bash # refresh-pi-token-live.sh — refresh KeyCape's privacyIDEA admin token. # # This is an attended repair for "mfa check error" when the privacyIDEA admin # JWT embedded in keycape-config has expired. It prompts for the pi-admin # password, fetches a fresh token inside the privacyIDEA pod, updates the # keycape-pi-token Secret, patches keycape-config without printing secrets, and # restarts KeyCape. # # Usage: # bash refresh-pi-token-live.sh [username] # # Optional environment: # KUBECTL=/path/to/kubectl # KEYCAPE_PI_REALM=coulomb|netkingdom # defaults to coulomb, the live privacyIDEA realm # KEYCAPE_PI_REQUIRE_FOR_ALL=true|false # defaults to true to avoid admin token-list checks set -euo pipefail USERNAME="${1:-platform-root}" KUBECTL="${KUBECTL:-kubectl}" SSO_NAMESPACE="${SSO_NAMESPACE:-sso}" MFA_NAMESPACE="${MFA_NAMESPACE:-mfa}" KEYCAPE_DEPLOYMENT="${KEYCAPE_DEPLOYMENT:-keycape}" KEYCAPE_SECRET="${KEYCAPE_SECRET:-keycape-config}" KEYCAPE_TOKEN_SECRET="${KEYCAPE_TOKEN_SECRET:-keycape-pi-token}" if [[ -r /dev/tty ]]; then printf "privacyIDEA pi-admin password: " > /dev/tty IFS= read -r -s PI_ADMIN_PASSWORD < /dev/tty printf "\n" > /dev/tty else printf "privacyIDEA pi-admin password: " >&2 IFS= read -r -s PI_ADMIN_PASSWORD printf "\n" >&2 fi if [[ -z "$PI_ADMIN_PASSWORD" ]]; then echo "[FAIL] Empty pi-admin password." >&2 exit 1 fi PI_POD="$( "$KUBECTL" get pod -n "$MFA_NAMESPACE" \ -l app.kubernetes.io/name=privacyidea \ --field-selector=status.phase=Running \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true )" if [[ -z "$PI_POD" ]]; then echo "[FAIL] No running privacyIDEA pod in namespace $MFA_NAMESPACE." >&2 exit 1 fi echo "Fetching fresh privacyIDEA admin token inside pod $PI_POD ..." PI_TOKEN="$( "$KUBECTL" exec -n "$MFA_NAMESPACE" "$PI_POD" -- \ env PI_ADMIN_PASSWORD="$PI_ADMIN_PASSWORD" \ python3 -c ' import json import os import sys import urllib.parse import urllib.request payload = urllib.parse.urlencode({ "username": "pi-admin", "password": os.environ["PI_ADMIN_PASSWORD"], }).encode() req = urllib.request.Request("http://localhost:8080/auth", data=payload) try: with urllib.request.urlopen(req, timeout=10) as response: body = json.load(response) print(body["result"]["value"]["token"]) except Exception as exc: print(str(exc), file=sys.stderr) sys.exit(1) ' 2>/dev/null )" unset PI_ADMIN_PASSWORD if [[ -z "$PI_TOKEN" ]]; then echo "[FAIL] Could not fetch a privacyIDEA admin token." >&2 exit 1 fi tmpdir="$(mktemp -d)" cleanup() { rm -rf "$tmpdir" } trap cleanup EXIT current_config="$( "$KUBECTL" get secret "$KEYCAPE_SECRET" -n "$SSO_NAMESPACE" \ -o jsonpath='{.data.config\.yaml}' | base64 -d )" current_realm="$( CONFIG_YAML="$current_config" python3 -c ' import os import sys try: import yaml except ImportError: print("PyYAML is required: install python3-yaml", file=sys.stderr) sys.exit(1) config = yaml.safe_load(os.environ["CONFIG_YAML"]) or {} privacyidea = config.get("privacyidea") or {} if not isinstance(privacyidea, dict): print("") else: print(str(privacyidea.get("realm") or "").strip()) ' )" selected_realm="${KEYCAPE_PI_REALM:-coulomb}" selected_require_for_all="${KEYCAPE_PI_REQUIRE_FOR_ALL:-true}" if [[ -z "${KEYCAPE_PI_REALM:-}" && -n "$current_realm" && "$current_realm" != "$selected_realm" ]]; then echo "[WARN] KeyCape currently points privacyIDEA at realm '$current_realm'; repairing to '$selected_realm'." >&2 fi if [[ "$selected_realm" != "coulomb" && "$selected_realm" != "netkingdom" ]]; then echo "[FAIL] Refusing unsupported privacyIDEA realm: $selected_realm" >&2 exit 1 fi if [[ "$selected_require_for_all" != "true" && "$selected_require_for_all" != "false" ]]; then echo "[FAIL] KEYCAPE_PI_REQUIRE_FOR_ALL must be true or false." >&2 exit 1 fi echo "Selected privacyIDEA realm for KeyCape: $selected_realm" echo "Selected privacyIDEA requireForAll for KeyCape: $selected_require_for_all" "$KUBECTL" get secret "$KEYCAPE_SECRET" -n "$SSO_NAMESPACE" \ -o jsonpath='{.data.key\.pem}' | base64 -d > "$tmpdir/key.pem" chmod 600 "$tmpdir/key.pem" CONFIG_YAML="$current_config" PI_TOKEN="$PI_TOKEN" PI_REALM="$selected_realm" PI_REQUIRE_FOR_ALL="$selected_require_for_all" \ python3 -c ' import os import sys try: import yaml except ImportError: print("PyYAML is required: install python3-yaml", file=sys.stderr) sys.exit(1) config = yaml.safe_load(os.environ["CONFIG_YAML"]) or {} if not isinstance(config, dict): print("KeyCape config.yaml must decode to a YAML mapping.", file=sys.stderr) sys.exit(1) privacyidea = config.setdefault("privacyidea", {}) if not isinstance(privacyidea, dict): print("KeyCape privacyidea config must decode to a YAML mapping.", file=sys.stderr) sys.exit(1) privacyidea["adminToken"] = os.environ["PI_TOKEN"] privacyidea["realm"] = os.environ["PI_REALM"] privacyidea["requireForAll"] = os.environ["PI_REQUIRE_FOR_ALL"] == "true" sys.stdout.write(yaml.safe_dump(config, sort_keys=False)) ' > "$tmpdir/config.yaml" echo "Applying refreshed KeyCape config Secret ..." "$KUBECTL" create secret generic "$KEYCAPE_TOKEN_SECRET" \ --namespace="$SSO_NAMESPACE" \ --from-literal=token="$PI_TOKEN" \ --dry-run=client -o yaml | "$KUBECTL" apply -f - "$KUBECTL" create secret generic "$KEYCAPE_SECRET" \ --namespace="$SSO_NAMESPACE" \ --from-file=config.yaml="$tmpdir/config.yaml" \ --from-file=key.pem="$tmpdir/key.pem" \ --dry-run=client -o yaml | "$KUBECTL" apply -f - unset PI_TOKEN echo "Restarting KeyCape ..." "$KUBECTL" rollout restart "deployment/$KEYCAPE_DEPLOYMENT" -n "$SSO_NAMESPACE" "$KUBECTL" rollout status "deployment/$KEYCAPE_DEPLOYMENT" -n "$SSO_NAMESPACE" --timeout=90s echo "" echo "[OK] KeyCape privacyIDEA token refreshed. Retry the OIDC login with a fresh URL."