generated from coulomb/repo-seed
Add KeyCape privacyIDEA token repair flow
This commit is contained in:
@@ -13,8 +13,9 @@
|
||||
# ./create-secrets.sh
|
||||
# kubectl rollout restart deployment/keycape -n sso
|
||||
#
|
||||
# The privacyIDEA admin token does NOT expire by default (it is a permanent
|
||||
# service account token). Store it in KeePassXC as:
|
||||
# The privacyIDEA admin token may expire depending on the live privacyIDEA
|
||||
# policy/configuration. Refresh it if KeyCape reports "mfa check error".
|
||||
# Store the current token in KeePassXC as:
|
||||
# net-kingdom/KeyCape/pi-admin-token
|
||||
#
|
||||
# Requires: kubectl, curl, jq
|
||||
@@ -108,7 +109,6 @@ echo " Done."
|
||||
|
||||
echo ""
|
||||
echo "Token written to: $TOKEN_FILE"
|
||||
echo "Token preview : ${TOKEN:0:32}…"
|
||||
echo ""
|
||||
echo "IMPORTANT: Store this token in KeePassXC → net-kingdom/KeyCape/pi-admin-token"
|
||||
echo " as a password entry. It cannot be recovered without re-authenticating."
|
||||
|
||||
226
sso-mfa/k8s/keycape/refresh-pi-token-live.sh
Normal file
226
sso-mfa/k8s/keycape/refresh-pi-token-live.sh
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/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
|
||||
|
||||
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}"
|
||||
|
||||
read -r -s -p "privacyIDEA pi-admin password: " PI_ADMIN_PASSWORD
|
||||
printf "\n" >&2
|
||||
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
|
||||
|
||||
api_get() {
|
||||
local path="$1"
|
||||
"$KUBECTL" exec -n "$MFA_NAMESPACE" "$PI_POD" -- \
|
||||
env PI_TOKEN="$PI_TOKEN" PI_PATH="$path" \
|
||||
python3 -c '
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
path = os.environ["PI_PATH"]
|
||||
token = os.environ["PI_TOKEN"]
|
||||
req = urllib.request.Request(
|
||||
"http://localhost:8080" + path,
|
||||
headers={"Authorization": token},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
sys.stdout.write(response.read().decode())
|
||||
except Exception as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
'
|
||||
}
|
||||
|
||||
echo "Inspecting non-secret MFA state for $USERNAME ..."
|
||||
realm_summary="$(
|
||||
for realm in coulomb netkingdom; do
|
||||
users_json="$(api_get "/user/?realm=$realm&username=$USERNAME" || true)"
|
||||
tokens_json="$(api_get "/token/?realm=$realm&user=$USERNAME" || true)"
|
||||
REALM="$realm" USERS_JSON="$users_json" TOKENS_JSON="$tokens_json" python3 -c '
|
||||
import json
|
||||
import os
|
||||
|
||||
realm = os.environ["REALM"]
|
||||
users = []
|
||||
tokens = []
|
||||
try:
|
||||
users = json.loads(os.environ["USERS_JSON"]).get("result", {}).get("value", {}).get("users", [])
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
value = json.loads(os.environ["TOKENS_JSON"]).get("result", {}).get("value", {})
|
||||
tokens = value.get("tokens", []) if isinstance(value, dict) else []
|
||||
except Exception:
|
||||
pass
|
||||
active = sum(1 for token in tokens if token.get("active", True))
|
||||
print(f"{realm} users={len(users)} tokens={len(tokens)} active={active}")
|
||||
'
|
||||
done
|
||||
)"
|
||||
printf '%s\n' "$realm_summary"
|
||||
|
||||
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 re
|
||||
|
||||
match = re.search(r"(?m)^ realm:\s*[\"'\"']?([^\"'\"'\n]+)", os.environ["CONFIG_YAML"])
|
||||
print(match.group(1).strip() if match else "")
|
||||
'
|
||||
)"
|
||||
|
||||
selected_realm="${KEYCAPE_PI_REALM:-}"
|
||||
if [[ -z "$selected_realm" ]]; then
|
||||
selected_realm="$(
|
||||
REALM_SUMMARY="$realm_summary" CURRENT_REALM="$current_realm" python3 -c '
|
||||
import os
|
||||
|
||||
lines = os.environ["REALM_SUMMARY"].splitlines()
|
||||
counts = {}
|
||||
for line in lines:
|
||||
parts = dict(item.split("=", 1) for item in line.split()[1:])
|
||||
counts[line.split()[0]] = {
|
||||
"users": int(parts.get("users", "0")),
|
||||
"tokens": int(parts.get("tokens", "0")),
|
||||
"active": int(parts.get("active", "0")),
|
||||
}
|
||||
for realm in ("coulomb", "netkingdom"):
|
||||
if counts.get(realm, {}).get("active", 0) > 0:
|
||||
print(realm)
|
||||
raise SystemExit
|
||||
current = os.environ.get("CURRENT_REALM", "")
|
||||
if current:
|
||||
print(current)
|
||||
else:
|
||||
print("coulomb")
|
||||
'
|
||||
)"
|
||||
fi
|
||||
|
||||
if [[ "$selected_realm" != "coulomb" && "$selected_realm" != "netkingdom" ]]; then
|
||||
echo "[FAIL] Refusing unsupported privacyIDEA realm: $selected_realm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Selected privacyIDEA realm for KeyCape: $selected_realm"
|
||||
|
||||
"$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" \
|
||||
python3 -c '
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
config = os.environ["CONFIG_YAML"]
|
||||
token = json.dumps(os.environ["PI_TOKEN"])
|
||||
realm = json.dumps(os.environ["PI_REALM"])
|
||||
|
||||
config, token_count = re.subn(r"(?m)^ adminToken:.*$", " adminToken: " + token, config)
|
||||
config, realm_count = re.subn(r"(?m)^ realm:.*$", " realm: " + realm, config)
|
||||
if token_count != 1 or realm_count != 1:
|
||||
print("Could not patch exactly one adminToken and one realm field.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
sys.stdout.write(config)
|
||||
' > "$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."
|
||||
89
sso-mfa/k8s/privacyidea/check-user-mfa-state.sh
Normal file
89
sso-mfa/k8s/privacyidea/check-user-mfa-state.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-user-mfa-state.sh — non-secret privacyIDEA MFA state diagnostic.
|
||||
#
|
||||
# Reports realm presence and user/token counts for a user without printing
|
||||
# passwords, OTP seeds, admin JWTs, token serials, or token metadata.
|
||||
#
|
||||
# Usage:
|
||||
# bash check-user-mfa-state.sh [username]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
USERNAME="${1:-platform-root}"
|
||||
PI_URL="${PI_URL:-https://pink.coulomb.social}"
|
||||
SSO_NAMESPACE="${SSO_NAMESPACE:-sso}"
|
||||
KUBECTL="${KUBECTL:-kubectl}"
|
||||
|
||||
TOKEN="$(
|
||||
"$KUBECTL" get secret keycape-pi-token -n "$SSO_NAMESPACE" \
|
||||
-o jsonpath='{.data.token}' 2>/dev/null | base64 -d 2>/dev/null || true
|
||||
)"
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
TOKEN="$(
|
||||
"$KUBECTL" get secret keycape-pi-token -n "$SSO_NAMESPACE" \
|
||||
-o jsonpath='{.data.pi_admin_token}' 2>/dev/null | base64 -d 2>/dev/null || true
|
||||
)"
|
||||
fi
|
||||
|
||||
if [[ -z "$TOKEN" || "$TOKEN" == "PENDING_create-pi-token.sh" ]]; then
|
||||
echo "[FAIL] keycape-pi-token is missing or still a placeholder."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api_get() {
|
||||
local path="$1"
|
||||
curl -sk -H "Authorization: $TOKEN" "$PI_URL$path"
|
||||
}
|
||||
|
||||
json_field() {
|
||||
python3 -c "$1"
|
||||
}
|
||||
|
||||
echo "privacyIDEA MFA state for user: $USERNAME"
|
||||
echo "Endpoint: $PI_URL"
|
||||
echo ""
|
||||
|
||||
REALM_RESP="$(api_get "/realm/")"
|
||||
echo "Realms:"
|
||||
printf '%s' "$REALM_RESP" | json_field '
|
||||
import json
|
||||
import sys
|
||||
|
||||
data = json.load(sys.stdin)
|
||||
if not data.get("result", {}).get("status", False):
|
||||
print(" [WARN] realm query returned status=false")
|
||||
err = data.get("result", {}).get("error", {})
|
||||
if err:
|
||||
print(" [WARN] error=" + str(err.get("message", err)))
|
||||
realms = data.get("result", {}).get("value", {})
|
||||
if not realms:
|
||||
print(" [WARN] no realms returned")
|
||||
for name in sorted(realms):
|
||||
print(" - " + name + " default=" + str(bool(realms[name].get("default"))))
|
||||
'
|
||||
|
||||
for realm in coulomb netkingdom; do
|
||||
echo ""
|
||||
echo "Realm: $realm"
|
||||
USER_RESP="$(api_get "/user/?realm=$realm&username=$USERNAME")"
|
||||
TOKEN_RESP="$(api_get "/token/?realm=$realm&user=$USERNAME")"
|
||||
|
||||
printf '%s' "$USER_RESP" | json_field '
|
||||
import json
|
||||
import sys
|
||||
|
||||
data = json.load(sys.stdin)
|
||||
users = data.get("result", {}).get("value", {}).get("users", [])
|
||||
print(f" users={len(users)}")
|
||||
'
|
||||
printf '%s' "$TOKEN_RESP" | json_field '
|
||||
import json
|
||||
import sys
|
||||
|
||||
data = json.load(sys.stdin)
|
||||
value = data.get("result", {}).get("value", {})
|
||||
tokens = value.get("tokens", []) if isinstance(value, dict) else []
|
||||
active = sum(1 for token in tokens if token.get("active", True))
|
||||
print(f" tokens={len(tokens)} active={active}")
|
||||
'
|
||||
done
|
||||
@@ -37,9 +37,9 @@ PASS=0
|
||||
FAIL=0
|
||||
WARN=0
|
||||
|
||||
pass() { echo " [PASS] $1"; ((PASS++)); }
|
||||
fail() { echo " [FAIL] $1"; ((FAIL++)); }
|
||||
warn() { echo " [WARN] $1"; ((WARN++)); }
|
||||
pass() { echo " [PASS] $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " [FAIL] $1"; FAIL=$((FAIL + 1)); }
|
||||
warn() { echo " [WARN] $1"; WARN=$((WARN + 1)); }
|
||||
|
||||
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user