New stack:
- stacks/oauth2-proxy/ — per-app sidecars (mlflow, portainer, rabbitmq)
that gate vhosts via nginx auth_request against Keycloak's wbd realm.
Native OIDC wired into:
- grafana (generic_oauth, role-attribute-path → Admin/Editor/Viewer)
- jupyterhub (oauthenticator.GenericOAuthenticator)
- node-red (passport-openidconnect; in-memory state store + users()
resolver because adminAuth doesn't expose req.session)
- jenkins (oic-auth plugin via JCasC; matrix-auth for authz; setup
wizard suppressed; custom image with plugins.txt)
Infra fixes uncovered while bringing the above online:
- nginx-proxy: bump proxy_buffer_size to 16k so oauth2-proxy callbacks
don't 502 on the JWT-bearing Set-Cookie header.
- nginx-proxy: add `resolver 127.0.0.11 valid=30s` so service names
re-resolve after sidecar recreates (was cross-wiring oauth2-proxy
upstreams after restart).
- jupyterhub: pass --allow-root to the singleuser spawner (hub runs as
root inside its container; jupyter-server refused root without flag).
- jupyterhub Dockerfile: install jupyterlab + notebook so
SimpleLocalProcessSpawner has something to launch.
- node-red Dockerfile: install passport-openidconnect into the image
so settings.js can require() it.
- portainer: pre-seed local admin via --admin-password=<bcrypt-hash>
so the 5-minute "no admin → lockout" timer can never trigger.
- deploy.sh: restore executable bit (was 644 in repo).
Admin/viewer policy:
- Created realm role `app-admin` in keycloak wbd realm.
- Grafana maps app-admin → Admin (default Viewer).
- Jenkins matrix-auth grants r.de.ren Overall/Administer, authenticated
users get Overall/Read + Job/Read + View/Read.
- Node-RED: NODERED_ADMIN_USERS env list → permissions "*", others
["read"]. (TODO: switch to app-admin realm role.)
- JupyterHub: JUPYTERHUB_ADMIN_USERS env list. (Same TODO.)
- Gitea: r.de.ren pre-created as local admin; OIDC auto-links via email.
Docs:
- README, cloud/README, stacks/oauth2-proxy/README, and per-stack
READMEs updated to reflect the new state and remove resolved TODOs.
- cloud/.env.example gains all the new OIDC client + cookie-secret keys.
- cloud/README documents the full kcadm realm bootstrap, including the
hardcoded-audience mapper and post-logout redirect URIs that are
non-obvious gotchas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
infra
R&D infrastructure stacks for Waterschap Brabantse Delta. Hub-and-spoke deployment: one cloud central hub + per-plant edge sites.
Layout
infra/
├── stacks/ # reusable, runnable stack defs (kebab-case)
├── cloud/ # the single central hub
├── sites/ # per-plant edge deployments
└── docs/ # architecture + conventions
Stacks are pulled into the cloud and site composes via the Compose Spec include: directive. Each stack is also runnable standalone for testing.
Quick start
# Cloud hub (run on the central server)
cd cloud
cp .env.example .env # fill in real secrets
./deploy.sh # one-shot bring-up + Let's Encrypt + smoke test
# A plant edge (run on the edge gateway at the plant)
cd sites/<plant>
cp .env.example .env
docker compose up -d
After deploy.sh finishes, see cloud/README.md for the one-time Keycloak realm bootstrap that wires every app to Keycloak SSO.
Stacks
| Stack | Purpose | Cloud | Edge |
|---|---|---|---|
| node-red | Flow-based automation | ✓ | ✓ |
| influxdb | Time-series database | ✓ | ✓ |
| grafana | Dashboards / SCADA | ✓ | ✓ |
| keycloak | Identity / SSO | ✓ | ✓ |
| portainer | Container management UI | ✓ | ✓ |
| nginx-proxy | Stock nginx + certbot sidecar | ✓ | ✓ |
| rabbitmq | General-purpose broker (AMQP + MQTT plugin) | ✓ | ✓ |
| postfix | Outbound mail relay | ✓ | ✓ |
| wireguard-server | VPN server | ✓ | — |
| wireguard-client | VPN client | — | ✓ |
| gitea | Git server (HTTPS-only) | ✓ | — |
| jenkins | CI/CD | ✓ | — |
| sql | Config DB (postgres 16) | ✓ | — |
| mlflow | ML experiment tracking + registry | ✓ | — |
| jupyterhub | Multi-user notebook server | ✓ | — |
| frost | OGC SensorThings API (postgis + dedicated bus) | ✓ | — |
| oauth2-proxy | Keycloak SSO gate (auth_request sidecar) for apps without native OIDC | ✓ | — |
Sites
| Site | Status |
|---|---|
| gemaal1 | Scaffolded — awaiting hardware provisioning |
Design
See docs/architecture.md for the hub-and-spoke topology, 4-network model, ingress table, and the reasoning behind each choice.
Conventions
- kebab-case folder names
compose.yml(Compose Spec), notdocker-compose.yml- Stack composes pulled into cloud/site via
include: - Secrets in
.envfiles (gitignored);.env.examplecommitted with placeholders - OT layer (OPCUA, PLCs) is out of scope for this repo
Description
Languages
Shell
56.5%
JavaScript
26.8%
Python
13.4%
Dockerfile
3.3%