[105-4][@ant] feat: Docker Compose overlay-стек CoopID (authentik 2026.2 + postgres:16 + caddy + crowdsec) — инфраструктурный seed Эпика 1: per-coop OIDC Provider с двумя изолированными БД и секретами через Docker Secrets вместо открытых .env
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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.7–1.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:
|
||||
@@ -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:
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Executable
+34
@@ -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
|
||||
Executable
+46
@@ -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"
|
||||
Reference in New Issue
Block a user