--- # Identity config as code (R-IDP-03): the `homelab` realm — groups, roles, OIDC clients, brute-force # hardening, and a conditional MFA flow that REQUIRES OTP for platform-admins (R-IDP-04). Reconciled # declaratively by the Keycloak Operator; CARRIED BY platform-config (dependsOn platform). # # Client secrets are NOT in this file: they are injected from the SOPS-encrypted keycloak-realm-clients # Secret via spec.placeholders ($(env:...)) — so secrets exist only encrypted (R-IDP-04; SOPS, docs/07). # Until that Secret is filled (operator hand-off, see keycloak-realm-clients.sops.yaml.example), this # import stays PENDING on the unresolved placeholder. apiVersion: k8s.keycloak.org/v2alpha1 kind: KeycloakRealmImport metadata: name: homelab namespace: keycloak spec: keycloakCRName: keycloak placeholders: GRAFANA_CLIENT_SECRET: secret: name: keycloak-realm-clients key: grafana-client-secret OAUTH2_PROXY_CLIENT_SECRET: secret: name: keycloak-realm-clients key: oauth2-proxy-client-secret realm: realm: homelab enabled: true displayName: "Homelab SSO" loginWithEmailAllowed: true registrationAllowed: false resetPasswordAllowed: true # Hardening (docs/15): brute-force detection ON; SSL required for external (edge-TLS) requests. bruteForceProtected: true permanentLockout: false sslRequired: external # --- Authorization model (R-IDP-06): groups drive access; platform-admins carry a realm role. --- roles: realm: - name: platform-admin description: "Privileged platform administrators — MFA required (R-IDP-04)." groups: - name: platform-admins realmRoles: - platform-admin - name: media-users - name: viewers # --- OIDC clients (native SSO). Group membership is exposed as a `groups` claim so apps and # oauth2-proxy can authorize by group (R-IDP-06). --- clients: # Grafana — the concrete native-OIDC SSO proof target (docs/10 / deliverable #4). - clientId: grafana name: Grafana protocol: openid-connect enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false secret: "$(env:GRAFANA_CLIENT_SECRET)" redirectUris: - https://grafana.home.keithjackson.dev/login/generic_oauth webOrigins: - https://grafana.home.keithjackson.dev defaultClientScopes: - email - profile - roles protocolMappers: - name: groups protocol: openid-connect protocolMapper: oidc-group-membership-mapper config: "full.path": "false" "id.token.claim": "true" "access.token.claim": "true" "userinfo.token.claim": "true" "claim.name": groups # oauth2-proxy — forward-auth for non-OIDC UIs (PiHole, *arr, Prometheus, Alertmanager, …). - clientId: oauth2-proxy name: oauth2-proxy (forward-auth) protocol: openid-connect enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false secret: "$(env:OAUTH2_PROXY_CLIENT_SECRET)" redirectUris: - https://*.home.keithjackson.dev/oauth2/callback - https://auth.home.keithjackson.dev/oauth2/callback webOrigins: - https://*.home.keithjackson.dev defaultClientScopes: - email - profile - roles protocolMappers: - name: groups protocol: openid-connect protocolMapper: oidc-group-membership-mapper config: "full.path": "false" "id.token.claim": "true" "access.token.claim": "true" "userinfo.token.claim": "true" "claim.name": groups # kubernetes — kubectl OIDC login via kubelogin (int128/kubelogin `oidc-login`). PUBLIC client + # PKCE (S256) so NO client secret lives in any user's kubeconfig; kubelogin runs the auth-code # +PKCE flow against a localhost callback and caches the ID token. The k3s API server validates # that ID token (kube-apiserver oidc-issuer-url=this realm, oidc-client-id=kubernetes, # oidc-username-claim=email, oidc-groups-claim=groups); RBAC binds the `groups` claim — see # k8s-oidc-rbac.yaml (oidc:platform-admins -> cluster-admin, oidc:viewers -> view). The cert # kubeconfig remains the break-glass admin, independent of Keycloak/OIDC availability. - clientId: kubernetes name: Kubernetes (kubectl) protocol: openid-connect enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false attributes: "pkce.code.challenge.method": "S256" redirectUris: - http://localhost:8000 - http://localhost:8000/ - http://localhost:18000 - http://localhost:18000/ - http://127.0.0.1:8000 - http://127.0.0.1:8000/ - http://127.0.0.1:18000 - http://127.0.0.1:18000/ webOrigins: - http://localhost - http://127.0.0.1 defaultClientScopes: - email - profile - roles protocolMappers: - name: groups protocol: openid-connect protocolMapper: oidc-group-membership-mapper config: "full.path": "false" "id.token.claim": "true" "access.token.claim": "true" "userinfo.token.claim": "true" "claim.name": groups # --- MFA for platform-admins (R-IDP-04): a browser flow whose OTP step is CONDITIONAL on the # platform-admin role. Non-admins log in without OTP; platform-admins must enroll/present TOTP. # Set as the realm browserFlow below. VERIFY at bring-up (see runbook): a malformed flow fails # the whole import — if so, the fallback is to comment this block + browserFlow and configure # the conditional-OTP-by-role flow via the admin console. --- authenticationFlows: - alias: browser-mfa description: "Browser flow with conditional OTP required for platform-admins." providerId: basic-flow topLevel: true builtIn: false authenticationExecutions: - authenticator: auth-cookie authenticatorFlow: false requirement: ALTERNATIVE priority: 10 userSetupAllowed: false - authenticator: identity-provider-redirector authenticatorFlow: false requirement: ALTERNATIVE priority: 20 userSetupAllowed: false - authenticatorFlow: true requirement: ALTERNATIVE priority: 30 flowAlias: browser-mfa-forms userSetupAllowed: false - alias: browser-mfa-forms description: "Username/password then conditional OTP." providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: auth-username-password-form authenticatorFlow: false requirement: REQUIRED priority: 10 userSetupAllowed: false - authenticatorFlow: true requirement: CONDITIONAL priority: 20 flowAlias: browser-mfa-conditional-otp userSetupAllowed: false - alias: browser-mfa-conditional-otp description: "Require OTP only for users with the platform-admin role." providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-user-role authenticatorFlow: false requirement: REQUIRED priority: 10 authenticatorConfig: conditional-otp-platform-admin userSetupAllowed: false - authenticator: auth-otp-form authenticatorFlow: false requirement: REQUIRED priority: 20 userSetupAllowed: false authenticatorConfig: - alias: conditional-otp-platform-admin config: "condition.evaluated.user.role": platform-admin negate: "false" browserFlow: browser-mfa