Все OIDC-операции выполняет authentik — контроллер узнаёт о них только через
native-события, доставляемые webhook'ом на /coop/internal/authentik-events
(механизм Story 1.5, shared-токен constant-time). Расширяем его на OIDC.
- infra/coopid/authentik/blueprints/coopid-oidc-audit.yaml — event-matcher +
notification-rule + policy-binding на каждое действие (login/logout/
authorize_application/login_failed/suspicious_request) + webhook body-mapping
(action/user/client_ip/app/created) + transport на тот же endpoint. Поля
сверены со /blueprints/schema.json образа 2026.2.
- authentik-events.controller.ts — mapAuthentikEvent расширен: login→OidcLoginSuccess,
logout→OidcLogout, authorize_application→OidcTokenIssued (семантические Oidc*);
прочие native-события → Authentik<Action> (login_failed→AuthentikLoginFailed,
suspicious_request→AuthentikSuspiciousRequest, failure-result). ip=client_ip,
context={authentik_action,app,authentik_created}. policy_execution (Story 1.5)
без изменений. Контекст проходит secret-blacklist аудита.
- authentik-events.controller.spec.ts — 10 кейсов (weak-password, Oidc*, Authentik*,
null, secret-blacklist).
Доставка идёт по членам destination_group: пустая группа НЕ доставляет (транспорт
не вызывается), поэтому выделенная группа coopid-audit с неактивным сервис-членом
coopid-audit-sink — надёжная доставка webhook'ом без спама реальных админов.
Live-валидация (стек поднят, потом потушен): blueprint применился (successful,
объекты+членство в БД); прямой webhook login→OidcLoginSuccess и login_failed→
AuthentikLoginFailed записались в audit_events; РЕАЛЬНЫЙ login_failed в authentik
прошёл всю цепочку (event→rule→webhook по docker-сети→контроллер→audit_events:
AuthentikLoginFailed, ip, user=akadmin). Unit 10/10, ESLint 0.
Отклонение (честно): OidcTokenRevoked/refresh не подключены — в authentik 2026.2 нет
надёжного native-action для отзыва/refresh OAuth-токена (enum eventmatcher не
содержит); endpoint'ы revoke/introspect работают, их аудит — отдельный механизм
при подключении потребителей. Отложено.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CoopID — инфраструктура per-coop (Story 1.1)
Стек OIDC-аутентификации кооператива встроен в основной docker-compose.yaml
монорепы: authentik 2026.2 (server + worker), caddy (внешний TLS 1.3) и crowdsec.
Отдельный postgres не поднимается — данные authentik и доменные данные CoopID
живут двумя отдельными БД в существующем сервисе postgres:
authentik_db— рольauthentik_user(данные authentik);coop_domain_db— рольcoop_app_user(vaults, audit, кэши — миграции V2.4.x).
Ролевая изоляция: подключиться к чужой БД может только её владелец (и суперюзер).
Redis — общий monoredis (authentik в DB-индексе 1, controller в 0).
Весь стек поднимается одной командой — никаких overlay -f-цепочек.
⚠️ ОБЯЗАТЕЛЬНО перед первым
docker compose up -d(и на каждом чекауте/ноде). CoopID-сервисы вdocker-compose.yamlссылаются на file-секреты изinfra/coopid/secrets/*(каталог в.gitignore— в репозиторий не попадает). Если этих файлов нет,docker compose up -dпадает на этапе валидации конфига (secret ... not found) и не поднимает вообще ничего, включая базовый стек. Лекарство — один раз прогнать:bash scripts/coopid-gen-secrets.shЭто касается любого чекаута монорепы (mono-ai-1..5) после
git pullветки с этими изменениями, не только mono-ai-3. На prod секреты раскладывает плейбук~/playbooks— там скрипт-генератор не используется.
Запуск
# 1. Секреты (один раз; идемпотентно — существующие не перезаписывает)
bash scripts/coopid-gen-secrets.sh
# 2. Весь стек
docker compose up -d
Состав (в docker-compose.yaml)
| Сервис | Образ | Назначение |
|---|---|---|
postgres |
postgres:18 | Общий postgres. Кроме voskhod держит authentik_db и coop_domain_db (создаются init-скриптом на свежем томе либо вручную — см. ниже) |
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) |
Две БД в существующем postgres
infra/coopid/postgres/init/01-init.sh смонтирован в /docker-entrypoint-initdb.d
и заводит роли+БД только при пустом data dir (правило официального образа
postgres). Поэтому:
-
Свежая машина / пустой том —
docker compose up -dсоздаёт обе БД сам. -
Уже инициализированный том (как на текущих dev-стендах) — init-скрипт не запускается, БД заводятся вручную тем же SQL:
AUTHPW=$(cat infra/coopid/secrets/coop_pg_authentik_password) APPPW=$(cat infra/coopid/secrets/coop_pg_app_password) docker compose exec -T postgres psql -v ON_ERROR_STOP=1 \ -v apw="$AUTHPW" -v cpw="$APPPW" -U postgres -d postgres <<'SQL' 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; SQLЗатем coopback на старте применит миграции
coop_domain_db(V2.4.0/V2.4.1).
Секреты
File-based Docker Secrets из infra/coopid/secrets/ (каталог в .gitignore):
coop_cert_key (secp256k1 PEM — подпись participant_certificate),
coop_vault_key, authentik_secret_key, bootstrap-пароль/токен akadmin,
authentik_webhook_token, auth_v2_session_binding_secret, два пароля ролей
postgres. Суперюзер postgres — inline-пароль в compose, отдельного секрета нет.
В .env/compose значений секретов нет; контейнеры читают их из /run/secrets/*
(postgres-роли — через init-скрипт, authentik — file://, coopback — *_FILE).
На prod секреты раскладывает плейбук с правами 0400.
Порты (dev, mono-ai-3)
127.0.0.1:8443— caddy HTTPS (self-signed приCOOPID_DOMAIN=localhost)127.0.0.1:8088— caddy HTTP127.0.0.1:9008— authentik напрямую (мимо caddy, для отладки;AUTHENTIK_HOST_PORT)127.0.0.1:5532— postgres (обе БД,PG_HOST_PORT)
Проверка
docker compose ps
# SNI обязателен: caddy с авто-TLS для localhost без SNI отдаёт TLS internal error.
curl -sk --tlsv1.3 --resolve localhost:8443:127.0.0.1 https://localhost:8443/ \
-o /dev/null -w '%{http_code}\n' # 302/200 от authentik (порт = CADDY_HTTPS_PORT)
# Ролевая изоляция (ожидаемо permission denied):
docker compose exec postgres psql -U coop_app_user -d authentik_db -c 'select 1'
Учётка администратора authentik: akadmin, пароль — в
infra/coopid/secrets/authentik_bootstrap_password.
Бэкапы (Story 9.8)
Ежедневный pg_basebackup всего PG-кластера (обе БД сразу) → один gzip-tar → S3
s3://{bucket}/{coopname}/{YYYY-MM-DD}.tar.gz с SSE-S3. Retention 30 дней — через
S3 lifecycle rule (не ручным удалением). На каждый прогон — строка audit_events
(coopid.backup.created: s3_key, size_bytes, duration_seconds).
Скрипт: infra/coopid/scripts/coopid-backup.sh. Env:
export COOPNAME=voskhod # имя кооператива (обязательно)
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
export S3_BUCKET=coopid-backups # дефолт
export S3_ENDPOINT= # пусто = реальный AWS S3; для MinIO: http://minio:9000
export RETENTION_DAYS=30 # дефолт
export PG_CONTAINER=coop-postgres # имя контейнера postgres
Один раз при деплое — создать bucket и поставить lifecycle:
COOPNAME=voskhod ./infra/coopid/scripts/coopid-backup.sh --setup
Cron (ставит прод-плейбук ~/playbooks; локально — в crontab хоста), 03:00 UTC daily:
0 3 * * * cd /path/to/mono && COOPNAME=voskhod AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... \
./infra/coopid/scripts/coopid-backup.sh >> /var/log/coopid-backup.log 2>&1
--dry-run печатает команды без выполнения. Восстановление — runbook
docs/operations/disaster-recovery.md (Story 9.9). Требования к PG: wal_level>=replica
(дефолт) и replication-доступ суперпользователя по локальному сокету контейнера.
Troubleshooting
- Init-скрипт postgres выполняется только при пустом data dir. На живом томе
заводите БД вручную (см. блок выше). Ротация паролей ролей —
ALTER ROLE, не перегенерацией секрета. - Прод с реальным доменом: задайте
COOPID_DOMAIN,CADDY_BIND=0.0.0.0,CADDY_HTTP_PORT=80,CADDY_HTTPS_PORT=443— ACME HTTP-01 должен быть достижим снаружи на 80/443. - authentik на postgres:18: официальная поддержка на pg16, но pg18 как сервер обратносовместим (Django+psycopg). На dev/testnet общий postgres используется сознательно — единый стек одной командой важнее версии-в-версию.