feat(openzaak): real secrets + TLS for the production OpenZaak harness (WP-55)
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 12s
CI / frontend (push) Successful in 14s
CI / storybook-a11y (push) Successful in 17s
CI / backend (push) Successful in 1m51s
CI / semgrep (push) Successful in 1m13s
CI / e2e (push) Successful in 2m56s
CI / api-client-drift (push) Successful in 1m41s
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 12s
CI / frontend (push) Successful in 14s
CI / storybook-a11y (push) Successful in 17s
CI / backend (push) Successful in 1m51s
CI / semgrep (push) Successful in 1m13s
CI / e2e (push) Successful in 2m56s
CI / api-client-drift (push) Successful in 1m41s
docker-compose.openzaak.prod.yml layers real SECRET_KEY/DB password/site
domain/allowed-hosts (all required, fail-fast via ${VAR:?...}) on top of the
WP-54 dev harness, switches Postgres off trust auth, and sets IS_HTTPS for a
front-facing reverse-proxy TLS setup. The ZGW client secret lives inside a
file setup_configuration reads rather than a compose env var, so it's
templated (data.prod.yaml.template, no secret) and rendered host-side via
render-prod-secrets.sh into a gitignored data.prod.yaml, mounted over the
container's dev data.yaml. ZgwOptions.cs already binds from IConfiguration,
so the BFF side needed no code change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,3 +54,6 @@ storybook-static
|
|||||||
|
|
||||||
# WP-54: bootstrap-catalogus.sh's own record of what it seeded into a local OpenZaak run
|
# WP-54: bootstrap-catalogus.sh's own record of what it seeded into a local OpenZaak run
|
||||||
backend/openzaak/seeded.env
|
backend/openzaak/seeded.env
|
||||||
|
|
||||||
|
# WP-55: render-prod-secrets.sh's output — the real client secret, never committed
|
||||||
|
backend/openzaak/setup_configuration/data.prod.yaml
|
||||||
|
|||||||
@@ -50,6 +50,40 @@ up, so it never runs where the harness doesn't exist.
|
|||||||
docker compose -f docker-compose.openzaak.yml down -v
|
docker compose -f docker-compose.openzaak.yml down -v
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Production (WP-55)
|
||||||
|
|
||||||
|
This dev harness stays dev-only: hardcoded `SECRET_KEY`, `POSTGRES_HOST_AUTH_METHOD=trust`,
|
||||||
|
`IS_HTTPS: 'no'`, a client secret checked into `setup_configuration/data.yaml`. A real
|
||||||
|
deployment layers `docker-compose.openzaak.prod.yml` on top instead of replacing anything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENZAAK_SECRET_KEY=... # Django SECRET_KEY — generate, don't reuse the dev value
|
||||||
|
export OPENZAAK_DB_PASSWORD=... # postgres password (switches auth off `trust`)
|
||||||
|
export OPENZAAK_SITE_DOMAIN=... # e.g. open-zaak.example.org — no scheme/port
|
||||||
|
export OPENZAAK_ALLOWED_HOSTS=... # Django ALLOWED_HOSTS, usually the same domain
|
||||||
|
export OPENZAAK_CLIENT_ID=... # the BFF's OpenZaak client id (ZgwOptions:ClientId)
|
||||||
|
export OPENZAAK_CLIENT_SECRET=... # the BFF's JWT signing secret (ZgwOptions:Secret)
|
||||||
|
export OPENZAAK_APPLICATIE_UUID=$(uuidgen)
|
||||||
|
|
||||||
|
./render-prod-secrets.sh # writes the gitignored setup_configuration/data.prod.yaml
|
||||||
|
docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Every one of those env vars is `${VAR:?...}`-checked — compose (and `render-prod-secrets.sh`
|
||||||
|
for the client secret) refuses to start rather than silently falling back to a dev-looking
|
||||||
|
default. There is no env var that "means insecure default"; if it's unset, it's a hard error.
|
||||||
|
|
||||||
|
**TLS**: OpenZaak itself does no certificate handling. Put a reverse proxy/ingress (the same
|
||||||
|
one fronting the BFF) in front of `web`'s `:8000`, terminate TLS there, and forward to
|
||||||
|
`http://web:8000` over the compose network. `IS_HTTPS: 'yes'` in the prod override only tells
|
||||||
|
Django it's being served over HTTPS (secure cookies, `SECURE_*` redirects) — it does not open
|
||||||
|
a TLS listener itself.
|
||||||
|
|
||||||
|
The BFF side needs no code change: `ZgwOptions` already binds `ClientId`/`Secret`/the base
|
||||||
|
URLs from `IConfiguration`, so pointing it at a production OpenZaak is a config change
|
||||||
|
(`Zgw:ClientId`/`Zgw:Secret`/`Zgw:ZrcBaseUrl` etc. via env vars or a secrets manager), not an
|
||||||
|
app change.
|
||||||
|
|
||||||
## What's in here / what isn't
|
## What's in here / what isn't
|
||||||
|
|
||||||
- `docker-compose.openzaak.yml` — postgres (postgis), redis, a one-shot `web-init` (runs
|
- `docker-compose.openzaak.yml` — postgres (postgis), redis, a one-shot `web-init` (runs
|
||||||
@@ -72,3 +106,11 @@ docker compose -f docker-compose.openzaak.yml down -v
|
|||||||
`procesType` on the public VNG selectielijst API).
|
`procesType` on the public VNG selectielijst API).
|
||||||
- **Not here**: Documenten (DRC) / Notificaties (NRC) content — add if a later WP needs to prove
|
- **Not here**: Documenten (DRC) / Notificaties (NRC) content — add if a later WP needs to prove
|
||||||
those round-trips against a live instance too (WP-51/52 are fixture-tested today).
|
those round-trips against a live instance too (WP-51/52 are fixture-tested today).
|
||||||
|
- `docker-compose.openzaak.prod.yml` (WP-55) — production overrides layered on top of
|
||||||
|
`docker-compose.openzaak.yml`: real `SECRET_KEY`/DB password/site domain/allowed-hosts from
|
||||||
|
required env vars (fails fast if unset), password DB auth instead of `trust`, `IS_HTTPS: 'yes'`.
|
||||||
|
Adds no image/service of its own — see "Production" above for the full flow.
|
||||||
|
- `setup_configuration/data.prod.yaml.template` (WP-55) — the prod counterpart of `data.yaml`
|
||||||
|
with no secret in it (`${OPENZAAK_CLIENT_SECRET}` etc. as placeholders); `render-prod-secrets.sh`
|
||||||
|
fills it in to the gitignored `data.prod.yaml`, which the prod compose override mounts over
|
||||||
|
the container's `data.yaml`.
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# WP-55 — production overrides for docker-compose.openzaak.yml: real secrets, real DB auth,
|
||||||
|
# HTTPS-aware settings. Use ON TOP of the base file, never alone (it has no image/ports of its
|
||||||
|
# own to add — see backend/openzaak/README.md for the required env vars and full flow):
|
||||||
|
#
|
||||||
|
# ./render-prod-secrets.sh # renders setup_configuration/data.prod.yaml (gitignored)
|
||||||
|
# docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.prod.yml up -d
|
||||||
|
#
|
||||||
|
# TLS is NOT terminated here — OpenZaak sits behind a reverse proxy/ingress that owns the
|
||||||
|
# certificate; this file only tells OpenZaak (via IS_HTTPS) that it's being served over HTTPS
|
||||||
|
# so it sets secure cookies / redirects correctly.
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
environment:
|
||||||
|
- POSTGRES_HOST_AUTH_METHOD=md5
|
||||||
|
- POSTGRES_PASSWORD=${OPENZAAK_DB_PASSWORD:?OPENZAAK_DB_PASSWORD must be set}
|
||||||
|
|
||||||
|
web-init:
|
||||||
|
environment:
|
||||||
|
SECRET_KEY: ${OPENZAAK_SECRET_KEY:?OPENZAAK_SECRET_KEY must be set}
|
||||||
|
DB_PASSWORD: ${OPENZAAK_DB_PASSWORD:?OPENZAAK_DB_PASSWORD must be set}
|
||||||
|
IS_HTTPS: 'yes'
|
||||||
|
SITE_DOMAIN: ${OPENZAAK_SITE_DOMAIN:?OPENZAAK_SITE_DOMAIN must be set}
|
||||||
|
ALLOWED_HOSTS: ${OPENZAAK_ALLOWED_HOSTS:?OPENZAAK_ALLOWED_HOSTS must be set}
|
||||||
|
DISABLE_2FA: 'false'
|
||||||
|
volumes:
|
||||||
|
# Shadows the dev data.yaml (still mounted read-only from the base file) with the
|
||||||
|
# secret-free template rendered by render-prod-secrets.sh.
|
||||||
|
- ./setup_configuration/data.prod.yaml:/app/setup_configuration/data.yaml:ro
|
||||||
|
|
||||||
|
web:
|
||||||
|
environment:
|
||||||
|
SECRET_KEY: ${OPENZAAK_SECRET_KEY:?OPENZAAK_SECRET_KEY must be set}
|
||||||
|
DB_PASSWORD: ${OPENZAAK_DB_PASSWORD:?OPENZAAK_DB_PASSWORD must be set}
|
||||||
|
IS_HTTPS: 'yes'
|
||||||
|
SITE_DOMAIN: ${OPENZAAK_SITE_DOMAIN:?OPENZAAK_SITE_DOMAIN must be set}
|
||||||
|
ALLOWED_HOSTS: ${OPENZAAK_ALLOWED_HOSTS:?OPENZAAK_ALLOWED_HOSTS must be set}
|
||||||
|
DISABLE_2FA: 'false'
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# WP-55 — renders setup_configuration/data.prod.yaml.template into the gitignored
|
||||||
|
# data.prod.yaml docker-compose.openzaak.prod.yml mounts over the container's data.yaml.
|
||||||
|
# Run this once before `docker compose ... up` in a production deploy; re-run whenever the
|
||||||
|
# secrets rotate. Fails fast (no output file) if a required env var is missing — never
|
||||||
|
# silently falls back to a real-looking default.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
: "${OPENZAAK_SITE_DOMAIN:?OPENZAAK_SITE_DOMAIN must be set (e.g. open-zaak.example.org)}"
|
||||||
|
: "${OPENZAAK_CLIENT_ID:?OPENZAAK_CLIENT_ID must be set}"
|
||||||
|
: "${OPENZAAK_CLIENT_SECRET:?OPENZAAK_CLIENT_SECRET must be set}"
|
||||||
|
: "${OPENZAAK_APPLICATIE_UUID:?OPENZAAK_APPLICATIE_UUID must be set (a fresh UUID, e.g. \$(uuidgen))}"
|
||||||
|
|
||||||
|
envsubst < setup_configuration/data.prod.yaml.template > setup_configuration/data.prod.yaml
|
||||||
|
echo "Wrote setup_configuration/data.prod.yaml"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Prod counterpart of data.yaml (WP-54's dev-only version, kept as-is for local iteration —
|
||||||
|
# see docker-compose.openzaak.yml's own comment on why it hardcodes a client secret). This
|
||||||
|
# template has no secret in it; render-prod-secrets.sh substitutes OPENZAAK_CLIENT_SECRET
|
||||||
|
# into it to produce the gitignored data.prod.yaml that docker-compose.openzaak.prod.yml
|
||||||
|
# mounts over the container's data.yaml.
|
||||||
|
#
|
||||||
|
# Least-privilege client scopes (heeft_alle_autorisaties: true below) are WP-57's job, not
|
||||||
|
# this WP's — left matching the dev harness on purpose.
|
||||||
|
sites_config_enable: true
|
||||||
|
sites_config:
|
||||||
|
items:
|
||||||
|
- domain: ${OPENZAAK_SITE_DOMAIN}
|
||||||
|
name: OpenZaak (production)
|
||||||
|
|
||||||
|
vng_api_common_credentials_config_enable: true
|
||||||
|
vng_api_common_credentials:
|
||||||
|
items:
|
||||||
|
- identifier: ${OPENZAAK_CLIENT_ID}
|
||||||
|
secret: ${OPENZAAK_CLIENT_SECRET}
|
||||||
|
|
||||||
|
vng_api_common_applicaties_config_enable: true
|
||||||
|
vng_api_common_applicaties:
|
||||||
|
items:
|
||||||
|
- uuid: ${OPENZAAK_APPLICATIE_UUID}
|
||||||
|
client_ids:
|
||||||
|
- ${OPENZAAK_CLIENT_ID}
|
||||||
|
label: BIG-register BFF (production)
|
||||||
|
heeft_alle_autorisaties: true
|
||||||
@@ -105,7 +105,7 @@ for its existing violations, so every WP ends green.
|
|||||||
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done |
|
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done |
|
||||||
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done |
|
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done |
|
||||||
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | done |
|
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | done |
|
||||||
| [WP-55](WP-55-openzaak-secrets-tls.md) | Real secrets + TLS for the OpenZaak harness | 10 · OpenZaak hardening | todo |
|
| [WP-55](WP-55-openzaak-secrets-tls.md) | Real secrets + TLS for the OpenZaak harness | 10 · OpenZaak hardening | done |
|
||||||
| [WP-56](WP-56-openzaak-catalogus-provisioning.md) | Idempotent catalogus provisioning | 10 · OpenZaak hardening | todo |
|
| [WP-56](WP-56-openzaak-catalogus-provisioning.md) | Idempotent catalogus provisioning | 10 · OpenZaak hardening | todo |
|
||||||
| [WP-57](WP-57-openzaak-least-privilege-scopes.md) | Least-privilege client scopes | 10 · OpenZaak hardening | todo |
|
| [WP-57](WP-57-openzaak-least-privilege-scopes.md) | Least-privilege client scopes | 10 · OpenZaak hardening | todo |
|
||||||
| [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | todo |
|
| [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | todo |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WP-55 — Real secrets + TLS for the OpenZaak harness
|
# WP-55 — Real secrets + TLS for the OpenZaak harness
|
||||||
|
|
||||||
Status: todo
|
Status: done
|
||||||
Phase: 10 — OpenZaak production hardening
|
Phase: 10 — OpenZaak production hardening
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
@@ -45,10 +45,35 @@ change, not application code.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] No secret value is hardcoded in any committed compose/config file.
|
- [x] No secret value is hardcoded in any committed compose/config file.
|
||||||
- [ ] The production compose fails fast (or docs state clearly) when secrets aren't
|
- [x] The production compose fails fast (or docs state clearly) when secrets aren't
|
||||||
supplied — no silent fallback to a real-looking default.
|
supplied — no silent fallback to a real-looking default.
|
||||||
- [ ] README documents exactly which env vars must be set and how TLS is terminated.
|
- [x] README documents exactly which env vars must be set and how TLS is terminated.
|
||||||
|
|
||||||
|
## Deviation from the original plan
|
||||||
|
|
||||||
|
The WP's own "Files" section expected the secret to be parameterized directly inside
|
||||||
|
`docker-compose.openzaak.yml`'s environment or a straight env-var override. That covers
|
||||||
|
`SECRET_KEY`/DB password/`IS_HTTPS` fine (compose does key-based environment merging across
|
||||||
|
`-f` files even though the base file writes some blocks as YAML mappings and others as
|
||||||
|
anchors), but the ZGW client secret lives inside `setup_configuration/data.yaml`, a file
|
||||||
|
OpenZaak's own `setup_configuration` management command reads — compose has no mechanism to
|
||||||
|
interpolate env vars _inside_ a mounted file's contents. Solved by templating that one file
|
||||||
|
(`data.prod.yaml.template`, no secret) + a tiny host-side `render-prod-secrets.sh`
|
||||||
|
(`envsubst`, fail-fast via `${VAR:?...}`) that produces a gitignored `data.prod.yaml`, which
|
||||||
|
`docker-compose.openzaak.prod.yml` mounts over the container's `data.yaml` (bind-mounting a
|
||||||
|
single file inside an already bind-mounted read-only directory works fine in Docker/Podman —
|
||||||
|
verified via `docker compose config` with the override applied). No new dependency: `envsubst`
|
||||||
|
is part of `gettext`, already present on this machine.
|
||||||
|
|
||||||
|
Verified for real: `docker compose -f docker-compose.openzaak.yml -f
|
||||||
|
docker-compose.openzaak.prod.yml config` succeeds with all required env vars set and both
|
||||||
|
environment overrides (SECRET_KEY, DB password/auth method) present in the merged output;
|
||||||
|
fails with a clear `${VAR:?...}` error when any is missing. `render-prod-secrets.sh` itself
|
||||||
|
fails fast (tested) when `OPENZAAK_CLIENT_SECRET` etc. are unset, and its rendered
|
||||||
|
`data.prod.yaml` was inspected and matched the template with real values substituted.
|
||||||
|
`cd backend && dotnet test` (WP-54 harness untouched): 159/159 green. The dev harness
|
||||||
|
(`docker-compose.openzaak.yml` alone, `setup_configuration/data.yaml`) is untouched.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ read (this WP), the behandelportal needs it as the thing it advances (WP-65).
|
|||||||
- The DTO change is additive: the SSP's `pendingHerregistratie` boolean can be derived
|
- The DTO change is additive: the SSP's `pendingHerregistratie` boolean can be derived
|
||||||
from the new status field (or kept as a computed convenience) so this ships with zero
|
from the new status field (or kept as a computed convenience) so this ships with zero
|
||||||
required FE behavior change — a pure backend + contract widening.
|
required FE behavior change — a pure backend + contract widening.
|
||||||
- Only the status *value* is published here; any transition (advancing it) is a separate
|
- Only the status _value_ is published here; any transition (advancing it) is a separate
|
||||||
write endpoint, not part of this slice (that's WP-65's mutation).
|
write endpoint, not part of this slice (that's WP-65's mutation).
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
@@ -60,7 +60,7 @@ dashboard still shows the same pending/approved states it does today.
|
|||||||
|
|
||||||
## Out of scope
|
## Out of scope
|
||||||
|
|
||||||
Any endpoint that *advances* the status (WP-65); the behandelportal consuming it (WP-64).
|
Any endpoint that _advances_ the status (WP-65); the behandelportal consuming it (WP-64).
|
||||||
|
|
||||||
## Risks
|
## Risks
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Phase: 11 — Behandelportal
|
|||||||
|
|
||||||
The core case-treatment write path — a medewerker opens one aanvraag's detail (including
|
The core case-treatment write path — a medewerker opens one aanvraag's detail (including
|
||||||
its documents) and records a decision (goedkeuren/afwijzen/meer info opvragen), advancing
|
its documents) and records a decision (goedkeuren/afwijzen/meer info opvragen), advancing
|
||||||
the status lifecycle WP-63 published. This is the first genuinely new *write* capability
|
the status lifecycle WP-63 published. This is the first genuinely new _write_ capability
|
||||||
in the system beyond what the citizen SSP already does to itself.
|
in the system beyond what the citizen SSP already does to itself.
|
||||||
|
|
||||||
## Read first
|
## Read first
|
||||||
|
|||||||
Reference in New Issue
Block a user