Files
net-kingdom/sso-mfa/k8s/keycape/refresh-pi-token-live.sh

181 lines
5.9 KiB
Bash

#!/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."