# ============================================================================= # L0 STATIC — the passkey configuration is derived from one declaration, continuously # reconciled, and the browser login flow converges BLUE/GREEN: the BOUND flow is # unwritable, and binding a new one is git's explicit decision (bind-browser-flow) # Catalog: docs/14 L0 row ("Pre-apply, in CI") — runs WITHOUT a cluster. # # R-IDP-03 (identity config is declarative) and R-IDP-04 (MFA available) are both # implemented through a mechanism that FAILS OPEN: the KeycloakRealmImport is a ONE-SHOT. # Measured on prod 2026-10-18, hours after the passkey declaration merged: # # kubectl -n keycloak get keycloakrealmimport homelab -o jsonpath='{.status}' # Done=True Started=False HasErrors=False # # Once it rests at Done=True the operator never re-runs it, so the WebAuthn passwordless # policy and the restructured `browser-mfa` flow that keycloak-realm-import.yaml declares # are INERT — in git, applied by Flux, reported Ready, reaching the realm never. Merging # them changed nothing about how anyone logs in, and nothing anywhere said so. # Declarative config that silently does not apply is not declarative config. # # GIVEN the repository as authored, # THEN the reconciler's desired state is GENERATED from the realm import (so the RP ID — # which is baked into every passkey at enrolment and can NEVER be re-keyed — is # declared exactly once); both files are rendered by the SAME Flux Kustomization, so # their ${BASE_DOMAIN} token cannot resolve to two values; the renderer's exported # policy keys and the reconciler's writable keys are the SAME tuple; the realm # import declares NO requiredActions block (26.0.6 treats it as REPLACE, which would # delete CONFIGURE_TOTP and lock out every platform-admin); the enrolment action is # enabled but NOT a defaultAction, so enrolment stays opt-in; the reconciler carries # no ServiceAccount token, mounts only what it needs, passes no mode flag, and # contains no dollar sign; the endpoint allow-list contains NO delete, NO # single-execution or config-edit write, and NO required-action write for any alias # other than the one named literally; the protected-subtree guard exists, is wired # into the single request choke point and is fail-closed; the SIBLING client-secret # reconciler's allow-list is NOT widened; and the reconciler's own `--selftest` # (extracted from the shipped ConfigMap and executed) proves compare-then-write, # idempotence, strict read-modify-write, the RpEntityName gate, the bound flow and # its sub-flows unwritable, the git bind switch and a rollback that sticks, the # conditional-OTP regression and fail-closed — with mutations.py proving that # selftest can FAIL. # # WHY THE FLOW IS BUILT BLUE/GREEN AND BOUND ONLY ON GIT'S SAY-SO, asserted here rather # than only argued: converging a five-flow, ten-execution restructure needs # create-subflow, create-execution, set-requirement and bind-config as SEPARATE admin API # calls with no transaction. Doing that to the BOUND flow means a run that dies between # them leaves a HALF-BUILT FLOW AS THE LOGIN PATH for Grafana, Gitea, Nextcloud, Headlamp, # Semaphore, PiHole, Crafty, the *arr stack AND kubectl — this repo has had a total SSO # outage from a realm change (2026-09-06), and the conditional-OTP sub-flow once 403'd # every platform-admin. So every multi-call write lands on a NEW, unused candidate flow, # which is verified by readback before anything is bound. Binding it is then ONE atomic # PUT of browserFlow, made only when passkey-desired-state.yaml sets # bind-browser-flow: "true" (default "false"): an untested login flow never goes live on # a schedule tick, and with the switch off an operator's rollback is never undone. # # STATUS: EXECUTABLE. `check.sh` is a pure offline assertion script (git + PyYAML + # stdlib); conformance/run.sh --layer l0 and ci/l0.sh run it directly, and this Chainsaw # Test wraps the SAME script. # # NOT proven here: that the LIVE realm's policy, enrolment action or browser flow match # git (that is cluster state — the reconciler's `--audit` mode is the read-only probe for # it, and NO L2 test is added by this change, so the live realm is UNVERIFIED by CI); # that a passkey ceremony actually completes or that the ALTERNATIVE fork degrades to the # password form (needs a real browser against a real realm); that the five flow-building # admin-API calls behave against a live Keycloak the way the 26.0.6 source says they do # (every proof here is against an in-process stub); or that the CronJob's schedule fires. # ============================================================================= apiVersion: chainsaw.kyverno.io/v1alpha1 kind: Test metadata: name: keycloak-passkey-reconciler labels: conformance.home.k3s/R-IDP-03: "true" conformance.home.k3s/R-IDP-04: "true" annotations: conformance.home.k3s/requirements: "R-IDP-03, R-IDP-04" conformance.home.k3s/layer: "L0" # WHOLE-REPO SCOPE, for the same reason the sibling keycloak-client-secret-reconciler # check declares it: this invariant spans platform/ (the realm import, the reconciler, # the generated desired state), platform-config/ (which Flux Kustomization renders # them, and therefore which substitution pass they share), ci/ (the renderer) and # clusters/ (that both are reachable from both entrypoints). A change that breaks it # need not attribute R-IDP-03/04 at all, and the requirement-scoped selection would # then filter this check out of exactly the change that introduces the bug — which is # how 12 unusable prod OIDC clients sat behind an entirely green cluster on # 2026-09-06. conformance.home.k3s/scope: "whole-repo" # Not safety-critical in the harness's sense (it mutates nothing and needs no # cluster), but it guards a component that holds WRITE access to the identity # provider's realm settings. conformance.home.k3s/safety-critical: "false" conformance.home.k3s/executable: "true" conformance.home.k3s/intent: >- The realm's passkey configuration — policy, enrolment action AND browser login flow — is declared once in the realm import, derived from it by a gated renderer, and continuously reconciled; the flow converges blue/green, so a one-shot import that never re-runs can neither leave the configuration inert nor let a reconciler half-rewrite the login path of the whole platform. spec: steps: - name: audit-passkey-declaration-renderer-and-reconciler description: >- Assert the desired state is derived from the realm import and shares its substitution pass, that the reconciler cannot write the bound flow or delete anything and binds only on git's bind-browser-flow, that the sibling reconciler is unwidened, and run the reconciler's own selftest plus the mutation suite that proves it can fail. try: - script: content: bash ./check.sh