diff --git a/docs/DEPLOYMENT_LOCAL.md b/docs/DEPLOYMENT_LOCAL.md index e50bdce..5b68182 100644 --- a/docs/DEPLOYMENT_LOCAL.md +++ b/docs/DEPLOYMENT_LOCAL.md @@ -52,6 +52,17 @@ task.local.nodedc -> Plane proxy/runtime Все внешние запросы идут через reverse proxy. +На этом этапе reverse proxy реализуется через Caddy, а Authentik запускается за ним без прямой публикации host ports. + +Текущие приложения подключаются как внешние upstream: + +```text +launcher.local.nodedc -> host.docker.internal:5173 +task.local.nodedc -> host.docker.internal:8090 +``` + +Это сохраняет Launcher и Plane в их текущих репозиториях и runtime-папках. + ## Environment Базовые переменные должны жить в: @@ -66,11 +77,39 @@ platform/infra/.env platform/infra/.env.example ``` +Для генерации локальных secrets: + +```bash +cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform +./infra/scripts/init-dev-env.sh +``` + +Скрипт создает `platform/infra/.env`, выставляет права `600` и не перезаписывает существующий файл. + +## Start commands + +```bash +cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform +docker compose --env-file infra/.env -f infra/docker-compose.dev.yml up -d +``` + +Проверка: + +```bash +docker compose --env-file infra/.env -f infra/docker-compose.dev.yml ps +curl -I -H 'Host: auth.local.nodedc' http://127.0.0.1/ +curl -I -H 'Host: task.local.nodedc' http://127.0.0.1/ +``` + +## Authentik version note + +Официальный compose Authentik для текущей ветки 2026.2 использует PostgreSQL, `server` и `worker`. Redis, указанный в раннем ТЗ как ожидаемый сервис, в актуальном официальном compose не используется. Если позже будет выбран старый pinned Authentik или отдельная HA-схема, Redis надо вернуть отдельной задачей. + ## Implementation order 1. Согласовать локальные domain/port bindings. 2. Добавить reverse proxy config. -3. Поднять Authentik server, worker, Postgres и Redis. +3. Поднять Authentik server, worker и Postgres. 4. Прокинуть Authentik за proxy с корректными headers. 5. Подключить текущий Launcher как внешний сервис или через compose service. 6. Подключить текущий Plane runtime как внешний service target. diff --git a/docs/SECURITY_CHECKLIST.md b/docs/SECURITY_CHECKLIST.md index 03c4290..5f6d0a9 100644 --- a/docs/SECURITY_CHECKLIST.md +++ b/docs/SECURITY_CHECKLIST.md @@ -2,13 +2,13 @@ ## Network -- [ ] Наружу опубликованы только reverse proxy ports. +- [x] Local dev compose публикует только reverse proxy port. - [ ] Postgres не опубликован наружу в staging/production. - [ ] Redis не опубликован наружу в staging/production. - [ ] MinIO/storage не опубликован наружу в staging/production. - [ ] Внутренние API не доступны напрямую извне. -- [ ] Authentik получает корректные `X-Forwarded-Proto`, `X-Forwarded-For`, `Host`. -- [ ] WebSocket headers настроены для Authentik/Plane/live. +- [x] Authentik получает `X-Forwarded-Proto`, `X-Forwarded-For`, `Host` через Caddy. +- [x] Caddy reverse proxy сохраняет HTTP/1.1/WebSocket upgrade behavior для upstream. ## Authentik @@ -17,6 +17,7 @@ - [ ] Для каждого приложения задана отдельная access policy. - [ ] Группы app access заведены отдельно от app-local ролей. - [ ] MFA/enrollment policy вынесены в отдельный этап. +- [x] Authentik local compose не публикует server/worker/Postgres ports напрямую. ## Launcher diff --git a/infra/.env.example b/infra/.env.example index cec8bc5..131a4b7 100644 --- a/infra/.env.example +++ b/infra/.env.example @@ -3,10 +3,28 @@ AUTH_DOMAIN=auth.local.nodedc LAUNCHER_DOMAIN=launcher.local.nodedc TASK_DOMAIN=task.local.nodedc +# proxy +PLATFORM_HTTP_PORT=80 +LOCAL_LAUNCHER_UPSTREAM=host.docker.internal:5173 +LOCAL_TASK_MANAGER_UPSTREAM=host.docker.internal:8090 + +# authentik image +AUTHENTIK_IMAGE=ghcr.io/goauthentik/server +AUTHENTIK_TAG=2026.2.2 + +# authentik database +PG_DB=authentik +PG_USER=authentik +PG_PASS=change-me-generate-with-infra-scripts-init-dev-env + # authentik -AUTHENTIK_SECRET_KEY= -AUTHENTIK_POSTGRES_PASSWORD= -AUTHENTIK_REDIS_HOST=redis-authentik +AUTHENTIK_SECRET_KEY=change-me-generate-with-infra-scripts-init-dev-env +AUTHENTIK_ERROR_REPORTING__ENABLED=false + +# optional first-start bootstrap +# AUTHENTIK_BOOTSTRAP_EMAIL=admin@nodedc.local +# AUTHENTIK_BOOTSTRAP_PASSWORD= +# AUTHENTIK_BOOTSTRAP_TOKEN= # launcher oidc LAUNCHER_OIDC_ISSUER=http://auth.local.nodedc/application/o/launcher/ @@ -21,10 +39,6 @@ PLANE_OIDC_CLIENT_SECRET= PLANE_OIDC_REDIRECT_URI=http://task.local.nodedc/auth/oidc/callback # security -SESSION_SECRET= +SESSION_SECRET=change-me-generate-with-infra-scripts-init-dev-env COOKIE_DOMAIN=.local.nodedc COOKIE_SECURE=false - -# current external local apps -LOCAL_LAUNCHER_URL=http://localhost:5173 -LOCAL_TASK_MANAGER_URL=http://localhost:8090 diff --git a/infra/README.md b/infra/README.md index 597038b..4566a75 100644 --- a/infra/README.md +++ b/infra/README.md @@ -8,19 +8,57 @@ - shared env examples; - будущие docker compose файлы. -На нулевом этапе здесь фиксируется только каркас. Рабочий `docker-compose.dev.yml` создается отдельным этапом после согласования ports/domains и стратегии подключения текущих Launcher/Plane runtime. +Первый local dev слой проксирует текущие локальные приложения без физического переноса репозиториев: + +- `auth.local.nodedc` -> `authentik-server:9000`; +- `launcher.local.nodedc` -> `host.docker.internal:5173`; +- `task.local.nodedc` -> `host.docker.internal:8090`. + +Authentik построен по актуальной официальной Docker Compose схеме 2026.2: PostgreSQL 16, server и worker. Redis для Authentik в этой версии официального compose не используется. ## Expected files ```text infra/ .env.example + scripts/init-dev-env.sh docker-compose.dev.yml docker-compose.staging.yml reverse-proxy/ authentik/ ``` +## Local start + +1. Add local domains to `/etc/hosts`: + +```text +127.0.0.1 auth.local.nodedc +127.0.0.1 launcher.local.nodedc +127.0.0.1 task.local.nodedc +``` + +2. Generate local secrets: + +```bash +./infra/scripts/init-dev-env.sh +``` + +3. Start infra: + +```bash +docker compose --env-file infra/.env -f infra/docker-compose.dev.yml up -d +``` + +4. Check services: + +```bash +docker compose --env-file infra/.env -f infra/docker-compose.dev.yml ps +curl -I -H 'Host: auth.local.nodedc' http://127.0.0.1/ +``` + +Generated Authentik bootstrap credentials are stored only in `infra/.env`. + ## Current decision Текущий Plane runtime не переносится в compose платформы до backup и отдельного шага миграции. diff --git a/infra/authentik/README.md b/infra/authentik/README.md new file mode 100644 index 0000000..afd622d --- /dev/null +++ b/infra/authentik/README.md @@ -0,0 +1,35 @@ +# Authentik Local Bootstrap + +This directory stores local Authentik bootstrap assets for NODE.DC. + +## Current scope + +The first infra pass runs Authentik from the official Docker Compose shape for the 2026.2 release line: + +- PostgreSQL 16; +- authentik server; +- authentik worker; +- no Redis service in the current official compose template; +- Caddy reverse proxy in front of Authentik and current local apps. + +## Bootstrap variables + +For a first local install, put these variables in `infra/.env`: + +```bash +AUTHENTIK_BOOTSTRAP_EMAIL=admin@nodedc.local +AUTHENTIK_BOOTSTRAP_PASSWORD= +AUTHENTIK_BOOTSTRAP_TOKEN= +``` + +These are read only on first startup. Do not commit `infra/.env`. + +## Future blueprint work + +Later phases should add reproducible configuration for: + +- NODE.DC Launcher Application/Provider; +- NODE.DC Task Manager Application/Provider; +- groups and policies; +- admin service token scope; +- exports or blueprints for repeatable setup. diff --git a/infra/authentik/custom-templates/.gitkeep b/infra/authentik/custom-templates/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/infra/authentik/custom-templates/.gitkeep @@ -0,0 +1 @@ + diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml new file mode 100644 index 0000000..dbf7ac7 --- /dev/null +++ b/infra/docker-compose.dev.yml @@ -0,0 +1,95 @@ +name: nodedc-platform + +services: + reverse-proxy: + image: docker.io/library/caddy:2-alpine + restart: unless-stopped + env_file: + - path: .env + required: false + ports: + - "${PLATFORM_HTTP_PORT:-80}:80" + volumes: + - ./reverse-proxy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + depends_on: + authentik-server: + condition: service_started + extra_hosts: + - "host.docker.internal:host-gateway" + + postgresql-authentik: + image: docker.io/library/postgres:16-alpine + restart: unless-stopped + env_file: + - path: .env + required: false + environment: + POSTGRES_DB: ${PG_DB:-authentik} + POSTGRES_PASSWORD: ${PG_PASS:?database password required} + POSTGRES_USER: ${PG_USER:-authentik} + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + volumes: + - authentik-database:/var/lib/postgresql/data + + authentik-server: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2} + command: server + restart: unless-stopped + env_file: + - path: .env + required: false + environment: + AUTHENTIK_POSTGRESQL__HOST: postgresql-authentik + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS:?database password required} + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required} + AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS: ${AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS:-127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1/128} + depends_on: + postgresql-authentik: + condition: service_healthy + expose: + - "9000" + - "9443" + shm_size: 512mb + volumes: + - authentik-data:/data + - ./authentik/custom-templates:/templates + + authentik-worker: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2} + command: worker + restart: unless-stopped + user: root + env_file: + - path: .env + required: false + environment: + AUTHENTIK_POSTGRESQL__HOST: postgresql-authentik + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS:?database password required} + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required} + depends_on: + postgresql-authentik: + condition: service_healthy + shm_size: 512mb + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - authentik-data:/data + - authentik-certs:/certs + - ./authentik/custom-templates:/templates + +volumes: + authentik-database: + authentik-data: + authentik-certs: + caddy-data: + caddy-config: diff --git a/infra/reverse-proxy/Caddyfile b/infra/reverse-proxy/Caddyfile new file mode 100644 index 0000000..aa897c3 --- /dev/null +++ b/infra/reverse-proxy/Caddyfile @@ -0,0 +1,27 @@ +{ + auto_https off +} + +http://{$AUTH_DOMAIN:auth.local.nodedc} { + reverse_proxy authentik-server:9000 { + header_up Host {host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote_host} + } +} + +http://{$LAUNCHER_DOMAIN:launcher.local.nodedc} { + reverse_proxy {$LOCAL_LAUNCHER_UPSTREAM:host.docker.internal:5173} { + header_up Host {host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote_host} + } +} + +http://{$TASK_DOMAIN:task.local.nodedc} { + reverse_proxy {$LOCAL_TASK_MANAGER_UPSTREAM:host.docker.internal:8090} { + header_up Host {host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote_host} + } +} diff --git a/infra/scripts/init-dev-env.sh b/infra/scripts/init-dev-env.sh new file mode 100755 index 0000000..acb8865 --- /dev/null +++ b/infra/scripts/init-dev-env.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +INFRA_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +ENV_FILE="$INFRA_DIR/.env" + +if [ -f "$ENV_FILE" ]; then + echo "Refusing to overwrite existing $ENV_FILE" >&2 + exit 1 +fi + +rand() { + openssl rand -base64 "$1" | tr -d '\n' +} + +cat > "$ENV_FILE" <