From 3f93ddc0028b81d802acb85734e968c552411db4 Mon Sep 17 00:00:00 2001 From: rock Date: Mon, 26 Oct 2026 23:12:53 -0700 Subject: [PATCH] feat(iam): provision the built-in login page loginV2.required lives in the instance database, not in chart values, so disabling the hosted Login UI in values is only half the change: the server kept redirecting /oauth/v2/authorize to /ui/v2/login, which no longer had a backend and returned 503. Set loginV2.required=false in the provisioning run so the server serves its own login page. Verified: authorize now redirects to /ui/login/login and the page renders the "Welcome Back!" login form. --- scripts/iam/zitadel-provision.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/iam/zitadel-provision.py b/scripts/iam/zitadel-provision.py index 8ecccc0..eca1409 100755 --- a/scripts/iam/zitadel-provision.py +++ b/scripts/iam/zitadel-provision.py @@ -299,6 +299,24 @@ def _ensure_project(token: str, name: str) -> str: return project_id +def _ensure_builtin_login(token: str) -> None: + """Force the built-in login page instead of the hosted Login UI. + + The hosted UI authenticates as the machine user `login-client`, which the + chart creates only from its one-shot setup job. When that user is missing, + every session write fails with "Could not create session for user" and no + one can sign in, at ZITADEL or at any relying party. + + Disabling the loginV2 feature makes the server serve its own login page, + which needs no machine user. This is instance state in the database, not a + values key, so it must be applied explicitly. + """ + code, d = _api("PUT", "/v2/features/instance", token, {"loginV2": {"required": False}}) + if code not in (200, 201): + die(f"disable loginV2 failed: {code} {d}") + print("login page : built-in (loginV2.required=false)") + + def _ensure_roles(token: str, project_id: str, roles: dict[str, str]) -> None: code, d = _api("POST", f"/management/v1/projects/{project_id}/roles/_search", token, {}) have = {r.get("roleKey") for r in (d.get("result") or [])} @@ -356,6 +374,7 @@ def phase_apply(_args: argparse.Namespace) -> int: if not token: die("no iam-admin PAT — run the keys phase first") + _ensure_builtin_login(token) project_id = _ensure_project(token, PROJECT_NAME) print(f"project : {PROJECT_NAME} ({project_id})") _ensure_roles(token, project_id, ROLES)