From 804031eeb855947a22baeec06dfa2f0e3a8f543e Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 25 Sep 2026 08:16:37 +0000 Subject: [PATCH] feat(k8s): publish the portals through the labs Caddy (refs #177) (#179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & why Makes the portals reachable on real hostnames through the Caddy that already fronts `*.labs.respellion.tech`, instead of five SSH port-forwards: | URL | Service | |---|---| | `https://big-register.labs.respellion.tech` | openbaar | | `https://big-mijn.labs.respellion.tech` | self-service | | `https://big-behandel.labs.respellion.tech` | behandel | | `https://big-beheer.labs.respellion.tech` | beheer | | `https://big-auth.labs.respellion.tech` | Keycloak (`/admin` blocked) | Chain: browser → labs Caddy (TLS) → `openssh-server` container → reverse SSH tunnel → Fedora host → Talos NodePorts. The Caddy routes and the tunnel unit are already on `main` in the Infra repo (`infra/development/`). This repo's part: - **Chart:** a `keycloakUrl` value. When set it replaces `host` + Keycloak's NodePort as the pinned issuer (`KC_HOSTNAME`) and the portals' OIDC authority. Both now come from one helper, `big.keycloakUrl`, so they can't drift apart (ADR-0010). Empty = rendered output identical to today. - **Deploy workflow:** passes the `KEYCLOAK_URL` repo variable as `--set keycloakUrl=…`. - **Runbook:** new section "Publishing through the labs Caddy". Refs #177 ## Definition of Done - [x] Linked Gitea issue (above). - [ ] Failing test committed before the implementation. *(Infra/config change, no test added.)* - [x] Implementation makes the test pass; refactor commit if structure improved. - [x] Conventional Commits referencing the issue (`refs #NN`). - [ ] CI green — all Gitea Actions jobs (or `make ci` green while no runner exists). - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(Compose untouched.)* - [x] Docs updated if behaviour, contracts, or operations changed. - [ ] ADR added in `docs/architecture/` if a non-obvious decision was made. - [ ] Demo note in `docs/demo-script.md` if user-visible. ## Notes for reviewers - **This takes the option #177 rejects.** #177 proposes an in-cluster Caddy edge (branch `feat/177-public-tls-edge`). This PR uses the existing labs Caddy instead, because it already holds 80/443 and the wildcard certificate. So it only *refs* #177. If we go this way, #177's ADR should record the host-Caddy option instead. - `make k8s-lint` and `infra/check-docs-nav.py` pass. I rendered the chart with and without `keycloakUrl`: empty gives the same output as before; set, it gives `https://big-auth.labs.respellion.tech` for both the issuer and the authority. - Once `KEYCLOAK_URL` is set, the `localhost` port-forward workflow (runbook §5) no longer logs in, because the issuer is a single string. - The portals are public, with no Azure `authorize` in front of them the way `marketing` has one. The test users use `test123`. - Rollout after merge: install `big-portals-tunnel.service` on the Fedora host, run `docker compose up -d caddy` on the labs server, then set the `KEYCLOAK_URL` variable. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: https://git.labs.respellion.tech/eho/register-referentie/pulls/179 --- .gitea/workflows/deploy.yaml | 10 +++++- docs/runbooks/kubernetes-talos.md | 36 +++++++++++++++++++ .../helm/big-reference/templates/_helpers.tpl | 8 +++++ .../helm/big-reference/templates/config.yaml | 2 +- .../big-reference/templates/deployments.yaml | 2 +- infra/helm/big-reference/values.yaml | 7 +++- 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 8e9d5f6..1f9dfae 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -27,6 +27,10 @@ jobs: # `kubectl port-forward` — runbook §5. Override with repo variables. TALOS_VM_IP: ${{ vars.TALOS_VM_IP }} TALOS_HOST: ${{ vars.TALOS_HOST }} + # Set it when the labs Caddy publishes the portals: Keycloak's public https + # origin, e.g. https://big-auth.labs.respellion.tech (runbook, "Publishing + # through the labs Caddy"). + KEYCLOAK_URL: ${{ vars.KEYCLOAK_URL }} steps: - uses: https://github.com/actions/checkout@v4 @@ -89,7 +93,11 @@ jobs: # The jobs are idempotent, and deleting them first is what keeps a changed # Job template from wedging the upgrade (`cannot patch … with kind Job`). - name: Deploy the chart - run: make k8s-reseed TALOS_HOST=${TALOS_HOST:-localhost} K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 + run: | + make k8s-reseed \ + TALOS_HOST=${TALOS_HOST:-localhost} \ + K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \ + K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL}" # `dev` is a mutable tag and helm sees an unchanged pod template, so the # new images only land on a restart (pullPolicy is already Always). diff --git a/docs/runbooks/kubernetes-talos.md b/docs/runbooks/kubernetes-talos.md index cf73ea9..75b3c2b 100644 --- a/docs/runbooks/kubernetes-talos.md +++ b/docs/runbooks/kubernetes-talos.md @@ -401,6 +401,42 @@ Not covered: the portals still need `make k8s-portals` (or an SSH forward) to be browser, because PKCE needs a secure context (§5). Giving the server a hostname + TLS is the upgrade path. +## Publishing through the labs Caddy + +The portals can be reached on real hostnames through the Caddy that already fronts +`*.labs.respellion.tech` (repo `Infra`, `infra/development/`). The chain: + +``` +browser → Caddy (labs server, TLS) → openssh-server:3014x/30180 + → reverse SSH tunnel → Fedora host → :3014x/30180 (NodePorts) +``` + +| URL | NodePort | +|---|---| +| `https://big-register.labs.respellion.tech` | 30141 openbaar | +| `https://big-mijn.labs.respellion.tech` | 30140 self-service | +| `https://big-behandel.labs.respellion.tech` | 30142 behandel | +| `https://big-beheer.labs.respellion.tech` | 30143 beheer | +| `https://big-auth.labs.respellion.tech` | 30180 Keycloak (`/admin` blocked) | + +HTTPS makes the portals a secure context, so PKCE works without port-forwards — but +Keycloak's issuer must be the public origin. Deploy with it: + +```bash +make k8s-up TALOS_HOST=localhost K8S_REGISTRY=:30500 \ + K8S_SET="--set keycloakUrl=https://big-auth.labs.respellion.tech" +``` + +For deploy-on-merge, set the repository variable `KEYCLOAK_URL` to the same value. +With it set, the `localhost` port-forwards (§5) no longer log in: the issuer is one string. + +One-time setup: + +1. Fedora host: install `infra/development/big-portals-tunnel.service` from the Infra repo + (instructions in the file). +2. Labs server: deploy the Infra `Caddyfile` + `compose.yml` (Caddy joins the + `openssh_default` network to reach the tunnel ends). + ## What is not ported - **Observability** (Tempo, Prometheus, Grafana) is defined but disabled — those are built diff --git a/infra/helm/big-reference/templates/_helpers.tpl b/infra/helm/big-reference/templates/_helpers.tpl index 5b24ded..24bb55d 100644 --- a/infra/helm/big-reference/templates/_helpers.tpl +++ b/infra/helm/big-reference/templates/_helpers.tpl @@ -135,6 +135,14 @@ cluster-internal hosts ({{ .Release.Namespace }}) and the node address {{- end }} {{- end -}} +{{/* +The origin a browser reaches Keycloak on: the issuer Keycloak pins and the +authority the portals use, from one place so they cannot drift (ADR-0010). +*/}} +{{- define "big.keycloakUrl" -}} +{{- .Values.keycloakUrl | default (printf "http://%s:%v" .Values.host (index .Values.nodePorts "keycloak")) -}} +{{- end -}} + {{- define "big.labels" -}} app.kubernetes.io/name: {{ .name }} app.kubernetes.io/instance: {{ .root.Release.Name }} diff --git a/infra/helm/big-reference/templates/config.yaml b/infra/helm/big-reference/templates/config.yaml index 8771b2d..74a1a44 100644 --- a/infra/helm/big-reference/templates/config.yaml +++ b/infra/helm/big-reference/templates/config.yaml @@ -40,5 +40,5 @@ metadata: {{- include "big.labels" (dict "root" $ "name" (printf "portal-config-%s" $realm)) | nindent 4 }} data: config.json: | - { "authority": "{{ printf "http://%s:%v" $.Values.host (index $.Values.nodePorts "keycloak") }}/realms/{{ $realm }}" } + { "authority": "{{ include "big.keycloakUrl" $ }}/realms/{{ $realm }}" } {{- end }} diff --git a/infra/helm/big-reference/templates/deployments.yaml b/infra/helm/big-reference/templates/deployments.yaml index 02ac150..64fd8fb 100644 --- a/infra/helm/big-reference/templates/deployments.yaml +++ b/infra/helm/big-reference/templates/deployments.yaml @@ -28,7 +28,7 @@ spec: {{- range $w.files }} {{- if hasPrefix "portal-config-" .configMap }} annotations: - checksum/portal-config: {{ printf "%s|%v" $.Values.host (index $.Values.nodePorts "keycloak") | sha256sum }} + checksum/portal-config: {{ include "big.keycloakUrl" $ | sha256sum }} {{- end }} {{- end }} labels: diff --git a/infra/helm/big-reference/values.yaml b/infra/helm/big-reference/values.yaml index f2e0f55..20e7361 100644 --- a/infra/helm/big-reference/values.yaml +++ b/infra/helm/big-reference/values.yaml @@ -25,6 +25,11 @@ # string, so browser tokens and the BFF's discovered issuer agree. host: 192.168.122.100 +# Set when a TLS proxy outside the cluster publishes Keycloak: the full origin, no +# trailing slash. It replaces `host` + Keycloak's NodePort as the issuer and the +# portals' authority (runbook, "Publishing through the labs Caddy"). +keycloakUrl: "" + # Set when pulling from a private registry (e.g. the Gitea Container Registry). imagePullSecrets: [] @@ -268,7 +273,7 @@ workloads: # Pin the issuer to the address the browser uses, and let backchannel calls # keep using keycloak:8080 — the BFF discovers metadata in-cluster and gets # this issuer back, which is what browser tokens carry (infra/host-browser.yml). - KC_HOSTNAME: "http://{{ .Values.host }}:{{ index .Values.nodePorts \"keycloak\" }}" + KC_HOSTNAME: '{{ include "big.keycloakUrl" . }}' KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" ports: [{ name: http, port: 8080 }] # TCP, not /health/ready on the management port: nothing here gates on realm