generated from coulomb/repo-seed
Replaces the Keycloak+privacyIDEA SSO tier with the lightweight stack built during KEY-WP-0001: Authelia (password frontend), LLDAP (directory), and KeyCape (OIDC orchestration). privacyIDEA is retained as the MFA engine. Stack: kc.coulomb.social — KeyCape OIDC server (stateless, custom Go) auth.coulomb.social — Authelia login portal (password auth → Authelia OIDC → KeyCape) lldap.coulomb.social — LLDAP admin UI (IP-restricted) pink.coulomb.social — privacyIDEA MFA engine (unchanged) Changes: - Remove sso-mfa/k8s/keycloak/ (7 files) - Add sso-mfa/k8s/lldap/ (pvc, deployment, middleware, ingress, create-secrets, README) - Add sso-mfa/k8s/authelia/ (pvc, configmap, deployment, ingress, create-secrets, README) - Add sso-mfa/k8s/keycape/ (deployment, middleware, ingress, create-secrets, create-pi-token, README) - Update network-policies/netpol-sso.yaml for new component topology - Update verify-t05.sh: checks LLDAP + Authelia + KeyCape (23 checks) - Update CONFIG.md: fix CP-NK-004 (KeyCape), add CP-NK-005 (Authelia), CP-NK-006 (LLDAP) - Update bootstrap/gen-secrets.sh: add LLDAP/Authelia/KeyCape sections, remove Keycloak - Update k8s/README.md: network policy table reflects new traffic paths - Add sso-mfa/WORKPLAN.md: resumable task checklist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
314 lines
11 KiB
Bash
Executable File
314 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# verify-t05.sh — verify NK-WP-0001-T05 done-criteria
|
|
#
|
|
# Checks all three components of the new SSO stack (LLDAP, Authelia, KeyCape).
|
|
#
|
|
# Sections:
|
|
# 1. LLDAP pod Running+Ready
|
|
# 2. LLDAP services (ldap :3890, web-ui :17170)
|
|
# 3. LLDAP Middleware (lldap-admin-allowlist)
|
|
# 4. LLDAP Ingress + hostname (lldap.coulomb.social)
|
|
# 5. LLDAP TLS certificate
|
|
# 6. LLDAP Secret (lldap-secrets)
|
|
# 7. LLDAP PVC Bound
|
|
# 8. LLDAP health endpoint
|
|
# 9. Authelia pod Running+Ready
|
|
# 10. Authelia service
|
|
# 11. Authelia Ingress + hostname (auth.coulomb.social)
|
|
# 12. Authelia TLS certificate
|
|
# 13. Authelia Secrets (authelia-secrets)
|
|
# 14. Authelia PVC Bound
|
|
# 15. Authelia health endpoint
|
|
# 16. KeyCape pod Running+Ready
|
|
# 17. KeyCape service
|
|
# 18. KeyCape Middlewares (keycape-rate-limit, keycape-hsts)
|
|
# 19. KeyCape Ingress + hostname (kc.coulomb.social)
|
|
# 20. KeyCape TLS certificate
|
|
# 21. KeyCape Secret (keycape-config)
|
|
# 22. KeyCape health endpoint (/healthz)
|
|
# 23. KeyCape OIDC discovery (/.well-known/openid-configuration)
|
|
#
|
|
# Usage:
|
|
# chmod +x verify-t05.sh
|
|
# ./verify-t05.sh
|
|
|
|
set -euo pipefail
|
|
|
|
NAMESPACE="sso"
|
|
LLDAP_HOST="lldap.coulomb.social"
|
|
AUTH_HOST="auth.coulomb.social"
|
|
KC_HOST="kc.coulomb.social"
|
|
PASS=0
|
|
FAIL=0
|
|
WARN=0
|
|
|
|
pass() { echo " [PASS] $1"; ((PASS++)); }
|
|
fail() { echo " [FAIL] $1"; ((FAIL++)); }
|
|
warn() { echo " [WARN] $1"; ((WARN++)); }
|
|
|
|
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
|
|
|
|
check_pod() {
|
|
local name="$1"
|
|
local label="$2"
|
|
local pod=""
|
|
pod=$(kubectl get pod -n "$NAMESPACE" \
|
|
-l "app.kubernetes.io/name=${label}" \
|
|
--field-selector=status.phase=Running \
|
|
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
|
if [[ -n "$pod" ]]; then
|
|
pass "Pod Running: $pod"
|
|
local ready
|
|
ready=$(kubectl get pod -n "$NAMESPACE" "$pod" \
|
|
-o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null || echo "false")
|
|
if [[ "$ready" == "true" ]]; then
|
|
pass "Pod readiness probe passing"
|
|
else
|
|
fail "Pod is Running but not Ready (probe failing — check logs)"
|
|
fi
|
|
echo "$pod"
|
|
else
|
|
local count
|
|
count=$(kubectl get pod -n "$NAMESPACE" \
|
|
-l "app.kubernetes.io/name=${label}" -o name 2>/dev/null | wc -l || echo 0)
|
|
if [[ "$count" -gt 0 ]]; then
|
|
fail "${name} pod(s) exist but none Running (kubectl describe pod -n $NAMESPACE -l app.kubernetes.io/name=${label})"
|
|
else
|
|
fail "No ${name} pods found — apply deployment.yaml"
|
|
fi
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
check_service() {
|
|
local svc="$1"; shift
|
|
local ports=("$@")
|
|
if kubectl get service "$svc" -n "$NAMESPACE" &>/dev/null; then
|
|
pass "Service $svc exists"
|
|
for port in "${ports[@]}"; do
|
|
if kubectl get service "$svc" -n "$NAMESPACE" \
|
|
-o jsonpath="{.spec.ports[*].port}" 2>/dev/null | grep -qw "$port"; then
|
|
pass "Service $svc has port $port"
|
|
else
|
|
fail "Service $svc missing port $port"
|
|
fi
|
|
done
|
|
else
|
|
fail "Service $svc not found"
|
|
fi
|
|
}
|
|
|
|
check_ingress() {
|
|
local ing="$1"; local expected_host="$2"
|
|
if kubectl get ingress "$ing" -n "$NAMESPACE" &>/dev/null; then
|
|
pass "Ingress $ing exists"
|
|
local host
|
|
host=$(kubectl get ingress "$ing" -n "$NAMESPACE" \
|
|
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "")
|
|
if [[ "$host" == "$expected_host" ]]; then
|
|
pass "Ingress $ing host: $expected_host"
|
|
else
|
|
fail "Ingress $ing host is '$host' (expected $expected_host)"
|
|
fi
|
|
else
|
|
fail "Ingress $ing not found — apply ingress.yaml"
|
|
fi
|
|
}
|
|
|
|
check_tls() {
|
|
local secret="$1"; local host="$2"
|
|
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
|
|
local cert_name="${secret%-tls}"
|
|
local cert_ready
|
|
cert_ready=$(kubectl get certificate "$cert_name" -n "$NAMESPACE" \
|
|
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "")
|
|
if [[ "$cert_ready" == "True" ]]; then
|
|
pass "Certificate $cert_name Ready (TLS secret $secret issued)"
|
|
else
|
|
warn "TLS secret $secret exists but cert status not Ready (DNS/ACME pending?)"
|
|
fi
|
|
else
|
|
warn "TLS secret $secret not yet issued (cert-manager pending — check DNS and ACME)"
|
|
fi
|
|
}
|
|
|
|
check_secret() {
|
|
local secret="$1"
|
|
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
|
|
pass "Secret $secret exists"
|
|
else
|
|
fail "Secret $secret not found — run create-secrets.sh"
|
|
fi
|
|
}
|
|
|
|
check_pvc() {
|
|
local pvc="$1"
|
|
local status
|
|
status=$(kubectl get pvc "$pvc" -n "$NAMESPACE" \
|
|
-o jsonpath='{.status.phase}' 2>/dev/null || echo "not found")
|
|
if [[ "$status" == "Bound" ]]; then
|
|
pass "PVC $pvc: Bound"
|
|
elif [[ "$status" == "not found" ]]; then
|
|
fail "PVC $pvc not found — apply pvc.yaml"
|
|
else
|
|
fail "PVC $pvc status: $status (expected Bound)"
|
|
fi
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# LLDAP
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
section "1. LLDAP pod (namespace: $NAMESPACE)"
|
|
LLDAP_POD=$(check_pod "LLDAP" "lldap")
|
|
|
|
section "2. LLDAP services"
|
|
check_service "lldap" 3890 17170
|
|
|
|
section "3. LLDAP Middleware"
|
|
if kubectl get middleware lldap-admin-allowlist -n "$NAMESPACE" &>/dev/null; then
|
|
pass "Middleware lldap-admin-allowlist exists"
|
|
else
|
|
fail "Middleware lldap-admin-allowlist not found — apply middleware.yaml"
|
|
fi
|
|
|
|
section "4. LLDAP Ingress"
|
|
check_ingress "lldap" "$LLDAP_HOST"
|
|
|
|
section "5. LLDAP TLS certificate"
|
|
check_tls "lldap-tls" "$LLDAP_HOST"
|
|
|
|
section "6. LLDAP Secret"
|
|
check_secret "lldap-secrets"
|
|
|
|
section "7. LLDAP PVC"
|
|
check_pvc "lldap-data"
|
|
|
|
section "8. LLDAP health endpoint"
|
|
if [[ -n "$LLDAP_POD" ]]; then
|
|
HTTP=$(kubectl exec -n "$NAMESPACE" "$LLDAP_POD" -- \
|
|
wget -qO- http://localhost:17170/health 2>/dev/null || echo "")
|
|
if echo "$HTTP" | grep -qi "ok\|healthy\|true"; then
|
|
pass "LLDAP health endpoint responds OK"
|
|
else
|
|
warn "LLDAP health endpoint response unclear: '$HTTP'"
|
|
fi
|
|
else
|
|
warn "Skipping LLDAP health check — no running pod"
|
|
fi
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# Authelia
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
section "9. Authelia pod (namespace: $NAMESPACE)"
|
|
AUTHELIA_POD=$(check_pod "Authelia" "authelia")
|
|
|
|
section "10. Authelia service"
|
|
check_service "authelia" 9091
|
|
|
|
section "11. Authelia Ingress"
|
|
check_ingress "authelia" "$AUTH_HOST"
|
|
|
|
section "12. Authelia TLS certificate"
|
|
check_tls "auth-tls" "$AUTH_HOST"
|
|
|
|
section "13. Authelia Secrets"
|
|
check_secret "authelia-secrets"
|
|
|
|
section "14. Authelia PVC"
|
|
check_pvc "authelia-data"
|
|
|
|
section "15. Authelia health endpoint"
|
|
if [[ -n "$AUTHELIA_POD" ]]; then
|
|
STATUS=$(kubectl exec -n "$NAMESPACE" "$AUTHELIA_POD" -- \
|
|
wget -qO- http://localhost:9091/api/health 2>/dev/null || echo "")
|
|
if echo "$STATUS" | grep -qi "ok\|healthy"; then
|
|
pass "Authelia /api/health responds OK"
|
|
else
|
|
warn "Authelia /api/health response unclear: '$STATUS'"
|
|
fi
|
|
else
|
|
warn "Skipping Authelia health check — no running pod"
|
|
fi
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# KeyCape
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
section "16. KeyCape pod (namespace: $NAMESPACE)"
|
|
KC_POD=$(check_pod "KeyCape" "keycape")
|
|
|
|
section "17. KeyCape service"
|
|
check_service "keycape" 8080
|
|
|
|
section "18. KeyCape Middlewares"
|
|
for mw in keycape-rate-limit keycape-hsts; do
|
|
if kubectl get middleware "$mw" -n "$NAMESPACE" &>/dev/null; then
|
|
pass "Middleware $mw exists"
|
|
else
|
|
fail "Middleware $mw not found — apply middleware.yaml"
|
|
fi
|
|
done
|
|
|
|
section "19. KeyCape Ingress"
|
|
check_ingress "keycape" "$KC_HOST"
|
|
|
|
section "20. KeyCape TLS certificate"
|
|
check_tls "kc-tls" "$KC_HOST"
|
|
|
|
section "21. KeyCape Secrets"
|
|
check_secret "keycape-config"
|
|
|
|
section "22. KeyCape health endpoint"
|
|
if [[ -n "$KC_POD" ]]; then
|
|
STATUS=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
|
|
wget -qO- http://localhost:8080/healthz 2>/dev/null || echo "")
|
|
if echo "$STATUS" | grep -qi "ok\|healthy"; then
|
|
pass "KeyCape /healthz responds OK"
|
|
else
|
|
warn "KeyCape /healthz response unclear: '$STATUS'"
|
|
fi
|
|
else
|
|
warn "Skipping KeyCape health check — no running pod"
|
|
fi
|
|
|
|
section "23. KeyCape OIDC discovery"
|
|
if [[ -n "$KC_POD" ]]; then
|
|
DISCOVERY=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
|
|
wget -qO- "http://localhost:8080/.well-known/openid-configuration" 2>/dev/null || echo "")
|
|
if echo "$DISCOVERY" | grep -q '"issuer"'; then
|
|
pass "OIDC discovery endpoint returns issuer"
|
|
ISSUER=$(echo "$DISCOVERY" | python3 -c \
|
|
"import sys,json; d=json.load(sys.stdin); print(d.get('issuer',''))" 2>/dev/null || echo "")
|
|
if [[ "$ISSUER" == "https://$KC_HOST" ]]; then
|
|
pass "OIDC issuer matches CP-NK-004: $ISSUER"
|
|
else
|
|
warn "OIDC issuer is '$ISSUER' (expected https://$KC_HOST)"
|
|
fi
|
|
else
|
|
fail "OIDC discovery endpoint did not return expected JSON"
|
|
fi
|
|
else
|
|
warn "Skipping OIDC discovery check — no running pod"
|
|
fi
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# Summary
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
echo ""
|
|
echo "════════════════════════════════════════════════════════════"
|
|
echo " T05 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL"
|
|
echo "════════════════════════════════════════════════════════════"
|
|
|
|
if [[ "$FAIL" -gt 0 ]]; then
|
|
echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T06"
|
|
exit 1
|
|
elif [[ "$WARN" -gt 0 ]]; then
|
|
echo " Result: PARTIAL — T05 core is up; WARN items should be resolved before T06"
|
|
exit 0
|
|
else
|
|
echo " Result: COMPLETE — T05 done-criteria met; proceed to T06 (MFA flow integration)"
|
|
exit 0
|
|
fi
|