Give every project its own credentials on the shared services

Every project used the Postgres superuser, the RabbitMQ administrator, an
admin S3 identity and the Zitadel instance-admin PAT, so any one of them
could read or change the others' data.

A new one-shot, `provision` (runs on every `up`, idempotent), gives each
project in x-projects its own:

- Postgres role, owner of its database. No database is open to PUBLIC
  anymore, so a role can only connect to its own. An existing database
  and everything in it is handed over to the project's role.
- RabbitMQ user, with permissions on its own vhost only.
- Zitadel service account, PROJECT_OWNER of its own project only; its
  PAT is written to zitadel/.output/<project>/ for that project's
  zitadel-init to mount instead of the admin PAT.

SeaweedFS renders one S3 identity per project, limited to its bucket, and
refuses to start with a clear message on a malformed or repeated key.

The admin accounts stay with this stack. postgres-init is gone: provision
creates the databases. *.sh files are checked out with LF so they run in
the Linux containers from a Windows checkout.
This commit is contained in:
2026-09-24 17:05:12 +02:00
parent 668cdb17b0
commit 33bf75d245
8 changed files with 458 additions and 55 deletions
+71 -21
View File
@@ -22,15 +22,21 @@ x-logging: &default-logging
max-size: "10m"
max-file: "3"
# The consuming projects that get their own credentials (see provision/).
# Adding one: add it here, give it the <PROJECT>_* settings in both
# `provision` and `seaweedfs` below, and their values in the env files.
x-projects: &projects musify ping dockermanager
networks:
infra-net:
name: infra-net
services:
# --- Database ---------------------------------------------------------
# One instance, one database per consuming project (see postgres-init/).
# Zitadel provisions its own `zitadel` database itself, from the admin
# credentials below, so it does not need an init script.
# One instance, one database and one role per consuming project (created
# by `provision` below). Zitadel creates its own `zitadel` database and
# user itself, from the admin credentials. The superuser is only for this
# stack: no project gets it.
postgres:
image: postgres:17-alpine
container_name: infra-postgres
@@ -39,7 +45,6 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./postgres-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
@@ -94,8 +99,8 @@ services:
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_DISPLAYNAME: ${ZITADEL_ADMIN_DISPLAY_NAME:-ZITADEL Admin}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_ADMIN_PASSWORD}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false"
# Service account + PAT written on init; each consuming project's own
# zitadel-init one-shot uses it to create its own project/apps.
# Instance-admin service account + PAT written on init. Only `provision`
# uses it, to give each project its own, project-scoped account.
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin Service Account
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z"
@@ -158,27 +163,31 @@ services:
# --- Storage --------------------------------------------------------------
# S3-compatible object store shared by every project. All-in-one: master +
# volume + filer + S3 gateway. The S3 identity file is rendered from the
# environment on start so the credentials can never drift from the ones
# handed to consuming projects. No bucket is created here; each
# consuming project's own seaweedfs-init one-shot creates its own.
# volume + filer + S3 gateway. The S3 identities are rendered from the
# environment on start (provision/seaweedfs-s3-config.sh): an admin one
# for this stack, and one per project limited to that project's bucket.
# No bucket is created here; each consuming project's own seaweedfs-init
# one-shot creates its own.
seaweedfs:
image: chrislusf/seaweedfs:4.47
container_name: infra-seaweedfs
environment:
PROJECTS: *projects
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
MUSIFY_S3_ACCESS_KEY: ${MUSIFY_S3_ACCESS_KEY:-}
MUSIFY_S3_SECRET_KEY: ${MUSIFY_S3_SECRET_KEY:-}
MUSIFY_S3_BUCKET: webapi-storage
PING_S3_ACCESS_KEY: ${PING_S3_ACCESS_KEY:-}
PING_S3_SECRET_KEY: ${PING_S3_SECRET_KEY:-}
PING_S3_BUCKET: ping-storage
DOCKERMANAGER_S3_ACCESS_KEY: ${DOCKERMANAGER_S3_ACCESS_KEY:-}
DOCKERMANAGER_S3_SECRET_KEY: ${DOCKERMANAGER_S3_SECRET_KEY:-}
DOCKERMANAGER_S3_BUCKET: dockermanager
entrypoint: ["/bin/sh", "-c"]
command:
- |
cat > /tmp/s3.conf <<'JSON'
{
"identities": [
{
"name": "infra",
"credentials": [{ "accessKey": "${S3_ACCESS_KEY}", "secretKey": "${S3_SECRET_KEY}" }],
"actions": ["Admin", "Read", "Write"]
}
]
}
JSON
sh /provision/seaweedfs-s3-config.sh >/tmp/s3.conf || exit 1
exec weed server \
-ip.bind=0.0.0.0 \
-dir=/data \
@@ -189,6 +198,7 @@ services:
-s3.config=/tmp/s3.conf
volumes:
- seaweedfs_data:/data
- ./provision:/provision:ro
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9333/cluster/healthz"]
interval: 10s
@@ -243,6 +253,46 @@ services:
logging: *default-logging
restart: unless-stopped
# --- Per-project credentials ----------------------------------------------
# One-shot, runs on every `up` (idempotent): each project's own Postgres
# role/database, RabbitMQ user/vhost and Zitadel service account, from the
# <PROJECT>_* settings (see provision/provision.sh). An empty setting skips
# that service for that project.
provision:
image: postgres:17-alpine
container_name: infra-provision
entrypoint: ["/bin/sh", "/provision/provision.sh"]
environment:
PROJECTS: *projects
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
ZITADEL_EXTERNAL_DOMAIN: ${ZITADEL_EXTERNAL_DOMAIN}
ZITADEL_PUBLIC_SCHEME: ${ZITADEL_PUBLIC_SCHEME}
MUSIFY_DB_PASSWORD: ${MUSIFY_DB_PASSWORD:-}
MUSIFY_RABBITMQ_PASSWORD: ${MUSIFY_RABBITMQ_PASSWORD:-}
MUSIFY_ZITADEL_PROJECT: Musify
PING_DB_PASSWORD: ${PING_DB_PASSWORD:-}
PING_RABBITMQ_PASSWORD: ${PING_RABBITMQ_PASSWORD:-}
PING_ZITADEL_PROJECT: Ping
DOCKERMANAGER_DB_PASSWORD: ${DOCKERMANAGER_DB_PASSWORD:-}
DOCKERMANAGER_RABBITMQ_PASSWORD: ${DOCKERMANAGER_RABBITMQ_PASSWORD:-}
DOCKERMANAGER_ZITADEL_PROJECT: ""
volumes:
- ./provision:/provision:ro
- ./zitadel/.output:/output
depends_on:
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
zitadel:
condition: service_healthy
networks: [infra-net]
logging: *default-logging
restart: "no"
volumes:
postgres_data:
zitadel_bootstrap: