--- # ============================================================================= # GENERATED FILE — DO NOT EDIT BY HAND, except the one bind-browser-flow line below. # # source: platform/keycloak/keycloak-realm-import.yaml (spec.realm) # regenerate: python3 ci/render-keycloak-passkey-policy.py --write # gated by: conformance/tests/l0-static/keycloak-passkey-reconciler # (runs this renderer with --check on every merge) # # WHAT THIS IS: the DESIRED STATE for the `homelab` realm's PASSKEY configuration — # the WebAuthn passwordless policy, the enrolment required action, and the declared # browser authentication flow tree. platform/keycloak/passkey-reconciler.yaml mounts # it and (a) converges the policy and the required action onto it, (b) builds the # declared flow tree BLUE/GREEN under a new alias and verifies it by readback, never # writing the flow users are logging in through, and (c) BINDS that verified flow as # the realm's browserFlow ONLY when `bind-browser-flow` below is "true". # # EVERYTHING HERE IS GENERATED EXCEPT ONE LINE: `bind-browser-flow`. That is an # operator decision, carried through by the renderer from this committed file (see # ci/render-keycloak-passkey-policy.py). It defaults to "false". # # WHY IT IS A SEPARATE OBJECT: the realm import is a ONE-SHOT that never re-runs # once Done=True, so a realm-level field or a flow edited in git never reaches the # realm. Measured on prod 2026-10-18, hours after the passkey declaration merged: # Done=True / Started=False, with the whole passkey block inert. The reconciler is # the level-triggered loop that closes that; this file is what it reconciles # TOWARDS. # # WHY IT IS GENERATED AND NOT AUTHORED: a hand-written copy of the RP ID, the # user-verification requirement and the flow tree would be a second source of truth, # free to disagree with the realm import — the same "two declarations that can # disagree" shape as the bug being closed. It is rendered from the import and the # render is gated, so the two cannot drift. # # STRINGS ARE VERBATIM: webAuthnPolicyPasswordlessRpId is the ${BASE_DOMAIN} # substitution token exactly as the realm import writes it. Both files are rendered # by the SAME Flux Kustomization (./platform-config), so the token resolves to the # same value in both and the domain is decided in exactly one place. # # `requiredAction` is the ONE entry that is DERIVED rather than copied: the realm # import deliberately declares no `requiredActions` block, because Keycloak 26.0.6 # treats that as REPLACE and declaring one action would delete CONFIGURE_TOTP and # nine more (a hard lock-out of every platform-admin). It is derived instead from # the presence of a webauthn-authenticator-passwordless execution in the flow: # passkeys are offered, therefore enrolment must be enabled — and defaultAction is # false, so enrolment stays opt-in. # # A field NOT declared on the realm in the import is simply ABSENT here, and the # reconciler leaves it alone. Absence means UNMANAGED, never "force to a default". # ============================================================================= apiVersion: v1 kind: ConfigMap metadata: name: keycloak-passkey-desired-state namespace: keycloak labels: app.kubernetes.io/name: keycloak-passkey-desired-state app.kubernetes.io/part-of: keycloak data: # --------------------------------------------------------------------------- # THE BIND SWITCH -- the ONLY hand-edited line in this file. Exactly "true" or # "false", QUOTED. # # "false" the reconciler builds and verifies the candidate browser flow and # logs `WOULD BIND `, but NEVER changes the realm's browserFlow. # Whatever the realm is bound to -- including a rollback -- is left alone. # "true" it binds the verified candidate in one atomic PUT, and keeps the # realm bound to it: a binding changed by hand is changed back. # # GO LIVE: test the candidate in a browser first (platform/keycloak/README.md, # "Passkeys"), then a PR setting this to "true". # ROLL BACK: a PR setting this to "false" FIRST (or the next run rebinds), then # one write setting browserFlow back to the previous alias from the Job log. # --------------------------------------------------------------------------- bind-browser-flow: "false" passkey-desired-state.json: | { "authenticatorConfig": { "conditional-otp-platform-admin": { "condUserRole": "platform-admin", "negate": "false" } }, "browserFlow": "browser-mfa", "flows": { "browser-mfa": { "executions": [ { "kind": "authenticator", "name": "auth-cookie", "requirement": "ALTERNATIVE" }, { "kind": "authenticator", "name": "identity-provider-redirector", "requirement": "ALTERNATIVE" }, { "kind": "flow", "name": "browser-mfa-forms", "requirement": "ALTERNATIVE" } ], "topLevel": true }, "browser-mfa-conditional-otp": { "executions": [ { "configAlias": "conditional-otp-platform-admin", "kind": "authenticator", "name": "conditional-user-role", "requirement": "REQUIRED" }, { "kind": "authenticator", "name": "auth-otp-form", "requirement": "REQUIRED" } ], "topLevel": false }, "browser-mfa-forms": { "executions": [ { "kind": "authenticator", "name": "auth-username-form", "requirement": "REQUIRED" }, { "kind": "flow", "name": "browser-mfa-passkey-or-password", "requirement": "REQUIRED" } ], "topLevel": false }, "browser-mfa-passkey-or-password": { "executions": [ { "kind": "authenticator", "name": "webauthn-authenticator-passwordless", "requirement": "ALTERNATIVE" }, { "kind": "flow", "name": "browser-mfa-password-and-otp", "requirement": "ALTERNATIVE" } ], "topLevel": false }, "browser-mfa-password-and-otp": { "executions": [ { "kind": "authenticator", "name": "auth-password-form", "requirement": "REQUIRED" }, { "kind": "flow", "name": "browser-mfa-conditional-otp", "requirement": "CONDITIONAL" } ], "topLevel": false } }, "realm": "homelab", "requiredAction": { "alias": "webauthn-register-passwordless", "defaultAction": false, "enabled": true }, "webAuthnPolicyPasswordless": { "webAuthnPolicyPasswordlessAcceptableAaguids": [], "webAuthnPolicyPasswordlessAttestationConveyancePreference": "none", "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": true, "webAuthnPolicyPasswordlessCreateTimeout": 300, "webAuthnPolicyPasswordlessRequireResidentKey": "Yes", "webAuthnPolicyPasswordlessRpEntityName": "Homelab SSO", "webAuthnPolicyPasswordlessRpId": "${BASE_DOMAIN}", "webAuthnPolicyPasswordlessSignatureAlgorithms": [ "ES256", "RS256" ], "webAuthnPolicyPasswordlessUserVerificationRequirement": "required" } }