#!/usr/bin/env bash # API-16 Ende-zu-Ende auf dem Entwicklungshost (Wegwerf-Cluster 17/coredev): echte Kontodienst-Prozesse # pruefen persistente Modul-Signaturschluessel: Neustart, zwei Instanzen, Rotation mit Ueberlappung, # Ausfall des Schluesselspeichers (503) und falscher Verschluesselungsschluessel. Dauer rund 3 Minuten. # usage (im Repo-Root, innerhalb der gemeinsamen Sperre): scripts/e2e-module-keys.sh set -euo pipefail cluster=/etc/nexarch-dev/core-cluster.sh set -a source /etc/nexarch-dev/core-dev.env source /etc/nexarch-dev/tenant-dev.env set +a export PATH="$PATH:/usr/local/go/bin" work="$(mktemp -d /tmp/nexarch-e2e-keys-XXXXXX)" pids=() cleanup() { for p in "${pids[@]:-}"; do kill "$p" 2>/dev/null || true; done psql "$TEST_TENANT_DSN" -q -c "ALTER TABLE IF EXISTS module_signing_keys_x RENAME TO module_signing_keys" >/dev/null 2>&1 || true rm -rf "$work" } trap cleanup EXIT fail() { echo "FAIL: $*" >&2; tail -15 "$work"/*.log >&2 2>/dev/null || true; exit 1; } pass() { echo "PASS: $*"; } rand() { head -c "$1" /dev/urandom | od -An -tx1 | tr -d ' \n'; } password="e2e-$(rand 12)" jwt_secret="e2e-$(rand 16)" enc_key="$(head -c 32 /dev/urandom | base64)" other_key="$(head -c 32 /dev/urandom | base64)" session_enc_key="$(head -c 32 /dev/urandom | base64)" "$cluster" resettenant for f in $(ls migrations/tenant/*.up.sql | sort); do psql "$TEST_TENANT_DSN" -q -v ON_ERROR_STOP=1 -f "$f" >/dev/null done go build -o "$work/account-api" ./cmd/account-api go build -o "$work/verify" ./scripts/e2e-modulekeys-verify NEXARCH_E2E_TENANT_DSN="$TEST_TENANT_DSN" NEXARCH_E2E_PASSWORD="$password" go run ./scripts/e2e-seed TTL=30 ACT=60 base_env=(NEXARCH_ACCOUNT_TENANT_DSN="$TEST_TENANT_DSN" NEXARCH_ACCOUNT_TENANT_SLUG=e2e NEXARCH_ACCOUNT_JWT_SECRET="$jwt_secret" NEXARCH_ACCOUNT_MODULE_TOKEN_TTL_SECONDS=$TTL NEXARCH_ACCOUNT_MODULE_KEY_ACTIVATION_DELAY_SECONDS=$ACT NEXARCH_ACCOUNT_MODULE_KEY_REFRESH_SECONDS=1 NEXARCH_ACCOUNT_MODULE_KEY_ROTATION_DAYS=0 # QA-15 F-10: dieses Skript startet den Kontodienst mehrfach neu und # nutzt eine vor dem Neustart erzeugte Sitzung danach weiter (mint auf # a2 mit dem Cookie von a) — ephemere Sitzungsschluessel wuerden diese # Sitzung beim Neustart ungueltig machen (siehe sessionkeys.go), daher # hier persistente Sitzungsschluessel mit festem Verschluesselungsschluessel. NEXARCH_ACCOUNT_SESSION_KEY_ENCRYPTION_KEY="$session_enc_key") # env ersetzt sich selbst durch das Programm: $! ist danach die PID des Kontodienstes (kein Zwischenprozess, der beim kill uebrig bleibt) LAST_PID="" start() { # port name [key] local key="${3:-$enc_key}" env "${base_env[@]}" NEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY="$key" NEXARCH_ACCOUNT_API_LISTEN_ADDR="127.0.0.1:$1" \ "$work/account-api" >"$work/$2.log" 2>&1 & LAST_PID=$! pids+=("$LAST_PID") } wait_up() { for _ in $(seq 1 60); do curl -fsS "http://127.0.0.1:$1/healthz" >/dev/null 2>&1 && return 0; sleep 0.2; done; fail "dienst auf $1 nicht erreichbar"; } kids() { curl -fsS "http://127.0.0.1:$1/auth/module-jwks" | grep -o '"kid":"[^"]*"' | cut -d'"' -f4 | sort | tr '\n' ' '; } code() { curl -s -o /dev/null -w '%{http_code}' "$@"; } login() { curl -fsS -c "$work/jar-$1" -H 'Content-Type: application/json' -d "{\"email\":\"alice@e2e.test\",\"password\":\"$password\"}" "http://127.0.0.1:$1/auth/login" >/dev/null; } mint() { curl -fsS -b "$work/jar-$1" -X POST "http://127.0.0.1:$1/auth/module-token" | grep -o '"token":"[^"]*"' | cut -d'"' -f4; } tokkid() { local seg; seg="$(echo "$1" | cut -d. -f1 | tr '_-' '/+')"; while [ $(( ${#seg} % 4 )) -ne 0 ]; do seg="$seg="; done; echo "$seg" | base64 -d 2>/dev/null | grep -o '"kid":"[^"]*"' | cut -d'"' -f4; } valid() { "$work/verify" "http://127.0.0.1:$1/auth/module-jwks" "$2" >/dev/null; } start 8099 a; a_pid=$LAST_PID; wait_up 8099 login 8099 kid0="$(kids 8099 | tr -d ' ')" [ -n "$kid0" ] || fail "keine kid in der jwks" tok0="$(mint 8099)" [ "$(tokkid "$tok0")" = "$kid0" ] || fail "token traegt nicht die aktive kid" valid 8099 "$tok0" || fail "token direkt nach dem start ungueltig" pass "start: jwks liefert kid $kid0, token gueltig" kill "$a_pid"; wait "$a_pid" 2>/dev/null || true for _ in $(seq 1 50); do curl -fsS "http://127.0.0.1:8099/healthz" >/dev/null 2>&1 || break; sleep 0.1; done curl -fsS "http://127.0.0.1:8099/healthz" >/dev/null 2>&1 && fail "kontodienst auf 8099 laeuft nach dem beenden weiter (kein echter neustart)" start 8099 a2; a2_pid=$LAST_PID; wait_up 8099 [ "$a2_pid" != "$a_pid" ] && kill -0 "$a2_pid" 2>/dev/null || fail "neu gestarteter kontodienst laeuft nicht" [ "$(kids 8099 | tr -d ' ')" = "$kid0" ] || fail "kid nach neustart geaendert" valid 8099 "$tok0" || fail "token vor dem neustart nach neustart ungueltig" pass "neustart (alter prozess beendet, neuer prozess $a2_pid): dieselbe kid, token vor dem neustart gueltig" start 8098 b; wait_up 8098 [ "$(kids 8098 | tr -d ' ')" = "$kid0" ] || fail "zweite instanz hat andere schluessel" login 8098 tokb="$(mint 8098)" tok_a="$(mint 8099)" valid 8099 "$tokb" || fail "token der instanz b gegen jwks von a ungueltig" valid 8098 "$tok_a" || fail "token der instanz a gegen jwks von b ungueltig" [ "$(psql "$TEST_TENANT_DSN" -Atc 'SELECT count(*) FROM module_signing_keys')" = 1 ] || fail "mehr als ein schluessel nach start zweier instanzen" pass "zwei instanzen: gleiche schluessel, gegenseitig verifizierbar, genau ein schluessel in der datenbank" rot_start=$(date +%s) env "${base_env[@]}" NEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY="$enc_key" "$work/account-api" -rotate-module-key >"$work/rotate.log" 2>&1 || fail "rotation fehlgeschlagen" sleep 3 [ "$(kids 8099 | wc -w)" = 2 ] || fail "a kennt den neuen schluessel nach 3 s nicht" [ "$(kids 8098 | wc -w)" = 2 ] || fail "b kennt den neuen schluessel nach 3 s nicht" tok_pending="$(mint 8099)" [ "$(tokkid "$tok_pending")" = "$kid0" ] || fail "neuer schluessel signiert vor der aktivierungsverzoegerung" pass "rotation: beide instanzen listen 2 schluessel, signiert wird bis zur aktivierung mit dem alten" sleep $(( ACT + 3 )) tok_new="$(mint 8099)" kid1="$(tokkid "$tok_new")" [ -n "$kid1" ] && [ "$kid1" != "$kid0" ] || fail "nach der aktivierungsverzoegerung signiert weiter der alte schluessel" [ "$(tokkid "$(mint 8098)")" = "$kid1" ] || fail "instanz b signiert nicht mit dem neuen schluessel" valid 8098 "$tok_new" || fail "token mit neuer kid ungueltig" [ "$(kids 8099 | wc -w)" = 2 ] || fail "alter schluessel vor ablauf von ttl und toleranz entfernt" pass "nach aktivierung: neue kid $kid1, alter schluessel bleibt in der jwks" wait_until=$(( rot_start + ACT + TTL + 30 + 8 )) now=$(date +%s) [ "$now" -lt "$wait_until" ] && sleep $(( wait_until - now )) [ "$(kids 8099 | tr -d ' ')" = "$kid1" ] || fail "alter schluessel nach ablauf noch in der jwks von a: $(kids 8099)" [ "$(kids 8098 | tr -d ' ')" = "$kid1" ] || fail "alter schluessel nach ablauf noch in der jwks von b" [ "$(psql "$TEST_TENANT_DSN" -Atc 'SELECT count(*) FROM module_signing_keys')" = 1 ] || fail "abgelaufener schluessel nicht aus der datenbank entfernt" tok_last="$(mint 8098)" valid 8099 "$tok_last" || fail "token nach dem entfernen des alten schluessels ungueltig" pass "nach ablauf von ttl und toleranz: alter schluessel aus jwks und datenbank entfernt" psql "$TEST_TENANT_DSN" -q -c "ALTER TABLE module_signing_keys RENAME TO module_signing_keys_x" >/dev/null sleep 2 [ "$(code "http://127.0.0.1:8099/auth/module-jwks")" = 503 ] || fail "jwks bei nicht lesbarem schluesselspeicher nicht 503" [ "$(code -b "$work/jar-8099" -X POST "http://127.0.0.1:8099/auth/module-token")" = 503 ] || fail "token bei nicht lesbarem schluesselspeicher nicht 503" psql "$TEST_TENANT_DSN" -q -c "ALTER TABLE module_signing_keys_x RENAME TO module_signing_keys" >/dev/null sleep 2 [ "$(code "http://127.0.0.1:8099/auth/module-jwks")" = 200 ] || fail "jwks nach wiederanlauf nicht 200" [ "$(code -b "$work/jar-8099" -X POST "http://127.0.0.1:8099/auth/module-token")" = 200 ] || fail "token nach wiederanlauf nicht 200" pass "ausfall des schluesselspeichers: 503 fuer jwks und token, danach wieder 200" start 8097 wrongkey "$other_key"; wrong_pid=$LAST_PID for _ in $(seq 1 50); do kill -0 "$wrong_pid" 2>/dev/null || break; sleep 0.2; done if kill -0 "$wrong_pid" 2>/dev/null; then fail "dienst mit falschem verschluesselungsschluessel laeuft"; fi grep -q "entschluesseln" "$work/wrongkey.log" || fail "keine meldung zum falschen schluessel: $(tail -3 "$work/wrongkey.log")" env "${base_env[@]}" NEXARCH_ACCOUNT_MODULE_KEY_ENCRYPTION_KEY="" NEXARCH_ACCOUNT_API_LISTEN_ADDR=127.0.0.1:8096 "$work/account-api" >"$work/nokey.log" 2>&1 && fail "dienst ohne verschluesselungsschluessel startet" grep -q "MODULE_KEY_ENCRYPTION_KEY" "$work/nokey.log" || fail "keine meldung zum fehlenden schluessel" pass "falscher oder fehlender verschluesselungsschluessel: dienst startet nicht" echo "ALLE PRUEFUNGEN BESTANDEN"