Compare commits

..
Author SHA1 Message Date
not 408cc32594 Merge branch 'main' into feat/demo-otp-autofill
CI / lint (pull_request) Successful in 1m44s
CI / k8s (pull_request) Successful in 12s
CI / build (pull_request) Successful in 1m17s
CI / unit (pull_request) Successful in 1m43s
CI / frontend (pull_request) Successful in 2m47s
CI / mutation (pull_request) Successful in 5m2s
CI / verify-stack (pull_request) Successful in 9m14s
2026-09-25 11:12:17 +00:00
not 594fdde227 fix(infra): cap celery workers at 2 so the shared node stops OOM-killing CI (closes #182) (#183)
CI / k8s (push) Successful in 9s
CI / build (push) Successful in 1m39s
CI / lint (push) Successful in 2m0s
CI / mutation (push) Canceled after 0s
CI / verify-stack (push) Canceled after 0s
CI / unit (push) Canceled after 1m9s
CI / frontend (push) Canceled after 1m16s
Deploy to Talos / deploy (push) Successful in 2m31s
## What & why

`verify-stack` is being killed by the OOM controller on the shared Talos node, on main (run 827) and on #180 (run 830). The cause is Celery: with `CELERY_WORKER_CONCURRENCY` unset, `oz-celery` and `nrc-celery` each fork one worker per CPU. That's 22 each on the lab node, 49 Celery processes at about 225 MB apiece. Details and the kernel log evidence are in #182.

This sets `CELERY_WORKER_CONCURRENCY: "2"` in the oz and nrc env groups:
- **compose** (`&oz-env`, `&nrc-env`): what `verify-stack` starts inside `dind`.
- **chart** (`envGroups.oz` / `.nrc`): the deployed demo on the same node.

Both images' `/celery_worker.sh` honour the variable; I checked in the running pods. Web, init and beat containers share the anchors and ignore it. `objecten-celery` already defaults to 1.

Closes #182

## Definition of Done

- [x] Linked Gitea issue (above).
- [ ] Failing test committed before the implementation. *(Resource setting; the evidence is the OOM log in #182.)*
- [x] Conventional Commits referencing the issue (`refs #NN`).
- [ ] CI green. This PR's `verify-stack` run is the check.
- [x] `docker compose config` renders the variable into all 7 services on the two anchors.
- [x] Docs: comments next to the setting, following the existing uWSGI notes.

## Notes for reviewers

- `make k8s-lint` and `make k8s-drift` pass.
- **Not applied live.** I couldn't patch the running cluster from my session. After merge, the deploy updates the `oz-env` / `nrc-env` ConfigMaps. The celery pods only pick that up on restart, and the deploy step restarts only this repo's nine services. So run once:
  `kubectl -n big rollout restart deploy/oz-celery deploy/nrc-celery`
- **Why 2 and not 1:** this matches `UWSGI_THREADS: "2"`, and it keeps one notification delivery from blocking behind a slow task. It cuts roughly 40 processes, about 9 GB RSS (less in practice, because forked workers share pages).
- **Longer term:** CI and the demo share one 15 GB VM. Resource requests on the runner, or moving the runner off the node, would stop one from starving the other.

🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #183
2026-09-25 11:11:45 +00:00
notandClaude Opus 5.5 0ea562db52 refactor(k8s): keep the big-demo theme as real files under infra/keycloak/themes (refs #177)
CI / lint (pull_request) Successful in 1m55s
CI / k8s (pull_request) Successful in 9s
CI / build (pull_request) Successful in 1m27s
CI / unit (pull_request) Successful in 1m51s
CI / frontend (pull_request) Successful in 3m24s
CI / mutation (pull_request) Successful in 6m40s
CI / verify-stack (pull_request) Failing after 1m1s
The theme was inline text in a Helm template. It now lives next to the realms
and is seeded as rr-kc-theme by seed-configmaps.sh, like every other file
input, so it can be edited as a normal Keycloak theme. demo.otpAutofill now
only decides whether KC_SPI_THEME_DEFAULT is set (big.env skips values that
render empty); off, Keycloak keeps its stock theme.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:15:35 +02:00
notandClaude Opus 5.5 6cd2268be9 fix(k8s): declare every theme type in big-demo so the account console loads (refs #177)
CI / lint (pull_request) Successful in 2m0s
CI / k8s (pull_request) Successful in 9s
CI / build (pull_request) Successful in 1m22s
CI / unit (pull_request) Successful in 1m34s
CI / verify-stack (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 3m8s
CI / mutation (pull_request) Canceled after 8m10s
KC_SPI_THEME_DEFAULT applies to all theme types and Keycloak does not fall back
for one the theme lacks: the account console returned 500 (NPE on a null
theme). The theme is now one ConfigMap mounted as a directory with login,
account, admin and email types; the podspec gains `items` for that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:02:46 +02:00
notandClaude Opus 5.5 1cc04de630 feat(k8s): optionally auto-fill the medewerker OTP step for the public demo (refs #177)
CI / k8s (pull_request) Successful in 13s
CI / build (pull_request) Successful in 5m36s
CI / unit (pull_request) Successful in 6m18s
CI / frontend (pull_request) Successful in 3m12s
CI / verify-stack (pull_request) Canceled after 0s
CI / mutation (pull_request) Canceled after 8m0s
CI / lint (pull_request) Canceled after 21m26s
A `big-demo` Keycloak login theme (keycloak.v2 + one script) is always mounted
and set as default. With demo.otpAutofill (repo variable OTP_AUTOFILL=true) the
script computes the code from the committed fixture secret and submits it, so
the demo shows MFA enforced without an authenticator. Off by default: the
script is empty and the login is plain keycloak.v2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 11:52:29 +02:00
not 804031eeb8 feat(k8s): publish the portals through the labs Caddy (refs #177) (#179)
CI / k8s (push) Successful in 14s
CI / build (push) Successful in 2m2s
CI / lint (push) Successful in 2m32s
CI / unit (push) Successful in 1m36s
CI / frontend (push) Successful in 3m3s
Deploy to Talos / deploy (push) Successful in 3m39s
CI / mutation (push) Successful in 5m31s
CI / verify-stack (push) Failing after 17m26s
## 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: #179
2026-09-25 08:16:37 +00:00
not 6cfcc4cf83 ci(deploy): deploy the stack to Talos on merge to main (closes #175) (#176)
CI / k8s (push) Successful in 6s
CI / unit (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / mutation (push) Canceled after 0s
CI / verify-stack (push) Canceled after 0s
CI / build (push) Canceled after 9s
CI / lint (push) Canceled after 24s
Deploy to Talos / deploy (push) Successful in 5m1s
## What & why

The chart has been deployable by hand since #25 and linted in CI since #168. This makes a
merged PR actually ship it to the Talos VM on the lab server.

`.gitea/workflows/deploy.yaml` runs on a push to `main` (a squash-merged PR) and on manual
dispatch:

1. **Tunnel** — neither the Kubernetes API nor the in-cluster registry is publicly reachable,
   so 6443, 30500 and 30141 are forwarded over the same SSH hop into the Fedora host that the
   Gitea-runner pipeline uses (`ssh -p 6667 user@labs.respellion.tech`).
2. **Images** — `make k8s-images K8S_REGISTRY=localhost:30500`, pushed *through* the tunnel.
3. **Deploy** — `make k8s-reseed TALOS_HOST=… K8S_REGISTRY=<vm-ip>:30500`, pulled by the node
   from its own NodePort.
4. **Roll** — `rollout restart` + `rollout status` on the nine repo deployments.
5. **Smoke** — `GET /openbaar/register` through the openbaar portal.

Three decisions worth the review:

- **One registry, two names.** The push target (`localhost:30500`, the tunnel) and the pull
  target (`<vm-ip>:30500`, the node's own NodePort) address the same store. The pull name has
  to be the one in the node's registry-mirror patch, which is what makes plain HTTP acceptable.
- **`k8s-reseed`, not `k8s-up`.** A Job's pod template is immutable, so a chart change to any
  bootstrap Job would otherwise fail the upgrade with `cannot patch … with kind Job`. The Jobs
  are idempotent by design, so re-running them every deploy is safe and removes that whole
  class of failure. Cost: a few minutes per deploy, and `seed-zaaktype` needs egress from the VM.
- **No re-run of the checks.** PR CI is the merge gate, so `main` is green by construction.
  Deploys **queue** (`cancel-in-progress: false`) — a `helm upgrade` killed half-way leaves the
  release in `pending-upgrade` and has to be unwedged by hand.

Settings on the repo (already added): secrets `TALOS_SSH_KEY` and `TALOS_KUBECONFIG`
(base64, and its `server:` must be `https://127.0.0.1:6443` — Talos puts `127.0.0.1` in the
apiserver cert SANs, so TLS still verifies through the tunnel); variables `TALOS_VM_IP`
(default `192.168.122.173`) and `TALOS_HOST` (default `localhost`).

Closes #175

## Definition of Done

- [x] Linked Gitea issue (above).
- [ ] Failing test committed before the implementation — **n/a**: this is a deployment
      workflow with no unit under test. Its check is the run itself: `rollout status` and the
      public-register smoke both have to pass or the job fails. `make k8s-lint` / `make k8s-drift`
      (#168) already gate the chart it deploys.
- [x] Implementation — one workflow file, no production code touched.
- [x] Conventional Commits referencing the issue (`refs #175`).
- [ ] CI green — awaiting the run on this PR.
- [x] `docker compose up` unaffected — no service, image or compose file is touched.
- [x] Docs updated — `docs/runbooks/kubernetes-talos.md` §9 (the tunnel, the two registry
      names, the secrets table, the smoke) and a pointer from `docs/runbooks/ci.md`.
- [x] No ADR needed: no new dependency (kubectl/helm/crane are already prerequisites of the
      `k8s-*` targets), no service boundary moved, no CLAUDE.md §8 rule bent.
- [ ] Demo note — not user-visible.

## Notes for reviewers

- **The first deploy is the real test.** It cannot be dry-run: the tunnel, the secrets and the
  registry only exist on the lab server. Merging is how we find out; `Pods on failure` dumps
  `get pods,jobs` if it doesn't.
- **Known gap — the portals still aren't browsable.** PKCE needs a secure context, so a
  NodePort on an IP can't serve them (runbook §5); they need `make k8s-portals` or an SSH
  forward. Giving the server a hostname + TLS is the follow-up, and is where `TALOS_HOST`
  stops defaulting to `localhost`.
- **Databases are `emptyDir`.** Any change to a database pod's template wipes it; the
  `k8s-reseed` in the deploy re-runs the bootstrap, so the stack recovers, but submitted
  registrations do not. Persistence is runbook §6.

🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #176
2026-09-18 13:53:58 +00:00
13 changed files with 167 additions and 71 deletions
+10 -15
View File
@@ -27,10 +27,13 @@ jobs:
# `kubectl port-forward` — runbook §5. Override with repo variables.
TALOS_VM_IP: ${{ vars.TALOS_VM_IP }}
TALOS_HOST: ${{ vars.TALOS_HOST }}
# Set it and the stack is published over TLS on <sub>.<domain> by the
# in-cluster edge (ADR-0035, runbook §10). Empty = NodePorts, as before.
PUBLIC_DOMAIN: ${{ vars.PUBLIC_DOMAIN }}
PUBLIC_EMAIL: ${{ vars.PUBLIC_EMAIL }}
# 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 }}
# `true` fills in the medewerker OTP step for the public demo (chart value
# demo.otpAutofill). The fixture secret is committed: demo only.
OTP_AUTOFILL: ${{ vars.OTP_AUTOFILL }}
steps:
- uses: https://github.com/actions/checkout@v4
@@ -94,12 +97,10 @@ jobs:
# Job template from wedging the upgrade (`cannot patch … with kind Job`).
- name: Deploy the chart
run: |
set -euo pipefail
publish="${PUBLIC_DOMAIN:+--set public.domain=$PUBLIC_DOMAIN --set public.email=${PUBLIC_EMAIL:-}}"
make k8s-reseed \
TALOS_HOST=${TALOS_HOST:-localhost} \
K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \
K8S_SET="$publish"
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL} --set demo.otpAutofill=${OTP_AUTOFILL:-false}"
# `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).
@@ -115,12 +116,6 @@ jobs:
- name: Smoke the public register
run: curl -fsS --retry 10 --retry-delay 6 --retry-all-errors http://localhost:30141/openbaar/register
# Cluster-wide, not just `big`: the first thing that can fail is the registry
# in its own namespace, and a scheduling problem shows up in the events, not
# in `rollout status` — which only ever says "timed out waiting".
- name: Pods and events on failure
- name: Pods on failure
if: failure()
run: |
kubectl get pods -A -o wide || true
kubectl -n big get jobs || true
kubectl get events -A --sort-by=.lastTimestamp | tail -30 || true
run: kubectl -n big get pods,jobs || true
+48 -50
View File
@@ -366,36 +366,6 @@ immutable, so `helm upgrade` is rejected with `cannot patch "…" with kind Job`
every time a PR is squash-merged to `main` (and on demand via *Run workflow*). PR CI is the
merge gate, so the workflow deploys without re-running the checks.
**Prerequisite: the VM must have been installed with the §1 patch.** A stock Talos config
gives you a node that still carries the control-plane taint and knows nothing about the
plain-HTTP registry, and the deploy hits those in that order: the `registry` pod sits
`Pending` until `rollout status` times out, and once that is fixed every repo image fails to
pull. Two separate fixes:
```bash
# on the Fedora host — 1. let workloads onto the only node (§1)
export KUBECONFIG=~/talos-kubeconfig-local
kubectl taint node --all node-role.kubernetes.io/control-plane-
# 2. trust the in-cluster registry over plain HTTP (§2)
cat > /tmp/registry-patch.yaml <<'YAML'
machine:
registries:
mirrors:
"<TALOS_VM_IP>:30500":
endpoints:
- http://<TALOS_VM_IP>:30500
YAML
talosctl -n <TALOS_VM_IP> -e <TALOS_VM_IP> patch mc --patch @/tmp/registry-patch.yaml
```
Keep those two apart. On Talos 1.14 a patch that also sets
`cluster.allowSchedulingOnControlPlanes` is rejected with *".cluster.allowSchedulingOnControlPlanes
is already set in v1alpha1 config"* — the field moved out of the v1alpha1 schema, the same way
`machine.install` did (§1) — and the rejection takes the whole patch with it, so the mirror
silently doesn't land either. `kubectl taint` is the documented way (§1); it is undone if the
node ever re-registers, which is a reboot, not a deploy.
The cluster's API and registry are not exposed publicly, so the job forwards them over the
same SSH hop the Gitea-runner pipeline uses:
@@ -427,30 +397,58 @@ Settings, all on the repository in Gitea:
The last step smokes `GET /openbaar/register` through the openbaar portal, which exercises
portal → Caddy → BFF → projection. An empty register passes; a 502 does not.
### Reaching the portals from a laptop
The deployed portals are pinned to `http://localhost:30180` for Keycloak (§5), so a browser
needs **all five** browser-facing ports on its own localhost — the portal alone is not
enough, and a missing Keycloak shows up as `ERR_CONNECTION_REFUSED` on
`/realms/*/.well-known/openid-configuration` followed by an opaque `ERROR Error: [object Object]`.
`make k8s-portals` does this when kubectl can reach the cluster; through the lab server one
SSH does it without a kubeconfig at all:
```bash
ssh -N -p 6667 \
-L 30140:<TALOS_VM_IP>:30140 -L 30141:<TALOS_VM_IP>:30141 \
-L 30142:<TALOS_VM_IP>:30142 -L 30143:<TALOS_VM_IP>:30143 \
-L 30180:<TALOS_VM_IP>:30180 \
user@labs.respellion.tech
```
Then the §5 table's URLs work as written. The admin UIs (OpenZaak, Flowable, …) need no
forward — they are server-rendered, so the VM's address is fine.
Not covered: the portals still need `make k8s-portals` (or an SSH forward) to be usable in a
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 → <TALOS_VM_IP>: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=<TALOS_HOST>: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.
Staff logins still hit the enforced OTP step. For a demo, set the repository variable
`OTP_AUTOFILL=true` (chart value `demo.otpAutofill`): Keycloak then uses the `big-demo`
theme, which fills in and submits the code from the fixture secret, so the step is visible
but needs no authenticator. Keycloak restarts when the value flips. Demo only — the secret
is committed.
The theme lives in `infra/keycloak/themes/big-demo/` and is seeded as the `rr-kc-theme`
ConfigMap by `infra/helm/seed-configmaps.sh` on every deploy. Keycloak runs `start-dev`,
which doesn't cache themes, so an edit shows up about a minute after the ConfigMap changes.
A *new* theme file also needs a key in the seed script and a path in the keycloak `files`
in `values.yaml`.
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
+5
View File
@@ -57,6 +57,9 @@ services:
# share this anchor and ignore it — they don't run uwsgi.
UWSGI_PROCESSES: "1"
UWSGI_THREADS: "2"
# Same lever for oz-celery: unset, the worker forks one process per CPU (22 on the lab node,
# ~225 MB each), which OOM-killed the shared runner mid-verify-stack. Only celery reads it.
CELERY_WORKER_CONCURRENCY: "2"
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
DB_HOST: oz-db
@@ -144,6 +147,8 @@ services:
# 1 uWSGI worker, not the image default of 4×4 (#147) — see the oz-env note above.
UWSGI_PROCESSES: "1"
UWSGI_THREADS: "2"
# Two celery workers, not one per CPU — see the oz-env note above.
CELERY_WORKER_CONCURRENCY: "2"
DJANGO_SETTINGS_MODULE: nrc.conf.docker
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
DB_HOST: nrc-db
@@ -94,6 +94,10 @@ volumes:
{{- with .defaultMode }}
defaultMode: {{ . }}
{{- end }}
{{- with .items }}
items:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with $w.data }}
- name: data
@@ -125,14 +129,26 @@ volumes:
{{/*
Env list from a map. Every value is run through `tpl`, so values.yaml can name
cluster-internal hosts ({{ .Release.Namespace }}) and the node address
({{ .Values.host }}) without the chart hard-coding either.
({{ .Values.host }}) without the chart hard-coding either. A value that renders
empty is left out, which is how a setting is made conditional on a chart value.
*/}}
{{- define "big.env" -}}
{{- $root := index . 0 -}}
{{- range $k, $v := index . 1 }}
{{- $val := tpl (toString $v) $root }}
{{- if $val }}
- name: {{ $k }}
value: {{ tpl (toString $v) $root | quote }}
value: {{ $val | quote }}
{{- end }}
{{- 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" -}}
@@ -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 }}
@@ -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:
+28 -2
View File
@@ -25,6 +25,17 @@
# 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: ""
demo:
# Fill in and submit the medewerker OTP step from the fixture secret, so a public
# demo shows MFA enforced without an authenticator: makes the big-demo theme
# (infra/keycloak/themes/big-demo) Keycloak's default. Demo only: the secret is committed.
otpAutofill: false
# Set when pulling from a private registry (e.g. the Gitea Container Registry).
imagePullSecrets: []
@@ -71,6 +82,7 @@ envGroups:
oz:
UWSGI_PROCESSES: "1"
UWSGI_THREADS: "2"
CELERY_WORKER_CONCURRENCY: "2"
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
SECRET_KEY: dev-only-not-for-production
DB_HOST: oz-db
@@ -93,6 +105,7 @@ envGroups:
nrc:
UWSGI_PROCESSES: "1"
UWSGI_THREADS: "2"
CELERY_WORKER_CONCURRENCY: "2"
DJANGO_SETTINGS_MODULE: nrc.conf.docker
SECRET_KEY: dev-only-not-for-production
DB_HOST: nrc-db
@@ -268,13 +281,26 @@ 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"
# Only rendered with demo.otpAutofill (big.env skips empty values); off, Keycloak
# keeps its stock theme and the mounted big-demo theme is unused.
KC_SPI_THEME_DEFAULT: '{{ if .Values.demo.otpAutofill }}big-demo{{ end }}'
ports: [{ name: http, port: 8080 }]
# TCP, not /health/ready on the management port: nothing here gates on realm
# import, and a wrong health path would leave the Service with no endpoints.
probe: { tcpSocket: { port: 8080 }, initialDelaySeconds: 15 }
files: [{ configMap: rr-kc-realms, mountPath: /opt/keycloak/data/import }]
files:
- { configMap: rr-kc-realms, mountPath: /opt/keycloak/data/import }
# infra/keycloak/themes/big-demo, seeded by infra/helm/seed-configmaps.sh.
- configMap: rr-kc-theme
mountPath: /opt/keycloak/themes/big-demo
items:
- { key: login.properties, path: login/theme.properties }
- { key: otp-autofill.js, path: login/resources/js/otp-autofill.js }
- { key: account.properties, path: account/theme.properties }
- { key: admin.properties, path: admin/theme.properties }
- { key: email.properties, path: email/theme.properties }
# ── Flowable (S-03) ─────────────────────────────────────────────────────────
flowable-db:
+9
View File
@@ -30,6 +30,15 @@ seed() { # name <kubectl --from-file args...>
seed rr-oz-config --from-file="$repo/infra/openzaak/setup_configuration/"
seed rr-nrc-config --from-file="$repo/infra/opennotificaties/setup_configuration/"
seed rr-kc-realms --from-file="$repo/infra/keycloak/realms/"
# The big-demo login theme (demo.otpAutofill). ConfigMap keys are flat, so each
# file gets a key here and its path back in the keycloak `files` in values.yaml.
theme="$repo/infra/keycloak/themes/big-demo"
seed rr-kc-theme \
--from-file=login.properties="$theme/login/theme.properties" \
--from-file=otp-autofill.js="$theme/login/resources/js/otp-autofill.js" \
--from-file=account.properties="$theme/account/theme.properties" \
--from-file=admin.properties="$theme/admin/theme.properties" \
--from-file=email.properties="$theme/email/theme.properties"
seed rr-objecttypen-config --from-file="$repo/infra/objecttypen/setup_configuration/"
seed rr-objecten-config --from-file="$repo/infra/objecten/setup_configuration/"
# register.py + the RegisterRecord JSON schema (the __pycache__ dir is skipped:
@@ -0,0 +1,4 @@
# The chart makes big-demo the default for every theme type, and Keycloak does not
# fall back for a type a theme lacks (the account page then fails), so each type is
# declared as a plain child of Keycloak 26's own default.
parent=keycloak.v3
@@ -0,0 +1,4 @@
# The chart makes big-demo the default for every theme type, and Keycloak does not
# fall back for a type a theme lacks (the admin page then fails), so each type is
# declared as a plain child of Keycloak 26's own default.
parent=keycloak.v2
@@ -0,0 +1,4 @@
# The chart makes big-demo the default for every theme type, and Keycloak does not
# fall back for a type a theme lacks (the email page then fails), so each type is
# declared as a plain child of Keycloak 26's own default.
parent=keycloak
@@ -0,0 +1,24 @@
// RFC 6238 with Keycloak's default policy (HmacSHA1, 6 digits, 30 s) over the
// raw bytes of the medewerker fixture secret — same as tests/e2e/keycloak-login.ts.
document.addEventListener('DOMContentLoaded', async () => {
const input = document.querySelector('input[name="otp"]');
if (!input || !input.form) return;
const key = await crypto.subtle.importKey('raw',
new TextEncoder().encode('BIGMEDEWERKEROTPSEED'), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
// A code is single-use, so a second login in the same window spends the next
// counter (Keycloak's look-ahead accepts it). Past that, fill but don't submit,
// so a rejected code can't turn into a submit loop.
const now = Math.floor(Date.now() / 30000);
let last = -1;
try { last = Number(sessionStorage.getItem('big-otp-counter')) || -1; } catch {}
const counter = Math.max(now, last + 1);
const msg = new DataView(new ArrayBuffer(8));
msg.setBigUint64(0, BigInt(counter));
const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg.buffer));
const o = mac[19] & 0x0f;
const n = ((mac[o] & 0x7f) << 24 | mac[o + 1] << 16 | mac[o + 2] << 8 | mac[o + 3]) % 1e6;
input.value = String(n).padStart(6, '0');
if (counter > now + 1) return;
try { sessionStorage.setItem('big-otp-counter', String(counter)); } catch {}
input.form.requestSubmit();
});
@@ -0,0 +1,11 @@
# Demo login theme for the public Talos deployment: keycloak.v2 plus a script that
# fills in and submits the medewerker OTP step from the committed fixture secret
# (docs/runbooks/keycloak.md). Only used when the chart's demo.otpAutofill is on —
# it then becomes Keycloak's default theme. Never enable it anywhere real.
#
# Add styles, messages or template overrides here as in any Keycloak theme
# (https://www.keycloak.org/ui-customization/themes); new files must also be
# listed in infra/helm/seed-configmaps.sh and the keycloak `files` in values.yaml.
parent=keycloak.v2
import=common/keycloak
scripts=js/otp-autofill.js