Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4b41aca84 | ||
|
|
5494363221 | ||
|
|
0074a1bff3 | ||
|
|
594fdde227 |
@@ -207,8 +207,14 @@ jobs:
|
|||||||
# dispatched (gitea-actions-gotchas.md §7). Default `if: success()` dispatches normally. Cost: a
|
# dispatched (gitea-actions-gotchas.md §7). Default `if: success()` dispatches normally. Cost: a
|
||||||
# failing mutation ratchet now skips verify-stack instead of running it anyway; the fix-and-re-push
|
# failing mutation ratchet now skips verify-stack instead of running it anyway; the fix-and-re-push
|
||||||
# re-run exercises verify-stack, so we still get the signal.
|
# re-run exercises verify-stack, so we still get the signal.
|
||||||
|
#
|
||||||
|
# Main only, not on PRs: the runner shares the lab node with the deployed stack, and a second
|
||||||
|
# full stack per PR was what got the runner OOM-killed (#182). PRs still gate on every job above;
|
||||||
|
# the live-stack check runs once per merge. A plain event `if` keeps the implicit success(), so it
|
||||||
|
# is not the status-function case from gotchas §7.
|
||||||
verify-stack:
|
verify-stack:
|
||||||
needs: [mutation]
|
needs: [mutation]
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ jobs:
|
|||||||
# origin, e.g. https://big-auth.labs.respellion.tech (runbook, "Publishing
|
# origin, e.g. https://big-auth.labs.respellion.tech (runbook, "Publishing
|
||||||
# through the labs Caddy").
|
# through the labs Caddy").
|
||||||
KEYCLOAK_URL: ${{ vars.KEYCLOAK_URL }}
|
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:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
@@ -97,7 +100,7 @@ jobs:
|
|||||||
make k8s-reseed \
|
make k8s-reseed \
|
||||||
TALOS_HOST=${TALOS_HOST:-localhost} \
|
TALOS_HOST=${TALOS_HOST:-localhost} \
|
||||||
K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \
|
K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \
|
||||||
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL}"
|
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
|
# `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).
|
# new images only land on a restart (pullPolicy is already Always).
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ and CI cannot drift:
|
|||||||
| `frontend` | `make frontend` → Nx lint/test/build for the four portals | pnpm + Node |
|
| `frontend` | `make frontend` → Nx lint/test/build for the four portals | pnpm + Node |
|
||||||
| `k8s` | `make k8s-lint` (render + schema-check the Helm chart) → `make k8s-drift` (chart still describes the same stack as `infra/docker-compose.yml`) | pinned `helm` binary + `docker compose` |
|
| `k8s` | `make k8s-lint` (render + schema-check the Helm chart) → `make k8s-drift` (chart still describes the same stack as `infra/docker-compose.yml`) | pinned `helm` binary + `docker compose` |
|
||||||
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
|
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
|
||||||
| `verify-stack` | the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
|
| `verify-stack` | **push to `main` only, skipped on PRs** (#182) — the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
|
||||||
|
|
||||||
> **Why one `verify-stack` job, not three.** The single self-hosted runner runs jobs
|
> **Why one `verify-stack` job, not three.** The single self-hosted runner runs jobs
|
||||||
> **sequentially**, so booting OpenZaak once (instead of once per check) is the
|
> **sequentially**, so booting OpenZaak once (instead of once per check) is the
|
||||||
|
|||||||
@@ -430,6 +430,18 @@ make k8s-up TALOS_HOST=localhost K8S_REGISTRY=<TALOS_HOST>:30500 \
|
|||||||
For deploy-on-merge, set the repository variable `KEYCLOAK_URL` to the same value.
|
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.
|
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:
|
One-time setup:
|
||||||
|
|
||||||
1. Fedora host: install `infra/development/big-portals-tunnel.service` from the Infra repo
|
1. Fedora host: install `infra/development/big-portals-tunnel.service` from the Infra repo
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ services:
|
|||||||
# share this anchor and ignore it — they don't run uwsgi.
|
# share this anchor and ignore it — they don't run uwsgi.
|
||||||
UWSGI_PROCESSES: "1"
|
UWSGI_PROCESSES: "1"
|
||||||
UWSGI_THREADS: "2"
|
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
|
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||||||
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
||||||
DB_HOST: oz-db
|
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.
|
# 1 uWSGI worker, not the image default of 4×4 (#147) — see the oz-env note above.
|
||||||
UWSGI_PROCESSES: "1"
|
UWSGI_PROCESSES: "1"
|
||||||
UWSGI_THREADS: "2"
|
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
|
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
||||||
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
|
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
|
||||||
DB_HOST: nrc-db
|
DB_HOST: nrc-db
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ volumes:
|
|||||||
{{- with .defaultMode }}
|
{{- with .defaultMode }}
|
||||||
defaultMode: {{ . }}
|
defaultMode: {{ . }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- with .items }}
|
||||||
|
items:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- with $w.data }}
|
{{- with $w.data }}
|
||||||
- name: data
|
- name: data
|
||||||
@@ -125,13 +129,17 @@ volumes:
|
|||||||
{{/*
|
{{/*
|
||||||
Env list from a map. Every value is run through `tpl`, so values.yaml can name
|
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
|
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" -}}
|
{{- define "big.env" -}}
|
||||||
{{- $root := index . 0 -}}
|
{{- $root := index . 0 -}}
|
||||||
{{- range $k, $v := index . 1 }}
|
{{- range $k, $v := index . 1 }}
|
||||||
|
{{- $val := tpl (toString $v) $root }}
|
||||||
|
{{- if $val }}
|
||||||
- name: {{ $k }}
|
- name: {{ $k }}
|
||||||
value: {{ tpl (toString $v) $root | quote }}
|
value: {{ $val | quote }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ host: 192.168.122.100
|
|||||||
# portals' authority (runbook, "Publishing through the labs Caddy").
|
# portals' authority (runbook, "Publishing through the labs Caddy").
|
||||||
keycloakUrl: ""
|
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).
|
# Set when pulling from a private registry (e.g. the Gitea Container Registry).
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
|
|
||||||
@@ -76,6 +82,7 @@ envGroups:
|
|||||||
oz:
|
oz:
|
||||||
UWSGI_PROCESSES: "1"
|
UWSGI_PROCESSES: "1"
|
||||||
UWSGI_THREADS: "2"
|
UWSGI_THREADS: "2"
|
||||||
|
CELERY_WORKER_CONCURRENCY: "2"
|
||||||
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||||||
SECRET_KEY: dev-only-not-for-production
|
SECRET_KEY: dev-only-not-for-production
|
||||||
DB_HOST: oz-db
|
DB_HOST: oz-db
|
||||||
@@ -98,6 +105,7 @@ envGroups:
|
|||||||
nrc:
|
nrc:
|
||||||
UWSGI_PROCESSES: "1"
|
UWSGI_PROCESSES: "1"
|
||||||
UWSGI_THREADS: "2"
|
UWSGI_THREADS: "2"
|
||||||
|
CELERY_WORKER_CONCURRENCY: "2"
|
||||||
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
||||||
SECRET_KEY: dev-only-not-for-production
|
SECRET_KEY: dev-only-not-for-production
|
||||||
DB_HOST: nrc-db
|
DB_HOST: nrc-db
|
||||||
@@ -275,11 +283,29 @@ workloads:
|
|||||||
# this issuer back, which is what browser tokens carry (infra/host-browser.yml).
|
# this issuer back, which is what browser tokens carry (infra/host-browser.yml).
|
||||||
KC_HOSTNAME: '{{ include "big.keycloakUrl" . }}'
|
KC_HOSTNAME: '{{ include "big.keycloakUrl" . }}'
|
||||||
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
|
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 }}'
|
||||||
|
# Behind a TLS proxy (keycloakUrl) the dynamic backchannel URLs — token,
|
||||||
|
# userinfo, certs — take their scheme from the request, which reaches Keycloak
|
||||||
|
# as plain http; trusting X-Forwarded-Proto keeps them https so the browser
|
||||||
|
# doesn't block them as mixed content. In-cluster calls send no such header.
|
||||||
|
KC_PROXY_HEADERS: xforwarded
|
||||||
ports: [{ name: http, port: 8080 }]
|
ports: [{ name: http, port: 8080 }]
|
||||||
# TCP, not /health/ready on the management port: nothing here gates on realm
|
# 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.
|
# import, and a wrong health path would leave the Service with no endpoints.
|
||||||
probe: { tcpSocket: { port: 8080 }, initialDelaySeconds: 15 }
|
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 (S-03) ─────────────────────────────────────────────────────────
|
||||||
flowable-db:
|
flowable-db:
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ seed() { # name <kubectl --from-file args...>
|
|||||||
seed rr-oz-config --from-file="$repo/infra/openzaak/setup_configuration/"
|
seed rr-oz-config --from-file="$repo/infra/openzaak/setup_configuration/"
|
||||||
seed rr-nrc-config --from-file="$repo/infra/opennotificaties/setup_configuration/"
|
seed rr-nrc-config --from-file="$repo/infra/opennotificaties/setup_configuration/"
|
||||||
seed rr-kc-realms --from-file="$repo/infra/keycloak/realms/"
|
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-objecttypen-config --from-file="$repo/infra/objecttypen/setup_configuration/"
|
||||||
seed rr-objecten-config --from-file="$repo/infra/objecten/setup_configuration/"
|
seed rr-objecten-config --from-file="$repo/infra/objecten/setup_configuration/"
|
||||||
# register.py + the RegisterRecord JSON schema (the __pycache__ dir is skipped:
|
# 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
|
||||||
Reference in New Issue
Block a user