# services/keycloak/realm-settings-job.yaml apiVersion: batch/v1 kind: Job metadata: name: keycloak-realm-settings-14 namespace: sso spec: backoffLimit: 0 template: spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: hardware operator: In values: ["rpi5","rpi4"] - key: node-role.kubernetes.io/worker operator: Exists restartPolicy: Never containers: - name: configure image: python:3.11-alpine env: - name: KEYCLOAK_SERVER value: http://keycloak.sso.svc.cluster.local - name: KEYCLOAK_REALM value: atlas - name: KEYCLOAK_ADMIN_USER valueFrom: secretKeyRef: name: keycloak-admin key: username - name: KEYCLOAK_ADMIN_PASSWORD valueFrom: secretKeyRef: name: keycloak-admin key: password - name: KEYCLOAK_SMTP_HOST value: mailu-front.mailu-mailserver.svc.cluster.local - name: KEYCLOAK_SMTP_PORT value: "25" - name: KEYCLOAK_SMTP_FROM value: no-reply@bstein.dev - name: KEYCLOAK_SMTP_FROM_NAME value: Atlas SSO - name: KEYCLOAK_SMTP_REPLY_TO value: no-reply@bstein.dev - name: KEYCLOAK_SMTP_REPLY_TO_NAME value: Atlas SSO command: ["/bin/sh", "-c"] args: - | set -euo pipefail python - <<'PY' import json import os import urllib.parse import urllib.error import urllib.request base_url = os.environ["KEYCLOAK_SERVER"].rstrip("/") realm = os.environ["KEYCLOAK_REALM"] admin_user = os.environ["KEYCLOAK_ADMIN_USER"] admin_password = os.environ["KEYCLOAK_ADMIN_PASSWORD"] def http_json(method: str, url: str, token: str, payload=None): data = None headers = {"Authorization": f"Bearer {token}"} if payload is not None: data = json.dumps(payload).encode() headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=30) as resp: body = resp.read() if not body: return resp.status, None return resp.status, json.loads(body.decode()) except urllib.error.HTTPError as exc: raw = exc.read() if not raw: return exc.code, None try: return exc.code, json.loads(raw.decode()) except Exception: return exc.code, {"raw": raw.decode(errors="replace")} token_data = urllib.parse.urlencode( { "grant_type": "password", "client_id": "admin-cli", "username": admin_user, "password": admin_password, } ).encode() token_req = urllib.request.Request( f"{base_url}/realms/master/protocol/openid-connect/token", data=token_data, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST", ) try: with urllib.request.urlopen(token_req, timeout=10) as resp: token_body = json.loads(resp.read().decode()) except urllib.error.HTTPError as exc: body = exc.read().decode(errors="replace") raise SystemExit(f"Token request failed: status={exc.code} body={body}") access_token = token_body["access_token"] # Update realm settings safely by fetching the full realm representation first. realm_url = f"{base_url}/admin/realms/{realm}" status, realm_rep = http_json("GET", realm_url, access_token) if status != 200 or not realm_rep: raise SystemExit(f"Unable to fetch realm {realm} (status={status})") realm_rep["resetPasswordAllowed"] = True smtp = realm_rep.get("smtpServer") or {} smtp.update( { "host": os.environ["KEYCLOAK_SMTP_HOST"], "port": os.environ["KEYCLOAK_SMTP_PORT"], "from": os.environ["KEYCLOAK_SMTP_FROM"], "fromDisplayName": os.environ["KEYCLOAK_SMTP_FROM_NAME"], "replyTo": os.environ["KEYCLOAK_SMTP_REPLY_TO"], "replyToDisplayName": os.environ["KEYCLOAK_SMTP_REPLY_TO_NAME"], "auth": "false", "starttls": "false", "ssl": "false", } ) realm_rep["smtpServer"] = smtp status, _ = http_json("PUT", realm_url, access_token, realm_rep) if status not in (200, 204): raise SystemExit(f"Unexpected realm update response: {status}") # Ensure required custom user-profile attributes exist. profile_url = f"{base_url}/admin/realms/{realm}/users/profile" status, profile = http_json("GET", profile_url, access_token) if status == 200 and isinstance(profile, dict): attrs = profile.get("attributes") if not isinstance(attrs, list): attrs = [] required_attrs = [ { "name": "vaultwarden_email", "displayName": "Vaultwarden Email", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"email": {}, "length": {"max": 255}}, }, { "name": "vaultwarden_status", "displayName": "Vaultwarden Status", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"length": {"max": 64}}, }, { "name": "vaultwarden_synced_at", "displayName": "Vaultwarden Last Synced", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"length": {"max": 64}}, }, { "name": "mailu_email", "displayName": "Atlas Mailbox", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"email": {}, "length": {"max": 255}}, }, { "name": "mailu_app_password", "displayName": "Atlas Mail App Password", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"length": {"max": 255}}, }, { "name": "nextcloud_mail_primary_email", "displayName": "Nextcloud Mail Primary Email", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"email": {}, "length": {"max": 255}}, }, { "name": "nextcloud_mail_account_count", "displayName": "Nextcloud Mail Account Count", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"length": {"max": 32}}, }, { "name": "nextcloud_mail_synced_at", "displayName": "Nextcloud Mail Last Synced", "multivalued": False, "annotations": {"group": "user-metadata"}, "permissions": {"view": ["admin"], "edit": ["admin"]}, "validations": {"length": {"max": 64}}, }, ] def has_attr(name: str) -> bool: return any(isinstance(item, dict) and item.get("name") == name for item in attrs) updated = False for attr in required_attrs: if not has_attr(attr.get("name", "")): attrs.append(attr) updated = True if updated: profile["attributes"] = attrs status, _ = http_json("PUT", profile_url, access_token, profile) if status not in (200, 204): raise SystemExit(f"Unexpected user-profile update response: {status}") # Ensure basic realm groups exist for provisioning. for group_name in ("dev", "admin"): status, groups = http_json( "GET", f"{base_url}/admin/realms/{realm}/groups?search={urllib.parse.quote(group_name)}", access_token, ) exists = False if status == 200 and isinstance(groups, list): for item in groups: if isinstance(item, dict) and item.get("name") == group_name: exists = True break if exists: continue status, _ = http_json( "POST", f"{base_url}/admin/realms/{realm}/groups", access_token, {"name": group_name}, ) if status not in (201, 204): raise SystemExit(f"Unexpected group create response for {group_name}: {status}") # Ensure MFA is on by default for newly-created users. status, required_actions = http_json( "GET", f"{base_url}/admin/realms/{realm}/authentication/required-actions", access_token, ) if status == 200 and isinstance(required_actions, list): for action in required_actions: if not isinstance(action, dict): continue if action.get("alias") != "CONFIGURE_TOTP": continue if action.get("enabled") is True and action.get("defaultAction") is True: break action["enabled"] = True action["defaultAction"] = True status, _ = http_json( "PUT", f"{base_url}/admin/realms/{realm}/authentication/required-actions/CONFIGURE_TOTP", access_token, action, ) if status not in (200, 204): raise SystemExit( f"Unexpected required-action update response for CONFIGURE_TOTP: {status}" ) # Disable Identity Provider Redirector in the browser flow for this realm. status, executions = http_json( "GET", f"{base_url}/admin/realms/{realm}/authentication/flows/browser/executions", access_token, ) if status == 200 and executions: for ex in executions: if ex.get("providerId") != "identity-provider-redirector": continue if ex.get("requirement") == "DISABLED": continue ex["requirement"] = "DISABLED" status, _ = http_json( "PUT", f"{base_url}/admin/realms/{realm}/authentication/flows/browser/executions", access_token, ex, ) if status not in (200, 204): raise SystemExit( f"Unexpected execution update response for identity-provider-redirector: {status}" ) PY