Files
cloud-ip-validator/docs/changes/2026-10-01_11-31_authentication-review.md
ayurishchevandClaude Sonnet 5.5 debf2afed2 Add authentication: admin/agent bearer tokens for the API, login for the dashboard
control-api: every route now carries a mandatory access level (admin / agent /
open) in a route table. All /api/v1/admin/* require the admin token; the
write calls of validator-agent and prober (self-check, events, results,
complete) require a separate static agent token; register, heartbeat and
fetching the assignment stay open. Tokens come from env vars, are compared in
constant time and never logged. An empty token leaves that level open with a
startup warning (backward compatible).

validator-agent / prober: apiclient sends the agent token only to control-api.

admin-dashboard: login/password (from env) with a stateless HMAC session
cookie, Origin-based CSRF check, per-IP brute-force throttle, HX-Redirect for
htmx polls, logout in the sidebar; the dashboard calls control-api with the
admin token. Login page layout fixed after review.

Also: env plumbing in docker-compose/rxprod-compose/systemd/config examples,
e2e script with token assertions, tests, docs (API, SETUP, USAGE, DASHBOARD,
README), plan and review under docs/changes/, bin/ rebuilt with new
SHA256SUMS.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:35:24 +03:00

11 KiB
Raw Permalink Blame History

Ревью и тестирование: аутентификация API и UI

Дата: 2026-10-01 11:31 MSK · План: 2026-10-01_11-12_authentication-plan.md Статус: реализовано и проверено на живом окружении; токен агентов на живом стенде намеренно не включён (см. «Состояние живого стенда»).

Итог

Реализовано по плану: ручки управления закрыты токеном администратора, записывающие вызовы валидатора и пробера — отдельным бессрочным токеном агентов, получение настройки и задания остаётся открытым, дашборд закрыт логином и паролем (сессия в подписанной cookie). Код написан отдельным агентом (Sonnet 5.5), ревью и все проверки выполнены независимо (Sonnet 5.5, high). Найден и исправлен один реальный дефект (вёрстка страницы входа); остальные замечания — ограничения дизайна, перечислены ниже.

Что реализовано

Область Изменения
control-api internal/httpapi/routes.go — таблица маршрутов {pattern, handler, access} с обязательным уровнем доступа (33 admin, 5 agent, 7 open); internal/httpapi/auth.go — Authenticator, Bearer, сравнение sha256 + subtle.ConstantTimeCompare, 401 + WWW-Authenticate: Bearer, отказы пишутся в лог без токена; Server.WithAuth; config.auth.{admin_token_env,agent_token_env}
validator-agent, prober apiclient.Client.Token — Bearer на каждом запросе к control-api; WithToken в agentcore/probercore; control_api_token_env. Токен не попадает в IP-echo и проверки
admin-dashboard internal/dashboard/auth.go — вход по логину/паролю, сессия без состояния (HMAC-SHA256), CSRF по Origin/Referer, защита от перебора, HX-Redirect для htmx, защита от open-redirect в next; templates/login.html; «Выйти» и имя пользователя в сайдбаре; Bearer к control-api в client.do; индикатор — только при включённой аутентификации
Развёртывание переменные в deploy/docker/*, rxprod-compose/docker-compose.yml, .env*.example, RUN.txt; EnvironmentFile= в 4 systemd-юнитах; ключи в configs/*.example.yaml и копиях rxprod-compose/sources/
Тесты internal/httpapi/auth_test.go (матрица по таблице маршрутов), internal/apiclient/apiclient_test.go, токены в agentcore/probercore, internal/dashboard/auth_test.go (16 тестов); scripts/run-local-e2e.sh — токены и явные проверки 401/open

Новых зависимостей нет (crypto/* стандартной библиотеки). Режим по умолчанию — обратная совместимость: пустой токен ⇒ соответствующий уровень открыт, в логе предупреждение.

Результаты проверок

Проверка Результат
gofmt, go build ./..., go vet ./... чисто
go test ./... все пакеты зелёные
go test -race (httpapi, dashboard, apiclient, agentcore, probercore) зелёные
scripts/run-local-e2e.sh с токенами exit 0: 5 проверок доступа ok, реальные агент и пробер с токеном довели адрес до pass, автоцикл ok
Живые контейнеры cloud-ip-validator-*, curl 40/40 (control-api 401/200, открытые маршруты, heartbeat внешних валидаторов продолжается; дашборд: редиректы, HX-Redirect, CSRF, cookie-флаги, open-redirect, tampered cookie, выход)
Живой дашборд в настоящем Chromium (Playwright) 15/15: вход/ошибка пароля, фоновый htmx-опрос с сессией, htmx PUT /settings и PUT /settings/auto-cycle (Origin-проверка проходит), выход, нет JS-ошибок
Изолированный стенд civ-authtest-* (mock, отдельная сеть, удалён после прогона) токен агентов: 401 без токена / с токеном администратора / с чужим, принят с токеном агентов; реальный агент без токена регистрируется и шлёт heartbeat (открытые маршруты), а его записи отклоняются (41 отказ в логе); с токеном новых отказов нет, self-check уходит; пробер с токеном регистрируется; перебор: 6-я неверная попытка → 429, Retry-After 600 с, верный пароль в блокировке тоже 429; секреты в логах не найдены

Примечание: 4 «FAIL» в выводе скрипта изолированного стенда — ошибка форматирования самого скрипта (сравнивалось yes со строкой yes (…)); значения в скобках подтверждают успех (HTTP 200, 41 отказ, 3 IP, Retry-After 600). Реальных провалов нет.

Замечания ревью

№ Серьёзность Замечание Статус
1 средняя Страница входа: сломана вёрстка — .field имеет flex: 1 1 220px, в колонке это давало 220px пустоты между полями; поле пароля не попадало в правило стилей input[type=…] и выглядело нестилизованным. Найдено по скриншоту на живом стенде исправлено (dashboard.css: input[type="password"] в общее правило, .login-card .field { flex: 0 0 auto }), перепроверено скриншотом
2 средняя Счётчик перебора ключуется по RemoteAddr и не доверяет X-Forwarded-For. За reverse-proxy все клиенты разделят одну корзину: 5 неверных попыток заблокируют вход всем (DoS на админа). Сейчас не проявляется — дашборд открыт напрямую на :8091, Caddy перед ним нет принято; при публикации через прокси нужен список доверенных прокси
3 низкая Сессия без состояния: «Выйти» стирает cookie в браузере, но украденная копия остаётся валидной до истечения (session_ttl_minutes); смена пароля сессии не отзывает. Инвалидация всех сессий — смена ADMIN_DASHBOARD_SESSION_SECRET и рестарт принято, описано в документации
4 низкая На защищённых страницах нет Cache-Control: no-store (кнопка «Назад» после выхода может показать кэш) не исправлено
5 низкая register и heartbeat открыты (решение пользователя): подделка heartbeat «оживляет» упавший валидатор. Перенос под токен агентов — одна строка в таблице маршрутов принято по решению пользователя
6 низкая Токены и пароль передаются по HTTP открытым текстом, если TLS не терминируется перед сервисами описано в документации (TLS на reverse-proxy)
7 инфо withAuthInfo заполняет PageData через reflect — работает, но хрупко при смене структур не блокирует
8 инфо Не добавлен вариант TestEndToEndHTTPFlow с токенами — покрыто матрицей по таблице маршрутов и e2e-скриптом принято
9 инфо CSRF-проверка сверяет Origin с Host: прокси, подменяющий Host, получит 403 на POST/PUT описано в документации

Состояние живого стенда (rxprod-compose, проект cloud-ip-validator)

  • Включено: токен администратора на control-api (CONTROL_API_ADMIN_TOKEN), тот же токен у дашборда (ADMIN_DASHBOARD_CONTROL_API_TOKEN), вход в дашборд (ADMIN_DASHBOARD_USERNAME / ADMIN_DASHBOARD_PASSWORD), ключ сессии (ADMIN_DASHBOARD_SESSION_SECRET). Значения — в rxprod-compose/.env (gitignored; права ужесточены до 600). Дашборд: http://<хост>:8091/, логин admin.
  • Не включено намеренно: CONTROL_API_AGENT_TOKEN. На стенде работают внешние компоненты со старыми бинарниками — валидаторы validator-1/validator-2 (облачные ВМ) и внешний пробер rxyc: после включения токена агентов их результаты начнут получать 401. Пока записывающие вызовы агентов открыты (в логе control-api — предупреждение agent write API is open).
  • Раскатка токена агентов: (1) обновить бинарники на валидаторах и внешних проберах; (2) задать им CONTROL_API_AGENT_TOKEN (openssl rand -hex 32) и перезапустить; (3) последним добавить тот же токен в .env стенда (control-api и prober) и выполнить docker compose up -d.
  • Образы civ-capi, civ-adash, civ-prober, civ-agent пересобраны; предыдущие сохранены под тегом :pre-auth (откат: docker tag civ-capi:pre-auth civ-capi:latest и docker compose up -d). Бэкап БД и прежнего .env — в каталоге scratchpad сессии (/tmp, временный).
  • bin/ пересобран (CGO_ENABLED=0, -trimpath -ldflags="-s -w"), bin/SHA256SUMS обновлён.

Что осталось

  • Раскатка токена агентов на внешние компоненты (см. выше).
  • По желанию: Cache-Control: no-store (замечание 4), список доверенных прокси для счётчика перебора (замечание 2).