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"