diff --git a/docs/CURRENT_INFRA_HANDOFF.md b/docs/CURRENT_INFRA_HANDOFF.md index 539c4ba..728f31f 100644 --- a/docs/CURRENT_INFRA_HANDOFF.md +++ b/docs/CURRENT_INFRA_HANDOFF.md @@ -61,17 +61,24 @@ https://ops.nodedc.ru -> Tasker / Operational Core https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP endpoint ``` +`id.nodedc.ru` is only the public OIDC/login host. Authentik Admin is deliberately kept off that public host; `/if/admin/*` returns `404` there. + Local/NAS check domains used during rollout: ```text auth.nas.nodedc +auth-admin.nas.nodedc +172.22.0.222 launcher.nas.nodedc task.nas.nodedc auth.local.nodedc +auth-admin.local.nodedc launcher.local.nodedc task.local.nodedc ``` +Use `http://auth-admin.nas.nodedc:18080/if/admin/` for technical Authentik Admin access from the NAS/local network. If a workstation does not resolve `auth-admin.nas.nodedc`, use `http://172.22.0.222:18080/if/admin/` as the local IP fallback. Both entrypoints keep Authentik access separate from the public `id.nodedc.ru` session and must not load NODE.DC auth-flow CSS. + ### Platform compose project Compose project: @@ -278,7 +285,14 @@ The platform Authentik service exposes a stable identity-network alias in the Sy nodedc-platform-authentik-server ``` -When changing this alias or Authentik env wiring, recreate `authentik-server`, `authentik-worker`, and `launcher` together. A freshly recreated Authentik server can temporarily return `503 Service Unavailable` until the worker/bootstrap path is ready, so verify with retry from inside the launcher container. For Launcher-only BFF/frontend fixes, rebuild `nodedc/launcher:local` and recreate only `launcher`. +When changing this alias or Authentik env wiring, recreate `authentik-server`, `authentik-worker`, and `launcher` together. A freshly recreated Authentik server can temporarily return `503 Service Unavailable` until the worker/bootstrap path is ready, so verify with retry from inside the launcher container. For Launcher-only BFF/frontend fixes, rebuild `nodedc/launcher:local` with `--no-cache` and recreate only `launcher`. + +Authentik Brand custom CSS must stay empty in live DB. NODE.DC auth/login styling is injected only by templates on the public auth-flow host. After any Authentik template rollout run: + +```bash +cd /volume1/docker/nodedc-platform/platform +sudo bash clear-authentik-brand-css.sh +``` Launcher image build is done from the Launcher repo when frontend/backend code changes: diff --git a/infra/.env.example b/infra/.env.example index 828459c..2c4290b 100644 --- a/infra/.env.example +++ b/infra/.env.example @@ -1,5 +1,6 @@ # domains AUTH_DOMAIN=auth.local.nodedc +AUTH_ADMIN_DOMAIN=auth-admin.local.nodedc LAUNCHER_DOMAIN=launcher.local.nodedc TASK_DOMAIN=task.local.nodedc diff --git a/infra/authentik/README.md b/infra/authentik/README.md index c90f739..7e6b772 100644 --- a/infra/authentik/README.md +++ b/infra/authentik/README.md @@ -36,10 +36,16 @@ Later phases should add reproducible configuration for: ## NODE.DC branded login -`custom-templates/branding/nodedc-login.css` is mounted into Authentik at `/templates/branding/nodedc-login.css` and applied by `bootstrap-dev.py` through the native Authentik Brand `branding_custom_css` field. +`custom-templates/branding/nodedc-login.css` is mounted into Authentik at `/templates/branding/nodedc-login.css`, but it must not be stored in the native Authentik Brand `branding_custom_css` field. -`custom-templates/base/header_js.html` keeps Authentik's native config script and adds a minimal NODE.DC field enhancement for the email clear control and password placeholder only. +Authentik Brand custom CSS is global. Authentik also passes it into the Admin/User web component runtime, so broad login selectors can break list pages, toolbars, tables, and other Admin interface content. Keep `Brand.branding_custom_css` empty in local bootstrap and Synology deploy scripts. + +`custom-templates/base/header_js.html` keeps Authentik's native config script and applies the NODE.DC favicon globally. It renders NODE.DC auth-screen CSS/DOM helpers only on `/if/flow/`, `/flows/`, and `/login/` paths, and excludes technical admin hosts such as `auth-admin.nas.nodedc` / `auth-admin.local.nodedc`. On public auth paths only, it also passes `nodedc-login.css` into `window.authentik.brand` so Authentik's Lit web components can style their shadow DOM. Admin and User paths receive an empty runtime brand CSS value. + +`custom-templates/base/skeleton.html` leaves ` + +{% endif %} +{% endif %} +{% endwith %} +{% with request_host=request.get_host %} +{% if request_host|slice:":11" != "auth-admin." and request_host|slice:":9" != "id-admin." and request_host|slice:":12" != "172.22.0.222" %} +{% if request.path|slice:":9" == "/if/flow/" or request.path|slice:":7" == "/flows/" or request.path|slice:":7" == "/login/" %} +{% endif %} +{% endif %} +{% endwith %} diff --git a/infra/authentik/custom-templates/base/skeleton.html b/infra/authentik/custom-templates/base/skeleton.html new file mode 100644 index 0000000..8949a26 --- /dev/null +++ b/infra/authentik/custom-templates/base/skeleton.html @@ -0,0 +1,50 @@ +{% load static %} +{% load i18n %} +{% load authentik_core %} +{% get_current_language as LANGUAGE_CODE %} + + + + + + + {# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #} + + {% block title %}{% trans title|default:brand.branding_title %}{% endblock %} + + + + {% block head_before %} + {% endblock %} + + {% include "base/theme.html" %} + + {% with request_host=request.get_host %} + {% if request_host|slice:":11" != "auth-admin." and request_host|slice:":9" != "id-admin." and request_host|slice:":12" != "172.22.0.222" %} + {% if request.path|slice:":9" == "/if/flow/" or request.path|slice:":7" == "/flows/" or request.path|slice:":7" == "/login/" %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endwith %} + + {% block head %} + {% endblock %} + {% for key, value in html_meta.items %} + + {% endfor %} + + + {% block body %} + {% endblock %} + {% block scripts %} + {% endblock %} + + diff --git a/infra/authentik/custom-templates/branding/nodedc-login.css b/infra/authentik/custom-templates/branding/nodedc-login.css index a5841c2..47b6a50 100644 --- a/infra/authentik/custom-templates/branding/nodedc-login.css +++ b/infra/authentik/custom-templates/branding/nodedc-login.css @@ -280,7 +280,7 @@ body.nodedc-auth-permission-denied .pf-c-login__main-header::after { } body.nodedc-auth-permission-denied .pf-c-login__main-header::after { - content: "Доступ к модулю NODE.DC ограничен." !important; + content: "Доступ запрещён." !important; } .pf-c-login__main-header[data-nodedc-permission-denied="true"] .pf-c-title, @@ -292,6 +292,16 @@ body.nodedc-auth-permission-denied h1.pf-c-title { display: none !important; } +body.nodedc-auth-permission-denied ak-flow-executor::part(main), +body.nodedc-auth-permission-denied .pf-c-login__main { + background: transparent !important; + border: 0 !important; + outline: 0 !important; + box-shadow: none !important; + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} + .pf-c-login__main-body { padding: 0 !important; } diff --git a/infra/authentik/custom-templates/if/admin.html b/infra/authentik/custom-templates/if/admin.html new file mode 100644 index 0000000..1dda875 --- /dev/null +++ b/infra/authentik/custom-templates/if/admin.html @@ -0,0 +1,16 @@ +{% extends "base/skeleton.html" %} + +{% load authentik_core %} + +{% block head %} + +{% include "base/header_js.html" %} +{% endblock %} + +{% block body %} + + + + {% include "base/placeholder.html" %} + +{% endblock %} diff --git a/infra/reverse-proxy/Caddyfile b/infra/reverse-proxy/Caddyfile index c4cb849..3ee4b32 100644 --- a/infra/reverse-proxy/Caddyfile +++ b/infra/reverse-proxy/Caddyfile @@ -3,6 +3,9 @@ } http://{$AUTH_DOMAIN:auth.local.nodedc} { + @auth_admin path /if/admin /if/admin/* + redir @auth_admin http://{$AUTH_ADMIN_DOMAIN:auth-admin.local.nodedc}{uri} 302 + @auth_root path / redir @auth_root http://{$LAUNCHER_DOMAIN:launcher.local.nodedc}/ 302 @@ -16,6 +19,17 @@ http://{$AUTH_DOMAIN:auth.local.nodedc} { } } +http://{$AUTH_ADMIN_DOMAIN:auth-admin.local.nodedc} { + @admin_root path / + redir @admin_root /if/admin/ 302 + + 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 localhost:5173 diff --git a/infra/synology/.env.synology.example b/infra/synology/.env.synology.example index dc9bcaa..cd80559 100644 --- a/infra/synology/.env.synology.example +++ b/infra/synology/.env.synology.example @@ -2,6 +2,8 @@ # This intentionally uses high ports and does not touch existing nodedc-demo. AUTH_DOMAIN=auth.nas.nodedc +AUTH_ADMIN_DOMAIN=auth-admin.nas.nodedc +AUTH_ADMIN_LAN_HOST=172.22.0.222 LAUNCHER_DOMAIN=launcher.nas.nodedc TASK_DOMAIN=task.nas.nodedc NODEDC_PUBLIC_HTTP_PORT=18080 diff --git a/infra/synology/Caddyfile.http b/infra/synology/Caddyfile.http index 79e45bf..4bbe9dc 100644 --- a/infra/synology/Caddyfile.http +++ b/infra/synology/Caddyfile.http @@ -3,6 +3,9 @@ } http://{$AUTH_DOMAIN} { + @auth_admin path /if/admin /if/admin/* + redir @auth_admin http://{$AUTH_ADMIN_DOMAIN:auth-admin.nas.nodedc}:{$NODEDC_PUBLIC_HTTP_PORT}{uri} 302 + @auth_root path / redir @auth_root http://{$LAUNCHER_DOMAIN}:{$NODEDC_PUBLIC_HTTP_PORT}/ 302 @@ -16,6 +19,17 @@ http://{$AUTH_DOMAIN} { } } +http://{$AUTH_ADMIN_DOMAIN:auth-admin.nas.nodedc}, http://{$AUTH_ADMIN_LAN_HOST:172.22.0.222} { + @admin_root path / + redir @admin_root /if/admin/ 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} @@ -35,6 +49,9 @@ http://{$TASK_DOMAIN} { } http://id.nodedc.ru { + @auth_admin path /if/admin /if/admin/* + respond @auth_admin 404 + @auth_root path / redir @auth_root https://hub.nodedc.ru/ 302 diff --git a/infra/synology/README.md b/infra/synology/README.md index a99d3c3..e52dec7 100644 --- a/infra/synology/README.md +++ b/infra/synology/README.md @@ -19,6 +19,8 @@ https://ops.nodedc.ru -> Tasker / Operational Core https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP ``` +`id.nodedc.ru` is the user-facing OIDC/login host. Authentik Admin is intentionally not exposed through this public host; `/if/admin/*` returns `404` there. + В `Caddyfile.http` эти домены проксируются через локальный HTTP edge, но upstream получает `X-Forwarded-Proto: https` и `X-Forwarded-Port: 443`. ## Локальные домены для первичной проверки @@ -27,6 +29,7 @@ https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP ```text 172.22.0.222 auth.nas.nodedc +172.22.0.222 auth-admin.nas.nodedc 172.22.0.222 launcher.nas.nodedc 172.22.0.222 task.nas.nodedc ``` @@ -35,15 +38,19 @@ https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP ```text http://auth.nas.nodedc:18080 +http://auth-admin.nas.nodedc:18080/if/admin/ +http://172.22.0.222:18080/if/admin/ http://launcher.nas.nodedc:18080 http://task.nas.nodedc:18080 http://task.nas.nodedc:18090 ``` +`auth-admin.nas.nodedc` is the technical Authentik Admin entrypoint. `172.22.0.222:18080` is a local IP fallback for workstations without `auth-admin.nas.nodedc` DNS. Both keep Authentik access separate from the public `id.nodedc.ru` login entrypoint and do not load NODE.DC auth-flow CSS. + ## Что входит - `docker-compose.platform-http.yml` поднимает новый Authentik, Launcher и Caddy edge. -- `Caddyfile.http` маршрутизирует локальные `auth/launcher/task.nas.nodedc` и внешние `id/hub/ops.nodedc.ru`. +- `Caddyfile.http` маршрутизирует локальные `auth/auth-admin/launcher/task.nas.nodedc`, IP fallback `172.22.0.222` для Authentik Admin и внешние `id/hub/ops.nodedc.ru`. - `deploy-current.sh` синхронизирует compose, Caddyfile и опционально Launcher source в NAS mount. Authentik templates синхронизируются только при явном `SYNC_AUTHENTIK_TEMPLATES=1`. - `backup-current.sh` делает snapshot Launcher runtime/uploads/Auth templates/config и готовит команду `pg_dump` для Authentik Postgres. - Tasker поднимается отдельным compose из `NODEDC_TASKMANAGER/plane-app/docker-compose.yaml` на порту `18090`. @@ -134,7 +141,7 @@ TASKER_SYNC_SOURCE=1 ./infra/synology/deploy-current.sh ```bash cd /volume1/docker/nodedc-platform/launcher/source -sudo /usr/local/bin/docker build -t nodedc/launcher:local . +sudo /usr/local/bin/docker build --no-cache -t nodedc/launcher:local . cd /volume1/docker/nodedc-platform/platform sudo /usr/local/bin/docker compose \ @@ -143,7 +150,61 @@ sudo /usr/local/bin/docker compose \ up -d --force-recreate --no-deps launcher ``` -После такого deploy проверить `healthz`, запись в launcher storage/uploads и сценарий пользователя без аппрува: сохранение аватара не должно показывать экран `Заявка ожидает подтверждения`. +После такого deploy проверить `healthz`, запись в launcher storage/uploads и сценарий пользователя без аппрува: сохранение аватара не должно показывать экран `Заявка ожидает подтверждения`. Дополнительно проверить, что live bundle больше не содержит старый pending gate: + +```bash +launcher_asset="$( + curl -k -sS --compressed -H 'Accept: text/html' https://hub.nodedc.ru/ \ + | grep -aoE 'index-[A-Za-z0-9_-]+\.js' \ + | head -n 1 +)" +test -n "$launcher_asset" +if curl -k -sS --compressed "https://hub.nodedc.ru/assets/${launcher_asset}" \ + | grep -aq 'Заявка ожидает подтверждения'; then + echo 'old pending gate still present' + exit 1 +fi +echo 'launcher-pending-gate-ok' +``` + +## Authentik Admin и Brand CSS + +NODE.DC auth-flow CSS must stay template-scoped. Do not store it in Authentik `Brand.branding_custom_css`: Authentik passes that CSS into Admin/User web component runtime and breaks native controls. + +After syncing Authentik templates to NAS, recreate `reverse-proxy authentik-server authentik-worker launcher`, then clear existing global Brand CSS in the live DB: + +```bash +cd /volume1/docker/nodedc-platform/platform +sudo bash clear-authentik-brand-css.sh +``` + +Verify: + +```bash +id_admin_status="$( + curl -k -sS -o /dev/null -w '%{http_code}' https://id.nodedc.ru/if/admin/ +)" +if [[ "$id_admin_status" != "404" ]]; then + echo "public id admin is not closed: status=${id_admin_status}" + exit 1 +fi +echo 'public-id-admin-closed-ok' + +auth_admin_page="$( + curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/admin/ +)" +printf '%s' "$auth_admin_page" \ + | grep -aE '|authentikBrand.branding_custom_css = ""' + +auth_admin_flow="$( + curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/flow/default-authentication-flow/ +)" +if printf '%s' "$auth_admin_flow" | grep -aq '|authentikBrand.branding_custom_css = ""' + +auth_admin_flow="$( + curl -k -fsS --compressed -H 'Host: auth-admin.nas.nodedc' http://127.0.0.1:18080/if/flow/default-authentication-flow/ +)" +if printf '%s' "$auth_admin_flow" | grep -aq '|authentikBrand.branding_custom_css = ""' + +auth_admin_flow="$( + curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/flow/default-authentication-flow/ +)" +if printf '%s' "$auth_admin_flow" | grep -aq '|authentikBrand.branding_custom_css = ""' + +auth_admin_flow="$( + fetch_with_retry http://127.0.0.1:18080/if/flow/default-authentication-flow/ -H 'Host: auth-admin.nas.nodedc' +)" +if printf '%s' "$auth_admin_flow" | grep -aq '|authentikBrand.branding_custom_css = ""' + +echo "== launcher bundle check ==" +launcher_html="$(fetch_with_retry https://hub.nodedc.ru/ -H 'Accept: text/html')" +launcher_asset="$( + printf '%s' "$launcher_html" \ + | grep -aoE 'index-[A-Za-z0-9_-]+\.js' \ + | head -n 1 +)" +test -n "$launcher_asset" +echo "launcher_asset=${launcher_asset}" +if fetch_with_retry "https://hub.nodedc.ru/assets/${launcher_asset}" \ + | grep -aq 'Заявка ожидает подтверждения'; then + echo "old pending gate still present" + exit 1 +fi +echo "launcher-pending-gate-ok" + +echo "runtime-verify-ok"