[105-4][@ant] feat: Docker Compose overlay-стек CoopID (authentik 2026.2 + postgres:16 + caddy + crowdsec) — инфраструктурный seed Эпика 1: per-coop OIDC Provider с двумя изолированными БД и секретами через Docker Secrets вместо открытых .env
Typecheck / desktop (pull_request) Failing after 8m58s
Typecheck / controller (pull_request) Has been cancelled

This commit is contained in:
coopops
2026-06-10 06:02:23 +00:00
parent 10cc7bdb9f
commit 86dede3d35
9 changed files with 360 additions and 0 deletions
+11
View File
@@ -39,3 +39,14 @@ MINIO_ROOT_PASSWORD=minioadmin
CHAIN_URL=http://127.0.0.1:8888
API_URL=http://127.0.0.1:2998/v1/graphql
MONGODB_URL=mongodb://127.0.0.1:27017
# --- CoopID (overlays docker-compose.authentik.yml + docker-compose.edge.yml) ---
# Поднимаются только явной цепочкой -f (см. infra/coopid/README.md);
# перед первым запуском: bash scripts/coopid-gen-secrets.sh
# К портам применяется то же правило offset = (INSTANCE_INDEX - 1) * 10.
COOP_PG_HOST_PORT=5632
AUTHENTIK_HOST_PORT=9008
CADDY_HTTP_PORT=8088
CADDY_HTTPS_PORT=8443
# Домен кооператива для caddy: localhost = dev self-signed; реальный домен = Let's Encrypt.
COOPID_DOMAIN=localhost
+3
View File
@@ -25,3 +25,6 @@ components/contracts/cpp/ledger2/scripts/out/
.pnpm-store
schema.gql
.claude
# CoopID: file-based Docker Secrets (генерируются scripts/coopid-gen-secrets.sh)
infra/coopid/secrets/
+106
View File
@@ -0,0 +1,106 @@
# Overlay CoopID: authentik OIDC Provider (per-coop) + выделенный postgres:16.
#
# Запуск вместе с базовым стеком:
# bash scripts/coopid-gen-secrets.sh # один раз, генерирует infra/coopid/secrets/
# docker compose -f docker-compose.yaml -f docker-compose.authentik.yml -f docker-compose.edge.yml up -d
#
# Существующий сервис postgres (18, БД voskhod) намеренно не используется:
# смена его образа на 16 сделала бы data dir нечитаемым. Две новые БД
# (authentik_db + coop_domain_db) живут в отдельном coop-postgres c ролевой
# изоляцией. Redis не дублируем — authentik работает в monoredis, DB-индекс 1.
# Подробности: infra/coopid/README.md
services:
coop-postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/coop_pg_superuser_password
secrets:
- coop_pg_superuser_password
- coop_pg_authentik_password
- coop_pg_app_password
volumes:
- coop_postgres_data:/var/lib/postgresql/data
- ./infra/coopid/postgres/init:/docker-entrypoint-initdb.d:ro
ports:
- "127.0.0.1:${COOP_PG_HOST_PORT:-5632}:5432"
healthcheck:
# Не pg_isready: он не проверяет существование БД и маскирует упавший init-скрипт.
test: ["CMD-SHELL", "psql -U postgres -d authentik_db -c 'select 1' >/dev/null 2>&1 && psql -U postgres -d coop_domain_db -c 'select 1' >/dev/null 2>&1"]
interval: 10s
timeout: 5s
retries: 12
authentik-server:
image: ghcr.io/goauthentik/server:2026.2
restart: unless-stopped
command: server
environment: &authentik-env
AUTHENTIK_SECRET_KEY: file:///run/secrets/authentik_secret_key
AUTHENTIK_POSTGRESQL__HOST: coop-postgres
AUTHENTIK_POSTGRESQL__PORT: 5432
AUTHENTIK_POSTGRESQL__NAME: authentik_db
AUTHENTIK_POSTGRESQL__USER: authentik_user
AUTHENTIK_POSTGRESQL__PASSWORD: file:///run/secrets/coop_pg_authentik_password
AUTHENTIK_REDIS__HOST: monoredis
# controller использует DB 0; при шаринге одного redis несколькими коопами — развести индексы.
AUTHENTIK_REDIS__DB: ${AUTHENTIK_REDIS_DB:-1}
AUTHENTIK_BOOTSTRAP_PASSWORD: file:///run/secrets/authentik_bootstrap_password
AUTHENTIK_BOOTSTRAP_TOKEN: file:///run/secrets/authentik_bootstrap_token
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
AUTHENTIK_DISABLE_UPDATE_CHECK: "true"
AUTHENTIK_DISABLE_STARTUP_ANALYTICS: "true"
secrets: &authentik-secrets
- authentik_secret_key
- authentik_bootstrap_password
- authentik_bootstrap_token
- coop_pg_authentik_password
ports:
- "127.0.0.1:${AUTHENTIK_HOST_PORT:-9008}:9000"
depends_on:
coop-postgres:
condition: service_healthy
monoredis:
condition: service_started
authentik-worker:
image: ghcr.io/goauthentik/server:2026.2
restart: unless-stopped
command: worker
environment: *authentik-env
secrets: *authentik-secrets
depends_on:
coop-postgres:
condition: service_healthy
monoredis:
condition: service_started
# Аддитивное расширение сервиса из базового compose: ключ подписи
# participant_certificate и vault-ключ кооператива монтируются в controller
# (читаются CryptoService начиная со Stories 1.71.8).
coopback:
secrets:
- coop_cert_key
- coop_vault_key
secrets:
coop_pg_superuser_password:
file: ./infra/coopid/secrets/coop_pg_superuser_password
coop_pg_authentik_password:
file: ./infra/coopid/secrets/coop_pg_authentik_password
coop_pg_app_password:
file: ./infra/coopid/secrets/coop_pg_app_password
authentik_secret_key:
file: ./infra/coopid/secrets/authentik_secret_key
authentik_bootstrap_password:
file: ./infra/coopid/secrets/authentik_bootstrap_password
authentik_bootstrap_token:
file: ./infra/coopid/secrets/authentik_bootstrap_token
coop_cert_key:
file: ./infra/coopid/secrets/coop_cert_key
coop_vault_key:
file: ./infra/coopid/secrets/coop_vault_key
volumes:
coop_postgres_data:
+46
View File
@@ -0,0 +1,46 @@
# Overlay CoopID edge: caddy (внешний TLS 1.3) + crowdsec.
# Применяется ТОЛЬКО вместе с docker-compose.authentik.yml (caddy проксирует
# на authentik-server). Внутри docker-сети трафик plain HTTP, внешний TLS
# терминирует caddy. В Story 1.1 crowdsec только разбирает access-логи caddy;
# активный bouncer (блокировки) — Story 9.2.
services:
caddy:
image: caddy:2.11
restart: unless-stopped
environment:
# localhost → self-signed (внутренний CA caddy); реальный домен → Let's Encrypt.
COOPID_DOMAIN: ${COOPID_DOMAIN:-localhost}
ports:
# Прод с реальным доменом: CADDY_BIND=0.0.0.0, порты 80/443 — иначе ACME
# HTTP-01 challenge недостижим снаружи и Let's Encrypt не выдаст сертификат.
- "${CADDY_BIND:-127.0.0.1}:${CADDY_HTTP_PORT:-8088}:80"
- "${CADDY_BIND:-127.0.0.1}:${CADDY_HTTPS_PORT:-8443}:443"
volumes:
- ./infra/coopid/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
- caddy_logs:/var/log/caddy
depends_on:
- authentik-server
- coopback
crowdsec:
image: crowdsecurity/crowdsec:v1.7.8
restart: unless-stopped
environment:
COLLECTIONS: "crowdsecurity/caddy crowdsecurity/http-cve"
volumes:
- ./infra/coopid/crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- crowdsec_db:/var/lib/crowdsec/data
- crowdsec_config:/etc/crowdsec
- caddy_logs:/var/log/caddy:ro
depends_on:
- caddy
volumes:
caddy_data:
caddy_config:
caddy_logs:
crowdsec_db:
crowdsec_config:
+76
View File
@@ -0,0 +1,76 @@
# CoopID — инфраструктура per-coop (Story 1.1)
Стек OIDC-аутентификации кооператива: authentik 2026.2 (server + worker),
выделенный postgres:16 с двумя БД, caddy (внешний TLS 1.3) и crowdsec.
Подключается к базовому дев-стеку двумя аддитивными overlay-файлами —
существующие сервисы (`postgres` 18/voskhod, `monoredis`, `coopback`, `desktop`)
не изменяются и не пересоздаются.
## Запуск
```bash
# 1. Секреты (один раз; идемпотентно)
bash scripts/coopid-gen-secrets.sh
# 2. Весь стек с overlays
docker compose -f docker-compose.yaml -f docker-compose.authentik.yml -f docker-compose.edge.yml up -d
```
Чтобы не повторять `-f`-цепочку, можно добавить в `.env`:
```
COMPOSE_FILE=docker-compose.yaml:docker-compose.authentik.yml:docker-compose.edge.yml
```
## Состав
| Сервис | Образ | Назначение |
|---|---|---|
| `coop-postgres` | postgres:16 | Две БД: `authentik_db` (роль `authentik_user`) и `coop_domain_db` (роль `coop_app_user`); ролевая изоляция — подключение только владельцу |
| `authentik-server` | ghcr.io/goauthentik/server:2026.2 | OIDC Provider; UI/API на :9000 внутри сети |
| `authentik-worker` | ghcr.io/goauthentik/server:2026.2 | Фоновые задачи authentik |
| `caddy` | caddy:2.11 | Внешний HTTPS (TLS 1.3 min): `/api/coopback/*` → controller, остальное → authentik; JSON access-log |
| `crowdsec` | crowdsecurity/crowdsec:v1.7.8 | Разбор access-логов caddy (активный bouncer — Story 9.2) |
Redis отдельно не поднимается: authentik использует существующий `monoredis`
(DB-индекс 1; controller сидит в индексе 0 по умолчанию).
## Секреты
Все секреты — file-based Docker Secrets из `infra/coopid/secrets/` (каталог в
`.gitignore`): `coop_cert_key` (secp256k1 PEM — подпись `participant_certificate`),
`coop_vault_key`, `authentik_secret_key`, bootstrap-пароль/токен `akadmin`,
три пароля postgres. В `.env` и compose значений секретов нет; контейнеры
читают их из `/run/secrets/*` (postgres — `*_FILE`, authentik — `file://`-схема).
На prod секреты раскладывает плейбук с правами `0400`.
## Порты (dev, INSTANCE_INDEX=1)
- `127.0.0.1:8443` — caddy HTTPS (self-signed при `COOPID_DOMAIN=localhost`)
- `127.0.0.1:8088` — caddy HTTP
- `127.0.0.1:9008` — authentik напрямую (мимо caddy, для отладки)
- `127.0.0.1:5632` — coop-postgres
## Проверка
```bash
docker compose -f docker-compose.yaml -f docker-compose.authentik.yml -f docker-compose.edge.yml ps
curl -sk --tlsv1.3 https://127.0.0.1:8443/ -o /dev/null -w '%{http_code}\n' # 2xx/3xx от authentik
# Ролевая изоляция (ожидаемо: permission denied):
docker compose exec coop-postgres psql -U coop_app_user -d authentik_db -c 'select 1'
```
Учётка администратора authentik: `akadmin`, пароль — в
`infra/coopid/secrets/authentik_bootstrap_password`.
## Troubleshooting
- **Init-скрипты postgres выполняются только при пустом data dir.** Если первый
старт coop-postgres упал на середине init (healthcheck вечно красный, БД/ролей
нет) или вы перегенерировали pg-пароли при живом volume — пересоздайте том:
`docker compose ... stop coop-postgres && docker compose ... rm -f coop-postgres
&& docker volume rm <project>_coop_postgres_data`, затем `up -d` заново.
Ротация паролей живой БД делается `ALTER ROLE`, не перегенерацией секрета.
- **Прод с реальным доменом:** задайте `COOPID_DOMAIN`, `CADDY_BIND=0.0.0.0`,
`CADDY_HTTP_PORT=80`, `CADDY_HTTPS_PORT=443` — ACME HTTP-01 должен быть
достижим снаружи на 80/443.
+32
View File
@@ -0,0 +1,32 @@
# Edge-вход кооператива (CoopID). TLS 1.3 минимум; внутрь сети — plain HTTP.
# COOPID_DOMAIN=localhost (dev) даёт self-signed сертификат внутреннего CA;
# реальный домен автоматически получает Let's Encrypt.
{
admin off
}
{$COOPID_DOMAIN:localhost} {
tls {
protocols tls1.3
}
log {
output file /var/log/caddy/access.log {
roll_size 50MiB
roll_keep 5
}
format json
}
# REST/GraphQL controller'а — до общего catch-all.
handle /api/coopback/* {
uri strip_prefix /api/coopback
reverse_proxy coopback:2998
}
# Всё остальное — OIDC Provider (authentik UI, /application/o/*, flows).
handle {
reverse_proxy authentik-server:9000
}
}
+6
View File
@@ -0,0 +1,6 @@
# Источники логов для crowdsec: JSON access-лог caddy (shared volume caddy_logs).
# Активные блокировки (bouncer в caddy) подключаются в Story 9.2.
filenames:
- /var/log/caddy/access.log
labels:
type: caddy
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Инициализация coop-postgres (выполняется entrypoint'ом postgres только при пустом data dir):
# две БД с ролевой изоляцией — authentik_db (authentik_user) и coop_domain_db (coop_app_user).
# Пароли ролей берутся из Docker Secrets, в env/compose они не попадают.
set -euo pipefail
AUTHENTIK_PW="$(cat /run/secrets/coop_pg_authentik_password)"
APP_PW="$(cat /run/secrets/coop_pg_app_password)"
# Пароли — через psql-переменные с квотированием :'var': не попадают в текст
# SQL-команды (логи statement'ов, pg_stat_activity) и безопасны к спецсимволам.
psql -v ON_ERROR_STOP=1 -v apw="$AUTHENTIK_PW" -v cpw="$APP_PW" \
--username "$POSTGRES_USER" --dbname postgres <<-'EOSQL'
CREATE ROLE authentik_user LOGIN PASSWORD :'apw';
CREATE ROLE coop_app_user LOGIN PASSWORD :'cpw';
CREATE DATABASE authentik_db OWNER authentik_user;
CREATE DATABASE coop_domain_db OWNER coop_app_user;
-- Ролевая изоляция: подключаться к БД может только её владелец (и суперпользователь).
REVOKE CONNECT ON DATABASE authentik_db FROM PUBLIC;
REVOKE CONNECT ON DATABASE coop_domain_db FROM PUBLIC;
EOSQL
# Запрет создания объектов в public-схеме чужой БД на случай выдачи CONNECT в будущем.
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname authentik_db <<-EOSQL
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT ALL ON SCHEMA public TO authentik_user;
EOSQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname coop_domain_db <<-EOSQL
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT ALL ON SCHEMA public TO coop_app_user;
EOSQL
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Генерация file-based Docker Secrets для CoopID-стека (dev).
# Идемпотентен: существующие непустые файлы не перезаписывает.
# На prod секреты раскладывает плейбук — этот скрипт только для локальной разработки.
set -euo pipefail
command -v openssl >/dev/null || { echo "Нужен openssl (генерация ключей/паролей)"; exit 1; }
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SECRETS_DIR="$ROOT/infra/coopid/secrets"
mkdir -p "$SECRETS_DIR"
gen() {
local mode="$1" name="$2"
shift 2
local file="$SECRETS_DIR/$name"
if [[ -s "$file" ]]; then
echo " = $name — уже существует, пропущен"
return
fi
"$@" > "$file"
chmod "$mode" "$file"
echo " + $name — создан"
}
rand_hex() { openssl rand -hex 32; }
rand_pass() { openssl rand -base64 36 | tr -d '\n=/+'; }
secp256k1_pem() { openssl ecparam -genkey -name secp256k1 -noout; }
echo "Секреты CoopID → $SECRETS_DIR"
# Ключ подписи participant_certificate (ES256K, secp256k1).
gen 0400 coop_cert_key secp256k1_pem
# AES-ключ vault'а кооператива (subject_type='coop').
gen 0400 coop_vault_key rand_hex
# authentik: secret key + bootstrap-учётка akadmin (образ работает под uid 1000 = владелец файлов).
gen 0400 authentik_secret_key rand_pass
gen 0400 authentik_bootstrap_password rand_pass
gen 0400 authentik_bootstrap_token rand_pass
# postgres: file-секреты в plain compose — bind-mount с хостовыми правами,
# а init-скрипты официального образа выполняются под uid 999 (postgres) —
# поэтому пароли postgres world-readable. Только для dev; prod-права — плейбук.
gen 0444 coop_pg_superuser_password rand_pass
gen 0444 coop_pg_authentik_password rand_pass
gen 0444 coop_pg_app_password rand_pass
echo "Готово. Дальше: docker compose -f docker-compose.yaml -f docker-compose.authentik.yml -f docker-compose.edge.yml up -d"