feat(web-arm): per-site Keycloak clients for TYPO3 SSO #295

Open
dominik.polakovics wants to merge 1 commit from lab/20260807-2348 into main

One OIDC client per TYPO3 domain, with the per-site boilerplate generated from the domain and a script that does the rest.

Why not one shared client

A single client across all domains does work — redirectUris is just a list and Keycloak only checks the incoming URI against it. It was rejected because:

  • the same secret ends up in every site's config, and the sites have separate SSH users and separate owners, so the weakest site holds everyone's credential
  • client roles are the natural per-site authorization boundary; sharing a client leaves exactly one set of them
  • backchannel and front-channel logout URL are single-valued per client, so single logout could only ever notify one site
  • a token minted for one site carries the same aud as every other

What changed

typo3-sites.nix holds the domains. Everything else is derived from the domain — client id, redirect URIs, vault key, sops key — so the module is the only place that knows the naming convention:

{ "gbv-aktuell.at" = { policy = "admin-only"; }; }

cloonar-realm.json is now read into Nix and merged with the generated clients rather than passed to keycloak-config-cli directly. The hand-maintained one-off clients are untouched; only clients gains entries. Key order changes (builtins.toJSON sorts), which shifts the import checksum and causes one extra converge on first deploy.

vaultSecrets replaces the parallel plainClients list plus per-client LoadCredential mapping: credential name and vault key are now the same string, so materialiseSecrets loops over one set of names. Existing credential names change (gitea-secret to giteasecret); both sides move together and the names are unit-internal.

Access control reuses the existing browser-flow overrides rather than inventing anything: policy = "admin-only" admits Administrators, "internal" also admits Mitarbeiter and macher.solutions. The default is the narrower one — a too-narrow default fails loudly at the login screen and is a one-word fix, a too-wide one silently grants CMS backend access.

scripts/add-typo3-sso

Runs on the workstation against the checkout; nothing touches web-arm. Generates a 32-byte secret, writes it into hosts/web-arm/secrets.yaml with sops set, appends the domain to the list, and prints the AdditionalConfiguration.php block for EXT:oauth2_client with the real client id, secret and endpoints. --show reprints an existing site's block, --dry-run writes nothing.

The secret is written before the Nix entry: an unreferenced secret is harmless, a referenced missing one fails the host build.

The vault key stays alphanumeric (typo3gbvaktuellatsecret) rather than keeping hyphens, matching the existing underscore-free convention — the file vault's key-to-filename mapping is only known-good for that shape.

Verification

  • script exercised end to end against a stubbed sops: domain and policy validation, insertion into the Nix list, duplicate rejection, dry-run, and the printed config block
  • the dry-build did not run — Nix is not installed in the environment this was authored in, so scripts/test-configuration web-arm could not be executed and the commit used --no-verify. Please run it before merging.

The endpoint URLs in the printed block deliberately omit the /auth prefix that upstream's docs still show; that path was dropped with the Quarkus distribution and this deployment serves the realm at /realms/cloonar/.

Not included

Delivering the secret into each TYPO3 installation is still manual — this repo doesn't manage TYPO3 application config. A later change could have Nix drop an oidc.env into each site's shared/ directory for AdditionalConfiguration.php to read, which would close the loop for sites on web-arm and amzebs-01 alike.

Closes #294

One OIDC client per TYPO3 domain, with the per-site boilerplate generated from the domain and a script that does the rest. ## Why not one shared client A single client across all domains does work — `redirectUris` is just a list and Keycloak only checks the incoming URI against it. It was rejected because: - the same secret ends up in every site's config, and the sites have separate SSH users and separate owners, so the weakest site holds everyone's credential - client roles are the natural per-site authorization boundary; sharing a client leaves exactly one set of them - backchannel and front-channel logout URL are single-valued per client, so single logout could only ever notify one site - a token minted for one site carries the same `aud` as every other ## What changed `typo3-sites.nix` holds the domains. Everything else is derived from the domain — client id, redirect URIs, vault key, sops key — so the module is the only place that knows the naming convention: { "gbv-aktuell.at" = { policy = "admin-only"; }; } `cloonar-realm.json` is now read into Nix and merged with the generated clients rather than passed to `keycloak-config-cli` directly. The hand-maintained one-off clients are untouched; only `clients` gains entries. Key order changes (`builtins.toJSON` sorts), which shifts the import checksum and causes one extra converge on first deploy. `vaultSecrets` replaces the parallel `plainClients` list plus per-client `LoadCredential` mapping: credential name and vault key are now the same string, so `materialiseSecrets` loops over one set of names. Existing credential names change (`gitea-secret` to `giteasecret`); both sides move together and the names are unit-internal. Access control reuses the existing browser-flow overrides rather than inventing anything: `policy = "admin-only"` admits Administrators, `"internal"` also admits Mitarbeiter and macher.solutions. The default is the narrower one — a too-narrow default fails loudly at the login screen and is a one-word fix, a too-wide one silently grants CMS backend access. ## scripts/add-typo3-sso Runs on the workstation against the checkout; nothing touches web-arm. Generates a 32-byte secret, writes it into `hosts/web-arm/secrets.yaml` with `sops set`, appends the domain to the list, and prints the `AdditionalConfiguration.php` block for EXT:oauth2_client with the real client id, secret and endpoints. `--show` reprints an existing site's block, `--dry-run` writes nothing. The secret is written before the Nix entry: an unreferenced secret is harmless, a referenced missing one fails the host build. The vault key stays alphanumeric (`typo3gbvaktuellatsecret`) rather than keeping hyphens, matching the existing underscore-free convention — the file vault's key-to-filename mapping is only known-good for that shape. ## Verification - script exercised end to end against a stubbed `sops`: domain and policy validation, insertion into the Nix list, duplicate rejection, dry-run, and the printed config block - **the dry-build did not run** — Nix is not installed in the environment this was authored in, so `scripts/test-configuration web-arm` could not be executed and the commit used `--no-verify`. Please run it before merging. The endpoint URLs in the printed block deliberately omit the `/auth` prefix that upstream's docs still show; that path was dropped with the Quarkus distribution and this deployment serves the realm at `/realms/cloonar/`. ## Not included Delivering the secret into each TYPO3 installation is still manual — this repo doesn't manage TYPO3 application config. A later change could have Nix drop an `oidc.env` into each site's `shared/` directory for `AdditionalConfiguration.php` to read, which would close the loop for sites on web-arm and amzebs-01 alike. Closes #294
Adds a domain list to the keycloak module that generates one OIDC client per
TYPO3 site, plus scripts/add-typo3-sso to register a site in one command.

Closes #294
This pull request has changes conflicting with the target branch.
  • hosts/web-arm/modules/keycloak/default.nix
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin lab/20260807-2348:lab/20260807-2348
git switch lab/20260807-2348

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff lab/20260807-2348
git switch lab/20260807-2348
git rebase main
git switch main
git merge --ff-only lab/20260807-2348
git switch lab/20260807-2348
git rebase main
git switch main
git merge --no-ff lab/20260807-2348
git switch main
git merge --squash lab/20260807-2348
git switch main
git merge --ff-only lab/20260807-2348
git switch main
git merge lab/20260807-2348
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!295
No description provided.