Docker audit fixes #1

Merged
NotS1mp merged 9 commits from docker-audit-fixes into master 2026-09-24 15:21:57 +00:00
6 changed files with 108 additions and 4 deletions
Showing only changes of commit 6dcbd9589a - Show all commits
+5
View File
@@ -17,6 +17,11 @@ ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters
ZITADEL_EXTERNAL_DOMAIN=host.docker.internal ZITADEL_EXTERNAL_DOMAIN=host.docker.internal
ZITADEL_EXTERNAL_PORT=8080 ZITADEL_EXTERNAL_PORT=8080
ZITADEL_EXTERNAL_SECURE=false ZITADEL_EXTERNAL_SECURE=false
ZITADEL_PUBLIC_SCHEME=http
# Login V2 UI (zitadel-login). In dev it has its own port; the URL has no
# trailing slash. Only read when the Zitadel instance is created.
ZITADEL_LOGIN_HOST_PORT=3100
ZITADEL_LOGIN_BASE_URL=http://host.docker.internal:3100/ui/v2/login
ZITADEL_DATABASE_PASSWORD=zitadel ZITADEL_DATABASE_PASSWORD=zitadel
# Console login: <username>@zitadel.<external domain> # Console login: <username>@zitadel.<external domain>
ZITADEL_ADMIN_USERNAME=admin ZITADEL_ADMIN_USERNAME=admin
+3
View File
@@ -20,6 +20,9 @@ ZITADEL_EXTERNAL_DOMAIN=auth.example.com
ZITADEL_EXTERNAL_PORT=443 ZITADEL_EXTERNAL_PORT=443
ZITADEL_EXTERNAL_SECURE=true ZITADEL_EXTERNAL_SECURE=true
ZITADEL_TLS_MODE=external ZITADEL_TLS_MODE=external
ZITADEL_PUBLIC_SCHEME=https
# Login V2 UI, routed by the edge under the same hostname. No trailing slash.
ZITADEL_LOGIN_BASE_URL=https://auth.example.com/ui/v2/login
ZITADEL_DATABASE_PASSWORD=CHANGE_ME ZITADEL_DATABASE_PASSWORD=CHANGE_ME
ZITADEL_ORG_NAME=Iker Demo ZITADEL_ORG_NAME=Iker Demo
ZITADEL_ADMIN_USERNAME=admin@ikerdemo.cat ZITADEL_ADMIN_USERNAME=admin@ikerdemo.cat
+34 -2
View File
@@ -37,7 +37,7 @@ cp .env.example .env
docker compose -f compose.yml -f compose.dev.yml up -d docker compose -f compose.yml -f compose.dev.yml up -d
``` ```
That starts Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger, with their That starts Postgres, Zitadel (and its login UI), SeaweedFS, RabbitMQ and Jaeger, with their
ports published to `localhost`. Add `--profile tools` to also start pgAdmin. ports published to `localhost`. Add `--profile tools` to also start pgAdmin.
In production, the base `compose.yml` file **is** the production shape In production, the base `compose.yml` file **is** the production shape
@@ -60,6 +60,7 @@ external network to reach `postgres`, `zitadel`, `seaweedfs`, `rabbitmq` and
|---|---|---| |---|---|---|
| PostgreSQL | `5432` | one instance, one database per project | | PostgreSQL | `5432` | one instance, one database per project |
| Zitadel | `8080` | OIDC/OAuth2, console at `/ui/console` | | Zitadel | `8080` | OIDC/OAuth2, console at `/ui/console` |
| Zitadel login | `3100` | Login V2 UI at `/ui/v2/login` (under `8080`'s hostname in prod) |
| SeaweedFS | `8333` / `8888` / `9333` | S3 API / filer / master | | SeaweedFS | `8333` / `8888` / `9333` | S3 API / filer / master |
| RabbitMQ | `5672` / `15672` | AMQP / management UI | | RabbitMQ | `5672` / `15672` | AMQP / management UI |
| Jaeger | `16686` / `4317` / `4318` | UI / OTLP gRPC / OTLP HTTP | | Jaeger | `16686` / `4317` / `4318` | UI / OTLP gRPC / OTLP HTTP |
@@ -95,6 +96,37 @@ with an example value that assumes these repos happen to sit as sibling
folders. If yours don't, change it. Not versioned, and regenerated whenever folders. If yours don't, change it. Not versioned, and regenerated whenever
the Zitadel volume is reset. the Zitadel volume is reset.
### The Zitadel login (Login V2)
Zitadel v4 serves its sign-in pages from a separate container,
`zitadel-login` (Next.js), under `/ui/v2/login`. Zitadel redirects every
login there, so it has to be running for anyone to sign in. It calls the
Zitadel API with its own service account (`login-client`), whose PAT Zitadel
writes on init to the `zitadel_bootstrap` volume, which only `zitadel-login`
mounts.
- **Dev**: no proxy, so the login has its own port (`ZITADEL_LOGIN_HOST_PORT`,
`3100`) and `ZITADEL_LOGIN_BASE_URL` points the browser at it.
- **Prod**: the edge routes `/ui/v2/login` on Zitadel's hostname to
`infra-zitadel-login`, and everything else to Zitadel.
- `zitadel` and `zitadel-login` are pinned to the same version; bump both
together.
`ZITADEL_LOGIN_BASE_URL` and the `login-client` PAT are only applied when the
instance is created. On an instance created before `zitadel-login` existed:
1. In the console, create a service user `login-client`, give it the
instance role **IAM Login Client**, and create a PAT for it.
2. Copy it into the volume and restart the login:
```bash
docker cp login-client.pat infra-zitadel:/bootstrap/login-client.pat
docker restart infra-zitadel-login
```
3. If the instance still uses the old login, set Login V2 as required with
base URI `${ZITADEL_LOGIN_BASE_URL}/` in the instance's feature settings.
## The public edge ## The public edge
`edge/compose.yml` is the only thing that publishes host ports 80/443: one `edge/compose.yml` is the only thing that publishes host ports 80/443: one
@@ -138,7 +170,7 @@ re-creates its piece on the next `up`, since both are idempotent.
``` ```
Infrastructure/ Infrastructure/
├─ compose.yml # postgres, zitadel, seaweedfs, rabbitmq, jaeger: the prod shape ├─ compose.yml # postgres, zitadel (+ login), seaweedfs, rabbitmq, jaeger: the prod shape
├─ edge/ # shared public nginx + certbot (prod only): compose.yml, conf.d/, snippets/ ├─ edge/ # shared public nginx + certbot (prod only): compose.yml, conf.d/, snippets/
├─ compose.dev.yml # dev overlay: publishes ports, adds pgAdmin ├─ compose.dev.yml # dev overlay: publishes ports, adds pgAdmin
├─ .env.example / .env.prod.example ├─ .env.example / .env.prod.example
+6
View File
@@ -15,6 +15,12 @@ services:
ports: ports:
- "${ZITADEL_EXTERNAL_PORT}:8080" - "${ZITADEL_EXTERNAL_PORT}:8080"
# No edge in dev to put the login under Zitadel's own port, so it gets its
# own (ZITADEL_LOGIN_BASE_URL in .env points the browser at it).
zitadel-login:
ports:
- "${ZITADEL_LOGIN_HOST_PORT}:3000"
seaweedfs: seaweedfs:
ports: ports:
- "9333:9333" # master / admin UI - "9333:9333" # master / admin UI
+53 -1
View File
@@ -45,8 +45,13 @@ services:
# setup (both idempotent) and then starts, so the same command works on a # setup (both idempotent) and then starts, so the same command works on a
# cold and on a warm volume. # cold and on a warm volume.
zitadel: zitadel:
image: ghcr.io/zitadel/zitadel:latest # Pinned together with zitadel-login below: bump both at once.
image: ghcr.io/zitadel/zitadel:v4.19.1
container_name: infra-zitadel container_name: infra-zitadel
# The image runs as uid 1000, but Docker creates both the bind mount
# (on Linux) and the bootstrap volume as root, so Zitadel couldn't write
# the PATs into them. Same as Zitadel's own compose.
user: "0"
# `disabled` (dev, plain http) makes Zitadel advertise http URLs; behind a # `disabled` (dev, plain http) makes Zitadel advertise http URLs; behind a
# TLS-terminating proxy it must be `external` or the console calls # TLS-terminating proxy it must be `external` or the console calls
# http://<domain> and the CSP blocks it. # http://<domain> and the CSP blocks it.
@@ -86,11 +91,57 @@ services:
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin Service Account ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin Service Account
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z" ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z"
ZITADEL_FIRSTINSTANCE_PATPATH: /output/admin-sa.pat ZITADEL_FIRSTINSTANCE_PATPATH: /output/admin-sa.pat
# Service account the login UI (zitadel-login) calls the API with. Its
# PAT goes to a volume only that container mounts, not to /output.
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Automatically Initialized IAM_LOGIN_CLIENT
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z"
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /bootstrap/login-client.pat
# Login V2: sign-in pages are served by zitadel-login, not by Zitadel
# itself. Like the FIRSTINSTANCE values, these are only read when the
# instance is created (see README for an existing instance).
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true"
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: ${ZITADEL_LOGIN_BASE_URL}/
ZITADEL_OIDC_DEFAULTLOGINURLV2: ${ZITADEL_LOGIN_BASE_URL}/login?authRequest=
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: ${ZITADEL_LOGIN_BASE_URL}/logout?post_logout_redirect=
ZITADEL_SAML_DEFAULTLOGINURLV2: ${ZITADEL_LOGIN_BASE_URL}/login?samlRequest=
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
volumes: volumes:
- ./zitadel/.output:/output - ./zitadel/.output:/output
- zitadel_bootstrap:/bootstrap
healthcheck:
test: ["CMD", "/app/zitadel", "ready"]
interval: 10s
timeout: 30s
retries: 12
start_period: 20s
networks: [infra-net]
restart: unless-stopped
# Login V2 UI (Next.js), served under /ui/v2/login. It calls the API
# directly on infra-net, so it has to present the public Host/scheme for
# Zitadel to find the instance.
zitadel-login:
image: ghcr.io/zitadel/zitadel-login:v4.19.1
container_name: infra-zitadel-login
environment:
ZITADEL_API_URL: http://zitadel:8080
NEXT_PUBLIC_BASE_PATH: /ui/v2/login
ZITADEL_SERVICE_USER_TOKEN_FILE: /bootstrap/login-client.pat
CUSTOM_REQUEST_HEADERS: Host:${ZITADEL_EXTERNAL_DOMAIN},X-Forwarded-Proto:${ZITADEL_PUBLIC_SCHEME}
healthcheck:
test: ["CMD", "/bin/sh", "-c", "node /app/healthcheck.mjs http://localhost:3000/ui/v2/login/healthy"]
interval: 10s
timeout: 30s
retries: 12
start_period: 20s
depends_on:
zitadel:
condition: service_healthy
volumes:
- zitadel_bootstrap:/bootstrap:ro
networks: [infra-net] networks: [infra-net]
restart: unless-stopped restart: unless-stopped
@@ -166,5 +217,6 @@ services:
volumes: volumes:
postgres_data: postgres_data:
zitadel_bootstrap:
seaweedfs_data: seaweedfs_data:
rabbitmq_data: rabbitmq_data:
+7 -1
View File
@@ -80,11 +80,17 @@ server {
ssl_certificate_key /etc/letsencrypt/live/musify.ikerdemo.cat/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/musify.ikerdemo.cat/privkey.pem;
include /etc/nginx/snippets/tls.conf; include /etc/nginx/snippets/tls.conf;
# Login V2 UI, a separate container (see ../compose.yml).
location /ui/v2/login {
set $upstream http://infra-zitadel-login:3000;
proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf;
}
location / { location / {
set $upstream http://zitadel:8080; set $upstream http://zitadel:8080;
proxy_pass $upstream; proxy_pass $upstream;
include /etc/nginx/snippets/proxy.conf; include /etc/nginx/snippets/proxy.conf;
grpc_set_header Host $host;
} }
} }