Subdomain rename (Versio side keeps original tool-named hostnames)
- nginx vhosts updated:
grafana -> dash.wbd-rd.nl
gitea -> git.wbd-rd.nl
keycloak -> auth.wbd-rd.nl
node-red -> flow.wbd-rd.nl
mlflow -> ml.wbd-rd.nl
jupyter -> hub.wbd-rd.nl
portainer -> ops.wbd-rd.nl
rabbitmq -> mq.wbd-rd.nl
jenkins -> ci.wbd-rd.nl
mqtt -> mqtt.wbd-rd.nl (no Versio conflict assumed)
- nginx-proxy README: bootstrap cert -d list + DNS A-record prereqs updated
- cloud/.env.example: GITEA_ROOT_URL, GRAFANA_ROOT_URL, KEYCLOAK_HOSTNAME
Function-based names are tool-agnostic (a Grafana -> Kibana swap leaves
dash.wbd-rd.nl meaningful) and avoid one-off "*2" suffixes.
Keycloak hardening
- Switch backend from bundled file storage to postgres (keycloak DB
already provisioned by sql/config/init.d/01-databases.sh).
- KC_HOSTNAME=auth.wbd-rd.nl, KC_PROXY_HEADERS=xforwarded for nginx
reverse-proxy posture; KC_HTTP_ENABLED=true since nginx terminates TLS.
- Added KC_HOSTNAME_STRICT, KC_HEALTH_ENABLED, KC_METRICS_ENABLED.
- Service joins app + mgmt + data networks (data needed for postgres).
- Mounted config/realms/ for realm-as-code (kc.sh import) — TODO to
populate once realm + clients are designed.
- README documents the recommended realm structure (wbd realm, one
client per app with redirect URIs) and the oauth2-proxy approach
for apps without native OIDC (mlflow, portainer-CE).
cloud
- Uncomment keycloak include in cloud/compose.yml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
4.5 KiB
Markdown
101 lines
4.5 KiB
Markdown
# nginx-proxy
|
|
|
|
The single web ingress for cloud + edge. Reverse-proxies HTTPS UIs and stream-proxies MQTT-TLS to RabbitMQ. TLS certs managed by a certbot sidecar (Let's Encrypt).
|
|
|
|
- **Image**: stock `nginx:1.27-alpine` (we don't use `nginxproxy/nginx-proxy` because we need the `stream {}` context for MQTT-TLS)
|
|
- **Sidecar**: `certbot/certbot:latest` — renews every 12h via HTTP-01 webroot challenges
|
|
- **Networks**: `edge` (the only port-publisher) + `app` (talks to upstream services)
|
|
- **Host ports**: `tcp/80`, `tcp/443`, `tcp/8883`
|
|
|
|
## Cert strategy
|
|
|
|
**Interim (Versio DNS)**: HTTP-01 SAN cert covering all subdomains, issued via `--webroot`. Requires:
|
|
- Public DNS A records for each subdomain pointing at the cloud host
|
|
- `tcp/80` reachable from the internet
|
|
|
|
**After TransIP migration**: switch to DNS-01 wildcard (`*.wbd-rd.nl`). Swap the `certbot/certbot` image for a build that includes `certbot-dns-transip` and reissue with `--cert-name infra` so the cert path stays stable — **no vhost config changes needed**.
|
|
|
|
## Config layout
|
|
|
|
```
|
|
config/
|
|
├── nginx.conf # base — http + stream contexts
|
|
├── conf.d/
|
|
│ ├── 00-default.conf # port 80: ACME challenge + HTTPS redirect
|
|
│ ├── grafana.conf # grafana.wbd-rd.nl
|
|
│ ├── gitea.conf # gitea.wbd-rd.nl
|
|
│ ├── keycloak.conf # keycloak.wbd-rd.nl
|
|
│ ├── nodered.conf # nodered.wbd-rd.nl
|
|
│ ├── mlflow.conf # mlflow.wbd-rd.nl
|
|
│ ├── jupyter.conf # jupyter.wbd-rd.nl
|
|
│ ├── portainer.conf # portainer.wbd-rd.nl (HTTPS upstream)
|
|
│ ├── rabbitmq.conf # rabbitmq.wbd-rd.nl (mgmt UI)
|
|
│ └── jenkins.conf # jenkins.wbd-rd.nl
|
|
└── stream.d/
|
|
└── mqtt.conf # mqtt.wbd-rd.nl:8883 → rabbitmq:1883
|
|
```
|
|
|
|
Volumes:
|
|
- `nginx-certs` — Let's Encrypt cert chains at `/etc/letsencrypt/`; read-only into nginx, writable from certbot
|
|
- `nginx-acme-challenge` — webroot for HTTP-01 challenges at `/var/www/certbot/`
|
|
|
|
All vhosts reference `/etc/letsencrypt/live/infra/fullchain.pem` and `privkey.pem` — a stable path independent of the issuance method.
|
|
|
|
## First-run bootstrap
|
|
|
|
The HTTPS server blocks won't load without a cert at `/etc/letsencrypt/live/infra/`. Bootstrap procedure (one-time):
|
|
|
|
```bash
|
|
cd stacks/nginx-proxy
|
|
|
|
# 1. Self-signed fallback so nginx starts and serves /.well-known/acme-challenge/
|
|
docker compose run --rm --entrypoint=/bin/sh nginx -c \
|
|
"mkdir -p /etc/letsencrypt/live/infra && \
|
|
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
|
|
-keyout /etc/letsencrypt/live/infra/privkey.pem \
|
|
-out /etc/letsencrypt/live/infra/fullchain.pem \
|
|
-subj '/CN=bootstrap-infra'"
|
|
|
|
# 2. Start nginx (HTTPS blocks load with the dummy cert)
|
|
docker compose up -d nginx
|
|
|
|
# 3. Issue the real cert via HTTP-01
|
|
docker compose run --rm certbot certonly \
|
|
--webroot -w /var/www/certbot \
|
|
--email "$LETSENCRYPT_EMAIL" --agree-tos --no-eff-email \
|
|
--cert-name infra \
|
|
-d git.wbd-rd.nl -d auth.wbd-rd.nl -d dash.wbd-rd.nl \
|
|
-d flow.wbd-rd.nl -d ml.wbd-rd.nl -d hub.wbd-rd.nl \
|
|
-d ops.wbd-rd.nl -d mq.wbd-rd.nl -d ci.wbd-rd.nl \
|
|
-d mqtt.wbd-rd.nl
|
|
|
|
# 4. Reload nginx to pick up the real cert
|
|
docker compose exec nginx nginx -s reload
|
|
```
|
|
|
|
The certbot sidecar then renews every 12h automatically.
|
|
|
|
## DNS prereqs (HTTP-01)
|
|
|
|
Before bootstrap, ensure A records exist in Versio for the 10 new short subdomains (the canonical tool-named ones — `gitea.wbd-rd.nl`, `grafana.wbd-rd.nl`, etc. — stay pointed at the existing Versio stack during the transition):
|
|
|
|
```
|
|
git.wbd-rd.nl A <cloud-public-ip> # gitea (new)
|
|
auth.wbd-rd.nl A <cloud-public-ip> # keycloak
|
|
dash.wbd-rd.nl A <cloud-public-ip> # grafana (new)
|
|
flow.wbd-rd.nl A <cloud-public-ip> # node-red (new)
|
|
ml.wbd-rd.nl A <cloud-public-ip> # mlflow
|
|
hub.wbd-rd.nl A <cloud-public-ip> # jupyterhub
|
|
ops.wbd-rd.nl A <cloud-public-ip> # portainer
|
|
mq.wbd-rd.nl A <cloud-public-ip> # rabbitmq mgmt UI
|
|
ci.wbd-rd.nl A <cloud-public-ip> # jenkins
|
|
mqtt.wbd-rd.nl A <cloud-public-ip> # MQTT-TLS broker
|
|
```
|
|
|
|
## TODO
|
|
|
|
- Wildcard cert via `certbot-dns-transip` (post TransIP migration)
|
|
- OIDC `auth_request` to Keycloak in front of services without native OIDC (mlflow, portainer-CE)
|
|
- Edge-side variant: bind to plant-LAN IP, internal CA for `*.local` hostnames
|
|
- HSTS + security headers (`add_header Strict-Transport-Security ...`)
|