Files
infra/stacks/nginx-proxy/README.md
znetsixe f69453df99 refactor(dns): rename frost.wbd-rd.nl → sta.wbd-rd.nl; drop redundant portainer.wbd-rd.nl
Match the short-functional naming convention used by the other vhosts
(git, auth, dash, flow, ml, hub, ops, mq, ci, mqtt). FROST implements
OGC SensorThings API, so `sta` is the natural fit.

portainer.wbd-rd.nl is dropped from deploy.sh HOSTS — there is no
nginx vhost for it; portainer is already served via ops.wbd-rd.nl.

DNS prereq for first deploy is now: create one new A record for
sta.wbd-rd.nl → cloud public IP. All other short subdomains already
point correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:46:32 +02:00

105 lines
4.7 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 # dash.wbd-rd.nl
│ ├── gitea.conf # git.wbd-rd.nl
│ ├── keycloak.conf # auth.wbd-rd.nl
│ ├── nodered.conf # flow.wbd-rd.nl
│ ├── mlflow.conf # ml.wbd-rd.nl
│ ├── jupyter.conf # hub.wbd-rd.nl
│ ├── portainer.conf # ops.wbd-rd.nl
│ ├── rabbitmq.conf # mq.wbd-rd.nl (mgmt UI)
│ ├── jenkins.conf # ci.wbd-rd.nl
│ └── frost.conf # sta.wbd-rd.nl (FROST / SensorThings)
└── 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 -d sta.wbd-rd.nl
# Easier: from the cloud directory just run ./deploy.sh — it handles steps 1-4.
# 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 11 short functional 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
sta.wbd-rd.nl A <cloud-public-ip> # FROST / SensorThings API
```
## 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 ...`)