scaffold: hub-and-spoke layout, 4-network topology, 13 stack stubs
Initial structure for R&D infrastructure:
- stacks/ — 13 reusable, runnable stack stubs (kebab-case)
cloud-and-edge: node-red, influxdb, grafana, keycloak, portainer,
nginx-proxy, mqtt, postfix
cloud-only: wireguard-server, gitea, jenkins, sql (postgres stub)
edge-only: wireguard-client
- cloud/ — single central hub composition with 4 networks
(edge, app, data internal, mgmt) and include: stubs
- sites/ — per-plant edge folders (template README only for now)
- docs/architecture.md — hub-and-spoke + ingress + segmentation rationale
Network model: only nginx-proxy (80/443/8883) and wireguard-server
(51820/udp) publish ports on the cloud host. Edge nginx publishes
80/443 on plant-LAN interface only. MQTT cloud-side via nginx stream
proxy; MQTT edge-side internal-only; Postfix outbound-only.
OT layer (OPCUA, PLCs) is out of scope for this repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:37:59 +02:00
# keycloak
2026-05-21 13:54:57 +02:00
Identity provider for SSO across all R&D services. **Cloud-only ** for now (edges get their own realms once we cover the edge layer).
scaffold: hub-and-spoke layout, 4-network topology, 13 stack stubs
Initial structure for R&D infrastructure:
- stacks/ — 13 reusable, runnable stack stubs (kebab-case)
cloud-and-edge: node-red, influxdb, grafana, keycloak, portainer,
nginx-proxy, mqtt, postfix
cloud-only: wireguard-server, gitea, jenkins, sql (postgres stub)
edge-only: wireguard-client
- cloud/ — single central hub composition with 4 networks
(edge, app, data internal, mgmt) and include: stubs
- sites/ — per-plant edge folders (template README only for now)
- docs/architecture.md — hub-and-spoke + ingress + segmentation rationale
Network model: only nginx-proxy (80/443/8883) and wireguard-server
(51820/udp) publish ports on the cloud host. Edge nginx publishes
80/443 on plant-LAN interface only. MQTT cloud-side via nginx stream
proxy; MQTT edge-side internal-only; Postfix outbound-only.
OT layer (OPCUA, PLCs) is out of scope for this repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:37:59 +02:00
2026-05-21 13:54:57 +02:00
- **Public hostname**: `auth.wbd-rd.nl` (reverse-proxied via nginx-proxy at HTTPS → backend HTTP 8080)
- **Networks**: `app` (OIDC endpoints for relying-party apps) + `mgmt` (admin console) + `data` (postgres backend)
- **Backend**: postgres `keycloak` database in `sql` stack — provisioned automatically by `sql/config/init.d/01-databases.sh` on first start
- **Volume**: `keycloak-data` (themes, providers, realm exports)
## First-run bootstrap
`KC_BOOTSTRAP_ADMIN_USERNAME` + `KC_BOOTSTRAP_ADMIN_PASSWORD` create the master-realm admin on first start. **Change the password immediately after first login ** via the admin console.
2026-05-21 18:34:37 +00:00
After `cloud/deploy.sh` succeeds, run the kcadm bootstrap from [`cloud/README.md → Keycloak realm bootstrap (one-time)` ](../../cloud/README.md#keycloak-realm-bootstrap-one-time ). That single script provisions:
- the `wbd` realm
- one OIDC client per app (with redirect URI + hardcoded audience mapper + post-logout URI)
- the `app-admin` realm role
- the first operator user
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
## Realm + clients
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
Every human-accessible app is wired to a Keycloak client in realm `wbd` :
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
| Client ID | App | Redirect URI | Mechanism |
|---|---|---|---|
| `gitea` | Gitea | `https://git.wbd-rd.nl/user/oauth2/keycloak/callback` | native OIDC |
| `grafana` | Grafana | `https://dash.wbd-rd.nl/login/generic_oauth` | native OIDC (`generic_oauth` ) |
| `node-red` | Node-RED | `https://flow.wbd-rd.nl/auth/strategy/callback/` | native OIDC (passport-openidconnect) |
| `jenkins` | Jenkins | `https://ci.wbd-rd.nl/securityRealm/finishLogin` | native OIDC (`oic-auth` plugin via JCasC) |
| `jupyterhub` | JupyterHub | `https://hub.wbd-rd.nl/hub/oauth_callback` | native OIDC (`oauthenticator.GenericOAuthenticator` ) |
| `mlflow` | MLflow | `https://ml.wbd-rd.nl/oauth2/callback` | oauth2-proxy + nginx `auth_request` |
| `portainer-ce` | Portainer | `https://ops.wbd-rd.nl/oauth2/callback` | oauth2-proxy + nginx `auth_request` |
| `rabbitmq` | RabbitMQ UI | `https://mq.wbd-rd.nl/oauth2/callback` | oauth2-proxy + nginx `auth_request` |
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
### Required client config (gotchas)
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
- **Hardcoded audience mapper** (`oidc-audience-mapper` with `included.client.audience=<clientId>` ) on every client. Without this, the access-token `aud` is `[realm-management, account]` and oauth2-proxy clients return HTTP 500 at callback time. The kcadm bootstrap script in `cloud/README.md` creates this mapper for every client.
- **Valid post-logout redirect URIs** must be set (Keycloak 24+ requirement) — `https://<app-host>/*` . Otherwise logout returns "Invalid redirect URI".
- **`directAccessGrantsEnabled=false` ** unless you specifically need ROPC for that client.
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
## Realm roles
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
- `app-admin` — granted to operators who should get full admin in every app. Each app's config maps this role to its own admin role:
- Grafana: `app-admin` → `Admin`
- Jenkins: matrix-auth grants `Overall/Administer` to specific usernames (not the role itself yet — TODO)
- Node-RED / JupyterHub: env-var lists are still username-based; switching to role-based is a TODO
- Anyone without `app-admin` gets viewer / read-only.
2026-05-21 13:54:57 +02:00
## Realm-as-code
2026-05-21 18:34:37 +00:00
Once you're happy with the manual setup, export the realm so future deploys auto-import:
```bash
docker compose exec keycloak /opt/keycloak/bin/kc.sh \
export --dir /opt/keycloak/data/import --realm wbd
```
2026-05-21 13:54:57 +02:00
2026-05-21 18:34:37 +00:00
Commit the resulting JSON to `stacks/keycloak/config/realms/wbd.json` . Keycloak imports anything in `/opt/keycloak/data/import/` on first start.
2026-05-21 13:54:57 +02:00
## TODO
2026-05-21 18:34:37 +00:00
- Export realm config → commit `config/realms/wbd.json`
2026-05-21 13:54:57 +02:00
- Theme: WBD branding (logo, colors)
- User federation (LDAP from corporate AD, if applicable)
- 2FA policy
- Session / token lifetimes per client
2026-05-21 18:34:37 +00:00
- Switch Node-RED + JupyterHub admin lookup from env-var lists to `app-admin` realm role checks (Grafana already does this)