feat(hermes): API-сервер и маршрут на него, OIDC дашборда через Zitadel, readOnlyRootFilesystem #70

Merged
hermes merged 9 commits from refs/pull/70/head into main 2026-09-20 19:01:53 +00:00
hermes commented 2026-09-20 18:47:07 +00:00 (Migrated from git.grachevko.ru)

Что это

Второй шаг переноса агента: наружу через Envoy отдаётся API-сервер, у дашборда basic-auth заменяется на Zitadel, контейнер переводится на readOnlyRootFilesystem.

Файлы

Файл Что
app/helmrelease.yaml env API-сервера и OIDC, readOnlyRootFilesystem, /run как emptyDir + initContainer, readiness на 8642
app/externalsecret.yaml ExternalSecret hermes-oidc из Vault kv/ai/hermes
app/httproute.yaml единый хост hermes.${SECRET_DOMAIN}: /v1 и /health → hermes-app:8642, остальное → hermes-dashboard:9119
app/kustomization.yaml подключение ExternalSecret
ks.yaml dependsOn: external-secrets-stores/security

Маршрутизация: почему по путям, а не отдельным хостом

API живёт на том же домене (правило владельца), а бэкенды у двух сервисов пересекаются только частично:

  • дашборд (9119) занимает /api/*: /api/auth/*, /api/status, /api/health, /api/sessions, /api/config/*, /api/tools/*, /api/logs, /api/cron/*, /api/plugins/*, /api/ws, плюс свой UI и колбэк /auth/callback;
  • API-сервер (8642) занимает /v1/* (chat/completions, responses, models, skills, toolsets, health) и /health.

Непересекающиеся префиксы /v1 и /health уходят на 8642, всё остальное — на дашборд. Служебные эндпоинты API /api/sessions и /api/jobs (у дашборда есть свои одноимённые) снаружи не публикуются — они остаются доступны внутри кластера на http://hermes-app.grachevko.svc.cluster.local:8642. Если понадобятся снаружи — отдельная задача с URLRewrite по префиксу.

Что проверено по исходникам, а не по документации

  1. У API-сервера встроенную аутентификацию отключить нельзя. gateway/platforms/api_server.py:3844 — «Return True when API_SERVER_KEY is present and strong enough to start», :3847 — «Refusing to start: API_SERVER_KEY is required for the API server». Ключ остаётся (он есть в .env, 64 символа), поэтому на /v1 и /health OIDC не вешаем: браузерный логин машинным клиентам не подходит.
  2. У дашборда провайдер self_hosted есть в нашем образе: plugins/dashboard_auth/self_hosted/__init__.py:270-276 читает HERMES_DASHBOARD_OIDC_ISSUER + HERMES_DASHBOARD_OIDC_CLIENT_ID (или dashboard.oauth.self_hosted.*). redirect_uri обязан заканчиваться на /auth/callback (plugins/dashboard_auth/_shared.py:100).
  3. Почему Envoy не может быть единственным слоем аутентификации дашборда. Дашборд слушает pod-IP (иначе Envoy до него не дотянется), а для non-loopback bind'а требует зарегистрированного провайдера и иначе падает закрытым: hermes_cli/web_server.py:471 и hermes_cli/main_dashboard.py:428. Вариант «выключить встроенную аутентификацию и повесить SecurityPolicy OIDC на маршрут» недостижим: либо OIDC-провайдер самого Hermes (этот PR), либо двойной логин.
  4. Порядок выключения basic-auth — после проверки входа. client_id/client_secret подставляются через secretKeyRef … optional: true: пока ключа в Vault нет, провайдер не регистрируется и работает basic-auth из .env. Строки HERMES_DASHBOARD_BASIC_AUTH_* убираются из .env только после успешного входа через Zitadel.
  5. readOnlyRootFilesystem: у s6-overlay вне тома есть ровно одно изменяемое место — /run; оно отдано emptyDir, владельца возвращает initContainer. Паттерн (RO rootfs + drop: ALL + /run и /tmp как emptyDir) взят из публичного home-ops на 119 звёзд, а не выдуман.

Что нужно сделать вне GitOps

  • Уже сделано: hermes config set network.force_ipv4 true и hermes config set dashboard.public_url https://hermes.grachevko.ru.
  • Приложение в Zitadel — создано пользователем как User Agent/PKCE, redirect URI https://hermes.grachevko.ru/auth/callback; в Vault kv/ai/hermes добавлен oidc-client-id (секрет не нужен — PKCE).
  • После проверки входа — убрать HERMES_DASHBOARD_BASIC_AUTH_* из /opt/data/.env.

Проверка после мрежа

  • kubectl -n grachevko get pods — под Ready 1/1, 0 рестартов; initContainer fix-run отработал.
  • Лог: старт без ошибок записи, gateway поднялся, HERMES_DASHBOARD_READY port=9119; при заполненном hermes-oidc дашборд регистрирует self-hosted OIDC-провайдер.
  • https://hermes.${SECRET_DOMAIN}/ — вход через Zitadel; https://hermes.${SECRET_DOMAIN}/health и /v1/models — с Authorization: Bearer $API_SERVER_KEY.

Откат

Реверт PR возвращает basic-auth-режим и прежний маршрут: провайдер не регистрируется, .env до отдельного шага не меняется.

## Что это Второй шаг переноса агента: наружу через Envoy отдаётся API-сервер, у дашборда basic-auth заменяется на Zitadel, контейнер переводится на `readOnlyRootFilesystem`. ## Файлы | Файл | Что | |---|---| | `app/helmrelease.yaml` | env API-сервера и OIDC, `readOnlyRootFilesystem`, `/run` как emptyDir + initContainer, readiness на 8642 | | `app/externalsecret.yaml` | ExternalSecret `hermes-oidc` из Vault `kv/ai/hermes` | | `app/httproute.yaml` | единый хост `hermes.${SECRET_DOMAIN}`: `/v1` и `/health` → `hermes-app:8642`, остальное → `hermes-dashboard:9119` | | `app/kustomization.yaml` | подключение ExternalSecret | | `ks.yaml` | `dependsOn: external-secrets-stores/security` | ## Маршрутизация: почему по путям, а не отдельным хостом API живёт на том же домене (правило владельца), а бэкенды у двух сервисов пересекаются только частично: - **дашборд (9119)** занимает `/api/*`: `/api/auth/*`, `/api/status`, `/api/health`, `/api/sessions`, `/api/config/*`, `/api/tools/*`, `/api/logs`, `/api/cron/*`, `/api/plugins/*`, `/api/ws`, плюс свой UI и колбэк `/auth/callback`; - **API-сервер (8642)** занимает `/v1/*` (`chat/completions`, `responses`, `models`, `skills`, `toolsets`, `health`) и `/health`. Непересекающиеся префиксы `/v1` и `/health` уходят на 8642, всё остальное — на дашборд. Служебные эндпоинты API `/api/sessions` и `/api/jobs` (у дашборда есть свои одноимённые) снаружи не публикуются — они остаются доступны внутри кластера на `http://hermes-app.grachevko.svc.cluster.local:8642`. Если понадобятся снаружи — отдельная задача с `URLRewrite` по префиксу. ## Что проверено по исходникам, а не по документации 1. **У API-сервера встроенную аутентификацию отключить нельзя.** `gateway/platforms/api_server.py:3844` — «Return True when API_SERVER_KEY is present and strong enough to start», `:3847` — «Refusing to start: API_SERVER_KEY is required for the API server». Ключ остаётся (он есть в `.env`, 64 символа), поэтому на `/v1` и `/health` OIDC не вешаем: браузерный логин машинным клиентам не подходит. 2. **У дашборда провайдер self_hosted есть в нашем образе:** `plugins/dashboard_auth/self_hosted/__init__.py:270-276` читает `HERMES_DASHBOARD_OIDC_ISSUER` + `HERMES_DASHBOARD_OIDC_CLIENT_ID` (или `dashboard.oauth.self_hosted.*`). `redirect_uri` обязан заканчиваться на `/auth/callback` (`plugins/dashboard_auth/_shared.py:100`). 3. **Почему Envoy не может быть единственным слоем аутентификации дашборда.** Дашборд слушает pod-IP (иначе Envoy до него не дотянется), а для non-loopback bind'а требует зарегистрированного провайдера и иначе падает закрытым: `hermes_cli/web_server.py:471` и `hermes_cli/main_dashboard.py:428`. Вариант «выключить встроенную аутентификацию и повесить SecurityPolicy OIDC на маршрут» недостижим: либо OIDC-провайдер самого Hermes (этот PR), либо двойной логин. 4. **Порядок выключения basic-auth — после проверки входа.** `client_id`/`client_secret` подставляются через `secretKeyRef … optional: true`: пока ключа в Vault нет, провайдер не регистрируется и работает basic-auth из `.env`. Строки `HERMES_DASHBOARD_BASIC_AUTH_*` убираются из `.env` только после успешного входа через Zitadel. 5. **`readOnlyRootFilesystem`:** у s6-overlay вне тома есть ровно одно изменяемое место — `/run`; оно отдано `emptyDir`, владельца возвращает initContainer. Паттерн (RO rootfs + `drop: ALL` + `/run` и `/tmp` как emptyDir) взят из публичного home-ops на 119 звёзд, а не выдуман. ## Что нужно сделать вне GitOps - Уже сделано: `hermes config set network.force_ipv4 true` и `hermes config set dashboard.public_url https://hermes.grachevko.ru`. - Приложение в Zitadel — создано пользователем как User Agent/PKCE, redirect URI `https://hermes.grachevko.ru/auth/callback`; в Vault `kv/ai/hermes` добавлен `oidc-client-id` (секрет не нужен — PKCE). - После проверки входа — убрать `HERMES_DASHBOARD_BASIC_AUTH_*` из `/opt/data/.env`. ## Проверка после мрежа - `kubectl -n grachevko get pods` — под `Ready 1/1`, 0 рестартов; initContainer `fix-run` отработал. - Лог: старт без ошибок записи, gateway поднялся, `HERMES_DASHBOARD_READY port=9119`; при заполненном `hermes-oidc` дашборд регистрирует self-hosted OIDC-провайдер. - `https://hermes.${SECRET_DOMAIN}/` — вход через Zitadel; `https://hermes.${SECRET_DOMAIN}/health` и `/v1/models` — с `Authorization: Bearer $API_SERVER_KEY`. ## Откат Реверт PR возвращает basic-auth-режим и прежний маршрут: провайдер не регистрируется, `.env` до отдельного шага не меняется.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
grachevko/home-ops!70
No description provided.