diff --git a/infra/authentik/bootstrap-dev.py b/infra/authentik/bootstrap-dev.py index 2c76a6a..746bb10 100644 --- a/infra/authentik/bootstrap-dev.py +++ b/infra/authentik/bootstrap-dev.py @@ -41,7 +41,9 @@ APP_SPECS = [ "redirect_uri_env": "LAUNCHER_OIDC_REDIRECT_URI", "logged_out_redirect_uri_env": "LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI", "default_logged_out_redirect_uri": "http://launcher.local.nodedc/auth/logged-out", + "launch_url_env": "LAUNCHER_BASE_URL", "launch_url": "http://launcher.local.nodedc", + "logout_uri_env": "LAUNCHER_LOGOUT_URI", "logout_uri": "http://launcher.local.nodedc/logout", "groups": ["nodedc:superadmin", "nodedc:launcher:admin", "nodedc:launcher:user"], "description": "NODE.DC control plane launcher.", @@ -53,7 +55,9 @@ APP_SPECS = [ "client_id_env": "PLANE_OIDC_CLIENT_ID", "client_secret_env": "PLANE_OIDC_CLIENT_SECRET", "redirect_uri_env": "PLANE_OIDC_REDIRECT_URI", + "launch_url_env": "TASK_BASE_URL", "launch_url": "http://task.local.nodedc", + "logout_uri_env": "TASK_LOGOUT_URI", "logout_uri": "http://task.local.nodedc/logout", "groups": ["nodedc:superadmin", "nodedc:taskmanager:admin", "nodedc:taskmanager:user"], "description": "NODE.DC Plane-based task manager.", @@ -260,7 +264,7 @@ def ensure_provider(spec, mappings): RedirectURI(RedirectURIMatchingMode.STRICT, redirect_uri) for redirect_uri in dict.fromkeys(redirect_uri_values) ] - provider.logout_uri = spec["logout_uri"] + provider.logout_uri = optional_env(spec.get("logout_uri_env", ""), spec["logout_uri"]) provider.logout_method = OAuth2LogoutMethod.FRONTCHANNEL provider.include_claims_in_id_token = True provider.sub_mode = SubModes.USER_UUID @@ -282,7 +286,7 @@ def ensure_application(spec, provider, groups): application.slug = spec["slug"] application.group = "NODE.DC" application.provider = provider - application.meta_launch_url = spec["launch_url"] + application.meta_launch_url = optional_env(spec.get("launch_url_env", ""), spec["launch_url"]) application.meta_description = spec["description"] application.meta_publisher = "NODE.DC" application.open_in_new_tab = False diff --git a/infra/authentik/custom-templates/base/header_js.html b/infra/authentik/custom-templates/base/header_js.html index 2ecb253..3c738ce 100644 --- a/infra/authentik/custom-templates/base/header_js.html +++ b/infra/authentik/custom-templates/base/header_js.html @@ -266,7 +266,19 @@ function getLauncherBaseUrl() { const hostname = window.location.hostname; - const launcherHostname = hostname.startsWith("auth.") ? `launcher.${hostname.slice(5)}` : "launcher.local.nodedc"; + const launcherHostnames = { + "id.nodedc.ru": "hub.nodedc.ru", + "auth.nodedc.ru": "hub.nodedc.ru", + "id.notdc.ru": "launcher.notdc.ru", + "auth.notdc.ru": "launcher.notdc.ru", + }; + const launcherHostname = + launcherHostnames[hostname] || + (hostname.startsWith("auth.") + ? `launcher.${hostname.slice(5)}` + : hostname.startsWith("id.") + ? `hub.${hostname.slice(3)}` + : "hub.nodedc.ru"); const port = window.location.port ? `:${window.location.port}` : ""; return `${window.location.protocol}//${launcherHostname}${port}/`; @@ -691,8 +703,8 @@ try { const url = new URL(rawUrl); const allowedHosts = new Set([ - "launcher.local.nodedc", - "launcher.local.notdc", + "hub.nodedc.ru", + "hub.notdc.ru", "launcher.notdc.ru", "platform.notdc.ru", "notdc.ru", diff --git a/infra/synology/.env.synology.example b/infra/synology/.env.synology.example new file mode 100644 index 0000000..ab7087c --- /dev/null +++ b/infra/synology/.env.synology.example @@ -0,0 +1,46 @@ +# Parallel Synology HTTP deployment. +# This intentionally uses high ports and does not touch existing nodedc-demo. + +AUTH_DOMAIN=auth.nas.nodedc +LAUNCHER_DOMAIN=launcher.nas.nodedc +TASK_DOMAIN=task.nas.nodedc +NODEDC_PUBLIC_HTTP_PORT=18080 +PLATFORM_HTTP_PORT=18080 +SYNOLOGY_TASK_MANAGER_UPSTREAM=host.docker.internal:18090 + +AUTHENTIK_IMAGE=ghcr.io/goauthentik/server +AUTHENTIK_TAG=2026.2.2 +AUTHENTIK_ERROR_REPORTING__ENABLED=false +AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS=127.0.0.0/8,172.16.0.0/12 + +PG_DB=authentik +PG_USER=authentik +PG_PASS=replace-with-random-synology-secret +AUTHENTIK_SECRET_KEY=replace-with-random-synology-secret +AUTHENTIK_BOOTSTRAP_EMAIL=admin@nodedc.local +AUTHENTIK_BOOTSTRAP_PASSWORD=replace-with-random-synology-secret +AUTHENTIK_BOOTSTRAP_TOKEN=replace-with-random-synology-secret + +LAUNCHER_BASE_URL=https://hub.nodedc.ru +TASK_BASE_URL=https://ops.nodedc.ru +TASK_LOGOUT_URI=https://ops.nodedc.ru/logout +TASK_INTERNAL_LOGOUT_URL=https://ops.nodedc.ru/api/internal/nodedc/logout/ + +LAUNCHER_OIDC_ISSUER=https://id.nodedc.ru/application/o/launcher/ +LAUNCHER_OIDC_CLIENT_ID=nodedc-launcher +LAUNCHER_OIDC_CLIENT_SECRET=replace-with-random-synology-secret +LAUNCHER_OIDC_REDIRECT_URI=https://hub.nodedc.ru/auth/callback +LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI=https://hub.nodedc.ru/auth/logged-out +LAUNCHER_LOGOUT_URI=https://hub.nodedc.ru/logout +LAUNCHER_COOKIE_DOMAIN=.nodedc.ru + +PLANE_OIDC_ISSUER=https://id.nodedc.ru/application/o/task-manager/ +PLANE_OIDC_CLIENT_ID=nodedc-task-manager +PLANE_OIDC_CLIENT_SECRET=replace-with-random-synology-secret +PLANE_OIDC_REDIRECT_URI=https://ops.nodedc.ru/auth/oidc/callback + +NODEDC_AUTHENTIK_BASE_URL=http://authentik-server:9000 +NODEDC_INTERNAL_ACCESS_TOKEN=replace-with-random-synology-secret +SESSION_SECRET=replace-with-random-synology-secret +COOKIE_DOMAIN=.nas.nodedc +COOKIE_SECURE=false diff --git a/infra/synology/Caddyfile.http b/infra/synology/Caddyfile.http new file mode 100644 index 0000000..79e45bf --- /dev/null +++ b/infra/synology/Caddyfile.http @@ -0,0 +1,71 @@ +{ + auto_https off +} + +http://{$AUTH_DOMAIN} { + @auth_root path / + redir @auth_root http://{$LAUNCHER_DOMAIN}:{$NODEDC_PUBLIC_HTTP_PORT}/ 302 + + @auth_user_dashboard path /if/user /if/user/* + redir @auth_user_dashboard http://{$LAUNCHER_DOMAIN}:{$NODEDC_PUBLIC_HTTP_PORT}/ 302 + + reverse_proxy authentik-server:9000 { + header_up Host {http.request.host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote_host} + } +} + +http://{$LAUNCHER_DOMAIN} { + reverse_proxy launcher:5173 { + header_up Host {http.request.host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote_host} + } +} + +http://{$TASK_DOMAIN} { + reverse_proxy {$SYNOLOGY_TASK_MANAGER_UPSTREAM} { + header_up Host {http.request.host} + header_up X-Forwarded-Host {http.request.host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Port {$NODEDC_PUBLIC_HTTP_PORT} + header_up X-Forwarded-For {remote_host} + } +} + +http://id.nodedc.ru { + @auth_root path / + redir @auth_root https://hub.nodedc.ru/ 302 + + @auth_user_dashboard path /if/user /if/user/* + redir @auth_user_dashboard https://hub.nodedc.ru/ 302 + + reverse_proxy authentik-server:9000 { + header_up Host id.nodedc.ru + header_up X-Forwarded-Host id.nodedc.ru + header_up X-Forwarded-Proto https + header_up X-Forwarded-Port 443 + header_up X-Forwarded-For {remote_host} + } +} + +http://hub.nodedc.ru { + reverse_proxy launcher:5173 { + header_up Host hub.nodedc.ru + header_up X-Forwarded-Host hub.nodedc.ru + header_up X-Forwarded-Proto https + header_up X-Forwarded-Port 443 + header_up X-Forwarded-For {remote_host} + } +} + +http://ops.nodedc.ru { + reverse_proxy {$SYNOLOGY_TASK_MANAGER_UPSTREAM} { + header_up Host ops.nodedc.ru + header_up X-Forwarded-Host ops.nodedc.ru + header_up X-Forwarded-Proto https + header_up X-Forwarded-Port 443 + header_up X-Forwarded-For {remote_host} + } +} diff --git a/infra/synology/README.md b/infra/synology/README.md new file mode 100644 index 0000000..41901fe --- /dev/null +++ b/infra/synology/README.md @@ -0,0 +1,100 @@ +# NODE.DC Synology deploy + +Эта папка фиксирует текущий воспроизводимый NAS-deploy для `nodedc-platform` на Synology RS1221RP+. + +## Правила + +- Не выполнять `docker stop`, `docker restart`, `docker compose down`, `docker system prune` для старых проектов. +- Новый compose project: `nodedc-platform`. +- Новая папка на NAS: `/volume1/docker/nodedc-platform`. +- Внутренний HTTP edge использует `18080`, Tasker upstream — `18090`. +- Старые порты `9000` и `5678` заняты старым `nodedc-demo` и не используются. + +## Текущие внешние домены + +```text +https://id.nodedc.ru -> Authentik +https://hub.nodedc.ru -> Launcher / Hub +https://ops.nodedc.ru -> Tasker / Operational Core +``` + +В `Caddyfile.http` эти домены проксируются через локальный HTTP edge, но upstream получает `X-Forwarded-Proto: https` и `X-Forwarded-Port: 443`. + +## Локальные домены для первичной проверки + +На Mac для первичной проверки добавить в `/etc/hosts`: + +```text +172.22.0.222 auth.nas.nodedc +172.22.0.222 launcher.nas.nodedc +172.22.0.222 task.nas.nodedc +``` + +Первичные URL: + +```text +http://auth.nas.nodedc:18080 +http://launcher.nas.nodedc:18080 +http://task.nas.nodedc:18080 +http://task.nas.nodedc:18090 +``` + +## Что входит + +- `docker-compose.platform-http.yml` поднимает новый Authentik, Launcher и Caddy edge. +- `Caddyfile.http` маршрутизирует локальные `auth/launcher/task.nas.nodedc` и внешние `id/hub/ops.nodedc.ru`. +- `deploy-current.sh` синхронизирует compose, Caddyfile, Authentik templates и опционально Launcher source в NAS mount. +- Tasker поднимается отдельным compose из `NODEDC_TASKMANAGER/plane-app/docker-compose.yaml` на порту `18090`. + +## Синхронизация текущего состояния + +С Mac, при смонтированном `/Volumes/docker`: + +```bash +cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform +NAS_ROOT=/Volumes/docker/nodedc-platform \ +LAUNCHER_REPO=/Users/dcconstructions/Downloads/mnt/data/nodedc_launcher \ +./infra/synology/deploy-current.sh +``` + +Скрипт не запускает Docker сам: на NAS `sudo` интерактивный, поэтому команды применения печатаются в конце. + +## Что нужно перед запуском + +- Собрать или загрузить `linux/amd64` images: + - `nodedc/launcher:local` + - `nodedc/plane-frontend:ru` + - `nodedc/plane-admin:ru` + - `nodedc/plane-space:ru` + - `nodedc/plane-live:local` + - `nodedc/plane-backend:local` + - `nodedc/plane-proxy:ru` +- Создать `.env.synology` из `.env.synology.example` и заменить все `replace-with-*`. +- Создать `plane.env.synology` для Tasker из `plane.env.staging.example`, но с HTTP URL на `*.nas.nodedc:18080` и портами `18090/18490`. + +## Обязательные runtime-права + +Launcher пишет runtime snapshot и uploads под пользователем `node` (`uid=1000`). После создания NAS-папок: + +```bash +cd /volume1/docker/nodedc-platform/platform +sudo mkdir -p ../launcher/server-storage ../launcher/uploads +sudo chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads +sudo chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads +``` + +Проверка внутри контейнера: + +```bash +sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc \ + 'touch /app/server/storage/.write-test && rm /app/server/storage/.write-test && echo storage-ok' +``` + +## Проверки после деплоя + +```bash +curl -k -sS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/ \ + | grep -aE 'hub.nodedc.ru|launcher.local|getLauncherBaseUrl|Запросить доступ' +``` + +В выводе должны быть `id.nodedc.ru -> hub.nodedc.ru` и не должно быть `launcher.local`. diff --git a/infra/synology/deploy-current.sh b/infra/synology/deploy-current.sh new file mode 100755 index 0000000..704c79b --- /dev/null +++ b/infra/synology/deploy-current.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PLATFORM_REPO="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +NAS_ROOT="${NAS_ROOT:-/Volumes/docker/nodedc-platform}" +LAUNCHER_REPO="${LAUNCHER_REPO:-}" + +if [[ ! -d "${NAS_ROOT}" ]]; then + echo "NAS_ROOT not found: ${NAS_ROOT}" >&2 + echo "Set NAS_ROOT=/path/to/nodedc-platform when the Synology share is mounted elsewhere." >&2 + exit 1 +fi + +mkdir -p "${NAS_ROOT}/platform" "${NAS_ROOT}/authentik/custom-templates" + +rsync -av \ + "${PLATFORM_REPO}/infra/synology/docker-compose.platform-http.yml" \ + "${NAS_ROOT}/platform/docker-compose.platform-http.yml" + +rsync -av \ + "${PLATFORM_REPO}/infra/synology/Caddyfile.http" \ + "${NAS_ROOT}/platform/Caddyfile.http" + +rsync -av --delete \ + "${PLATFORM_REPO}/infra/authentik/custom-templates/" \ + "${NAS_ROOT}/authentik/custom-templates/" + +if [[ -n "${LAUNCHER_REPO}" ]]; then + if [[ ! -d "${LAUNCHER_REPO}" ]]; then + echo "LAUNCHER_REPO not found: ${LAUNCHER_REPO}" >&2 + exit 1 + fi + + mkdir -p "${NAS_ROOT}/launcher/source" + rsync -av --delete \ + --exclude='.git/' \ + --exclude='node_modules/' \ + --exclude='dist/' \ + --exclude='server/storage/*' \ + --exclude='public/storage/uploads/*' \ + "${LAUNCHER_REPO}/" \ + "${NAS_ROOT}/launcher/source/" +else + echo "LAUNCHER_REPO is not set; launcher source was not synced." +fi + +cat <<'EOF' + +Synced files to NAS mount. Run on Synology to apply runtime changes: + +cd /volume1/docker/nodedc-platform/platform +sudo mkdir -p ../launcher/server-storage ../launcher/uploads +sudo chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads +sudo chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads + +cd /volume1/docker/nodedc-platform/launcher/source +sudo /usr/local/bin/docker build -t nodedc/launcher:local . + +cd /volume1/docker/nodedc-platform/platform +sudo /usr/local/bin/docker compose \ + --env-file /volume1/docker/nodedc-platform/platform/.env.synology \ + -f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \ + up -d --force-recreate launcher reverse-proxy authentik-server authentik-worker + +Verify: + +sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc \ + 'touch /app/server/storage/.write-test && rm /app/server/storage/.write-test && echo storage-ok' + +curl -k -sS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/ \ + | grep -aE 'hub.nodedc.ru|launcher.local|getLauncherBaseUrl|Запросить доступ' +EOF diff --git a/infra/synology/docker-compose.platform-http.yml b/infra/synology/docker-compose.platform-http.yml new file mode 100644 index 0000000..a22af56 --- /dev/null +++ b/infra/synology/docker-compose.platform-http.yml @@ -0,0 +1,136 @@ +name: nodedc-platform + +services: + reverse-proxy: + image: caddy:2-alpine + restart: unless-stopped + env_file: + - ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology} + ports: + - "${PLATFORM_HTTP_PORT:-18080}:80" + volumes: + - ./Caddyfile.http:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + depends_on: + authentik-server: + condition: service_started + launcher: + condition: service_started + extra_hosts: + - "id.nodedc.ru:host-gateway" + - "hub.nodedc.ru:host-gateway" + - "ops.nodedc.ru:host-gateway" + - "host.docker.internal:host-gateway" + networks: + - edge + - identity + + launcher: + image: nodedc/launcher:local + restart: unless-stopped + env_file: + - ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology} + environment: + NODE_ENV: production + PORT: 5173 + NODEDC_LAUNCHER_STORAGE_DIR: /app/server/storage + expose: + - "5173" + volumes: + - ../launcher/server-storage:/app/server/storage + - ../launcher/uploads:/app/dist/storage/uploads + - ../launcher/uploads:/app/public/storage/uploads + extra_hosts: + - "id.nodedc.ru:host-gateway" + - "hub.nodedc.ru:host-gateway" + - "ops.nodedc.ru:host-gateway" + - "${AUTH_DOMAIN:-auth.nas.nodedc}:host-gateway" + - "${LAUNCHER_DOMAIN:-launcher.nas.nodedc}:host-gateway" + - "${TASK_DOMAIN:-task.nas.nodedc}:host-gateway" + networks: + - edge + - identity + + postgresql-authentik: + image: postgres:16-alpine + restart: unless-stopped + env_file: + - ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology} + 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 + networks: + - identity + + authentik-server: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2} + command: server + restart: unless-stopped + env_file: + - ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology} + 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_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING__ENABLED:-false} + AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS: ${AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS:-127.0.0.0/8,172.16.0.0/12} + depends_on: + postgresql-authentik: + condition: service_healthy + expose: + - "9000" + - "9443" + shm_size: 512mb + volumes: + - authentik-data:/data + - ../authentik/custom-templates:/templates:ro + networks: + - identity + + authentik-worker: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2} + command: worker + restart: unless-stopped + env_file: + - ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology} + 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_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING__ENABLED:-false} + depends_on: + postgresql-authentik: + condition: service_healthy + shm_size: 512mb + volumes: + - authentik-data:/data + - authentik-certs:/certs + - ../authentik/custom-templates:/templates:ro + networks: + - identity + +networks: + edge: + identity: + internal: true + +volumes: + authentik-database: + authentik-data: + authentik-certs: + caddy-data: + caddy-config: