# services/keycloak/oneoffs/veles-realm-ensure-job.yaml # One-off job for sso/veles-realm-ensure-4. # Purpose: create the Veles realm, groups, OIDC client, SMTP settings, and Vault client secret. # Keep suspended until Veles Vault paths/policies have reconciled, then unsuspend once. apiVersion: batch/v1 kind: Job metadata: name: veles-realm-ensure-4 namespace: sso spec: suspend: true backoffLimit: 0 ttlSecondsAfterFinished: 3600 template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/role: "sso-secrets" vault.hashicorp.com/agent-inject-secret-keycloak-admin-env.sh: "kv/data/atlas/shared/keycloak-admin" vault.hashicorp.com/agent-inject-template-keycloak-admin-env.sh: | {{ with secret "kv/data/atlas/shared/keycloak-admin" }} export KEYCLOAK_ADMIN="{{ .Data.data.username }}" export KEYCLOAK_ADMIN_USER="{{ .Data.data.username }}" export KEYCLOAK_ADMIN_PASSWORD="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} export KEYCLOAK_SMTP_USER="{{ index .Data.data "apikey" }}" export KEYCLOAK_SMTP_PASSWORD="{{ index .Data.data "apikey" }}" {{ end }} spec: serviceAccountName: mas-secrets-ensure restartPolicy: Never affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role.kubernetes.io/worker operator: Exists preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: kubernetes.io/arch operator: In values: ["arm64"] containers: - name: configure image: python:3.11-alpine env: - name: KEYCLOAK_SERVER value: http://keycloak.sso.svc.cluster.local - name: KEYCLOAK_REALM value: veles - name: KEYCLOAK_CLIENT_ID value: veles-web - name: KEYCLOAK_PUBLIC_ISSUER value: https://sso.bstein.dev/realms/veles - name: VELES_BASE_URL value: https://veles.bstein.dev - name: KEYCLOAK_SMTP_HOST value: mail.bstein.dev - name: KEYCLOAK_SMTP_PORT value: "587" - name: KEYCLOAK_SMTP_FROM value: no-reply-veles@bstein.dev - name: KEYCLOAK_SMTP_FROM_NAME value: Veles command: ["/bin/sh", "-c"] args: - | set -eu . /vault/secrets/keycloak-admin-env.sh python - <<'PY' import json import os import time import urllib.error import urllib.parse import urllib.request base_url = os.environ["KEYCLOAK_SERVER"].rstrip("/") realm = os.environ["KEYCLOAK_REALM"] client_id = os.environ["KEYCLOAK_CLIENT_ID"] issuer = os.environ["KEYCLOAK_PUBLIC_ISSUER"] veles_base_url = os.environ["VELES_BASE_URL"].rstrip("/") admin_user = os.environ["KEYCLOAK_ADMIN_USER"] admin_password = os.environ["KEYCLOAK_ADMIN_PASSWORD"] def request(method, url, token=None, payload=None, headers=None, timeout=30): data = None req_headers = headers.copy() if headers else {} if token: req_headers["Authorization"] = f"Bearer {token}" if payload is not None: data = json.dumps(payload).encode() req_headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=req_headers, method=method) try: with urllib.request.urlopen(req, timeout=timeout) 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_body = None form = urllib.parse.urlencode( { "grant_type": "password", "client_id": "admin-cli", "username": admin_user, "password": admin_password, } ).encode() for attempt in range(1, 11): req = urllib.request.Request( f"{base_url}/realms/master/protocol/openid-connect/token", data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=10) as resp: token_body = json.loads(resp.read().decode()) break except urllib.error.URLError as exc: if attempt == 10: raise SystemExit(f"Keycloak token request failed after retries: {exc}") time.sleep(attempt * 2) token = token_body["access_token"] smtp = { "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_FROM"], "replyToDisplayName": os.environ["KEYCLOAK_SMTP_FROM_NAME"], "user": os.environ["KEYCLOAK_SMTP_USER"], "password": os.environ["KEYCLOAK_SMTP_PASSWORD"], "auth": "true", "starttls": "true", "ssl": "false", } status, realm_rep = request("GET", f"{base_url}/admin/realms/{realm}", token) if status == 404: create_payload = { "realm": realm, "enabled": True, "registrationAllowed": True, "resetPasswordAllowed": True, "verifyEmail": True, "loginWithEmailAllowed": True, "duplicateEmailsAllowed": False, "smtpServer": smtp, } status, body = request("POST", f"{base_url}/admin/realms", token, create_payload) if status not in (201, 204, 409): raise SystemExit(f"Realm create failed: status={status} body={body}") status, realm_rep = request("GET", f"{base_url}/admin/realms/{realm}", token) if status != 200 or not isinstance(realm_rep, dict): raise SystemExit(f"Realm fetch failed: status={status}") realm_rep.update( { "enabled": True, "registrationAllowed": True, "resetPasswordAllowed": True, "verifyEmail": True, "loginWithEmailAllowed": True, "duplicateEmailsAllowed": False, "smtpServer": smtp, } ) status, body = request("PUT", f"{base_url}/admin/realms/{realm}", token, realm_rep) if status not in (200, 204): raise SystemExit(f"Realm update failed: status={status} body={body}") def ensure_group(name): status, groups = request( "GET", f"{base_url}/admin/realms/{realm}/groups?search={urllib.parse.quote(name)}", token, ) if status != 200: raise SystemExit(f"Group search failed for {name}: status={status}") for group in groups or []: if group.get("name") == name: return group["id"] status, body = request("POST", f"{base_url}/admin/realms/{realm}/groups", token, {"name": name}) if status not in (201, 204, 409): raise SystemExit(f"Group create failed for {name}: status={status} body={body}") status, groups = request( "GET", f"{base_url}/admin/realms/{realm}/groups?search={urllib.parse.quote(name)}", token, ) if status != 200: raise SystemExit(f"Group lookup failed after create for {name}: status={status}") for group in groups or []: if group.get("name") == name: return group["id"] raise SystemExit(f"Group {name} not found after create") def ensure_role(name): status, role = request("GET", f"{base_url}/admin/realms/{realm}/roles/{urllib.parse.quote(name)}", token) if status == 404: status, body = request("POST", f"{base_url}/admin/realms/{realm}/roles", token, {"name": name}) if status not in (201, 204, 409): raise SystemExit(f"Role create failed for {name}: status={status} body={body}") status, role = request( "GET", f"{base_url}/admin/realms/{realm}/roles/{urllib.parse.quote(name)}", token, ) if status != 200 or not isinstance(role, dict): raise SystemExit(f"Role lookup failed for {name}: status={status}") return role def ensure_group_role(group_id, role): status, mappings = request( "GET", f"{base_url}/admin/realms/{realm}/groups/{group_id}/role-mappings/realm", token, ) if status != 200: raise SystemExit(f"Group role mapping lookup failed: status={status}") if any(mapping.get("name") == role["name"] for mapping in mappings or []): return status, body = request( "POST", f"{base_url}/admin/realms/{realm}/groups/{group_id}/role-mappings/realm", token, [role], ) if status not in (200, 204): raise SystemExit(f"Group role mapping failed for {role['name']}: status={status} body={body}") def ensure_default_group(group_id, name): status, groups = request("GET", f"{base_url}/admin/realms/{realm}/default-groups", token) if status != 200: raise SystemExit(f"Default group lookup failed: status={status}") for group in groups or []: if group.get("id") == group_id or group.get("name") == name: return status, body = request("PUT", f"{base_url}/admin/realms/{realm}/default-groups/{group_id}", token) if status not in (200, 204): raise SystemExit(f"Default group update failed for {name}: status={status} body={body}") alpha_group_id = ensure_group("alpha") admin_group_id = ensure_group("admin") alpha_role = ensure_role("alpha") admin_role = ensure_role("admin") ensure_group_role(alpha_group_id, alpha_role) ensure_group_role(admin_group_id, alpha_role) ensure_group_role(admin_group_id, admin_role) ensure_default_group(alpha_group_id, "alpha") status, clients = request( "GET", f"{base_url}/admin/realms/{realm}/clients?clientId={urllib.parse.quote(client_id)}", token, ) if status != 200: raise SystemExit(f"Client lookup failed: status={status}") client_uuid = clients[0]["id"] if clients else None client_payload = { "clientId": client_id, "enabled": True, "protocol": "openid-connect", "publicClient": False, "standardFlowEnabled": True, "implicitFlowEnabled": False, "directAccessGrantsEnabled": False, "serviceAccountsEnabled": False, "redirectUris": [f"{veles_base_url}/*"], "webOrigins": [veles_base_url], "rootUrl": veles_base_url, "baseUrl": "/", "attributes": { "pkce.code.challenge.method": "S256", "post.logout.redirect.uris": f"{veles_base_url}/*", }, } if not client_uuid: status, body = request("POST", f"{base_url}/admin/realms/{realm}/clients", token, client_payload) if status not in (201, 204, 409): raise SystemExit(f"Client create failed: status={status} body={body}") status, clients = request( "GET", f"{base_url}/admin/realms/{realm}/clients?clientId={urllib.parse.quote(client_id)}", token, ) client_uuid = clients[0]["id"] if clients else None if not client_uuid: raise SystemExit("Client veles-web not found after create") status, body = request( "PUT", f"{base_url}/admin/realms/{realm}/clients/{client_uuid}", token, client_payload, ) if status not in (200, 204): raise SystemExit(f"Client update failed: status={status} body={body}") mapper_payload = { "name": "groups", "protocol": "openid-connect", "protocolMapper": "oidc-group-membership-mapper", "consentRequired": False, "config": { "full.path": "false", "id.token.claim": "true", "access.token.claim": "true", "userinfo.token.claim": "true", "claim.name": "groups", "jsonType.label": "String", }, } status, mappers = request( "GET", f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/protocol-mappers/models", token, ) if status != 200: raise SystemExit(f"Mapper lookup failed: status={status}") mapper_id = next((mapper.get("id") for mapper in mappers or [] if mapper.get("name") == "groups"), None) if mapper_id: mapper_payload["id"] = mapper_id status, body = request( "PUT", f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/protocol-mappers/models/{mapper_id}", token, mapper_payload, ) else: status, body = request( "POST", f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/protocol-mappers/models", token, mapper_payload, ) if status not in (200, 201, 204): raise SystemExit(f"Mapper ensure failed: status={status} body={body}") status, secret = request( "GET", f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/client-secret", token, ) client_secret = (secret or {}).get("value") if status != 200 or not client_secret: raise SystemExit(f"Client secret fetch failed: status={status}") vault_addr = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200") jwt = open("/var/run/secrets/kubernetes.io/serviceaccount/token", encoding="utf-8").read().strip() login_payload = json.dumps({"jwt": jwt, "role": os.environ.get("VAULT_ROLE", "sso-secrets")}).encode() req = urllib.request.Request( f"{vault_addr}/v1/auth/kubernetes/login", data=login_payload, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=20) as resp: vault_token = json.loads(resp.read().decode())["auth"]["client_token"] payload = { "data": { "client_id": client_id, "client_secret": client_secret, "issuer": issuer, "realm": realm, "required_groups": "alpha,admin", } } req = urllib.request.Request( f"{vault_addr}/v1/kv/data/atlas/veles/veles-oidc", data=json.dumps(payload).encode(), headers={"X-Vault-Token": vault_token, "Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=20) as resp: if resp.status not in (200, 204): raise SystemExit(f"Vault write returned {resp.status}") print("Veles Keycloak realm/client ready") PY