From 88fda300084ce6ea2493867d0f6f9c0c97fe5592 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 18 Sep 2026 16:30:38 +0200 Subject: [PATCH] test(k8s): pin the issuer, the portal authority and the public edge (refs #177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keycloak pins one issuer and each portal is configured with one authority; when they drift the symptom lands three services away — a login that bounces back logged out, or a 401 from the BFF (ADR-0010) — so assert they are the same string. The same check states what publishing the stack has to mean: with `public.domain` set the five hostnames are served and both halves become `https://auth.`, and with it empty nothing of the edge renders, which is what compose, CI and a laptop cluster depend on. Red: the chart has no `public.domain`, so setting it changes nothing and no hostname is published. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 1 + infra/helm/check-issuer.py | 80 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 infra/helm/check-issuer.py diff --git a/Makefile b/Makefile index 9c76f3b..afd1810 100644 --- a/Makefile +++ b/Makefile @@ -352,6 +352,7 @@ K8S_IMAGES := acl domain bff event-subscriber projection-api self-service open k8s-lint: helm lint $(K8S_CHART) helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null + python3 infra/helm/check-issuer.py ## k8s-drift: fail if compose and the Helm chart describe different stacks # Compose is CI-canonical (ADR-0033) and the chart is a transcription of it; this diff --git a/infra/helm/check-issuer.py b/infra/helm/check-issuer.py new file mode 100644 index 0000000..efc97e4 --- /dev/null +++ b/infra/helm/check-issuer.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Fail when the pinned issuer and the portals' OIDC authority stop agreeing. + +Keycloak pins one issuer (`KC_HOSTNAME`) and each portal is configured with one +authority (`config.json`). A browser token carries the first; the BFF validates +against what it discovers from the second (ADR-0010). When the two drift the +symptom is three services away — a login that bounces back logged out, or a 401 +from the BFF — so the chart builds both from one helper and this asserts it. + +It also pins the two halves of the public edge (ADR-0035): that setting +`public.domain` actually publishes the hostnames, and that leaving it empty +renders no edge at all, which is what compose, CI and a laptop cluster rely on. + +Run it with `make k8s-lint`. No cluster needed. +""" + +import json +import re +import subprocess +import sys +from pathlib import Path + +CHART = Path(__file__).resolve().parent / "big-reference" +DOMAIN = "example.test" + + +def render(*sets: str) -> str: + argv = ["helm", "template", "big", str(CHART), "-n", "big"] + for s in sets: + argv += ["--set", s] + proc = subprocess.run(argv, capture_output=True, text=True) + if proc.returncode != 0: + sys.exit(f"helm template failed:\n{proc.stderr}") + return proc.stdout + + +def issuer(out: str) -> str: + """The value of KC_HOSTNAME in the rendered manifests.""" + m = re.search(r"name: KC_HOSTNAME\n\s+value: \"(\S+)\"", out) + return m[1] if m else "" + + +def authorities(out: str) -> set[str]: + """Every portal's OIDC authority, with the realm path stripped.""" + found = set() + for line in re.findall(r'\{ "authority": .* \}', out): + url = json.loads(line)["authority"] + found.add(url.rsplit("/realms/", 1)[0]) + return found + + +def main() -> int: + problems = [] + + public = render(f"public.domain={DOMAIN}") + if issuer(public) != f"https://auth.{DOMAIN}": + problems.append(f" with public.domain set, KC_HOSTNAME is {issuer(public)!r}, not https://auth.{DOMAIN}") + if authorities(public) != {f"https://auth.{DOMAIN}"}: + problems.append(f" with public.domain set, the portals point at {sorted(authorities(public))}") + for host in (f"register.{DOMAIN}", f"mijn.{DOMAIN}", f"behandel.{DOMAIN}", f"beheer.{DOMAIN}", f"auth.{DOMAIN}"): + if host not in public: + problems.append(f" {host} is not published by the edge") + + private = render() + if authorities(private) != {issuer(private)}: + problems.append(f" by default the portals point at {sorted(authorities(private))}, the issuer is {issuer(private)!r}") + if "caddy-edge" in private: + problems.append(" the edge renders with no public.domain — compose, CI and a laptop cluster expect nothing") + + if problems: + print("the chart's OIDC origin is inconsistent:\n" + "\n".join(problems)) + print("\nBoth halves come from the `big.keycloakUrl` helper — change it, not one caller.") + return 1 + + print(f"issuer + portal authority agree, with and without a public domain") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())