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>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-01 11:35:24 +03:00
1 parent 972ad47d0c
commit debf2afed2
67 files changed
+2050 -105

No files matched your search

+13
View File
@@ -14,6 +14,19 @@ PROBER_SITE_ID=site-1
# --- validator-agent (must match a validator_id in the mounted control-api config) ---
VALIDATOR_AGENT_VALIDATOR_ID=validator_01
# --- authentication (empty = that level stays open; services log a warning) ---
# Generate each secret with: openssl rand -hex 32
# control-api admin token (protects /api/v1/admin/*) and the dashboard's copy of it:
CONTROL_API_ADMIN_TOKEN=
ADMIN_DASHBOARD_CONTROL_API_TOKEN=
# control-api agent token (protects result/event writes); the same value goes
# to prober and validator-agent:
CONTROL_API_AGENT_TOKEN=
# Dashboard login (both must be set to enable it) and cookie-signing key:
ADMIN_DASHBOARD_USERNAME=
ADMIN_DASHBOARD_PASSWORD=
ADMIN_DASHBOARD_SESSION_SECRET=
# ADMIN_DASHBOARD_CONTROL_API_URL / PROBER_CONTROL_API_URL /
# VALIDATOR_AGENT_CONTROL_API_URL default to http://control-api:8080 in
# docker-compose.yml (same Docker network) — only set here to override.
+16
View File
@@ -39,3 +39,19 @@ VALIDATOR_AGENT_CONTROL_API_URL=https://control-api.internal.example.com
# --- admin-dashboard (only needed on a dashboard-profile host) ---
ADMIN_DASHBOARD_CONTROL_API_URL=http://control-api:8080
# --- authentication (fill in only what this host needs; empty = open + warning) ---
# Generate each secret with: openssl rand -hex 32
# Rollout order without downtime: update all binaries first, then give the
# tokens to prober / validator-agent / dashboard, and set the control-api
# tokens (and restart it) last.
#
# control-plane host: tokens that control-api enforces
CONTROL_API_ADMIN_TOKEN=
CONTROL_API_AGENT_TOKEN=
# dashboard host: control-api's admin token, the login, and the cookie-signing key
ADMIN_DASHBOARD_CONTROL_API_TOKEN=
ADMIN_DASHBOARD_USERNAME=
ADMIN_DASHBOARD_PASSWORD=
ADMIN_DASHBOARD_SESSION_SECRET=
# prober / validator hosts use CONTROL_API_AGENT_TOKEN (same value as above)
+25
View File
@@ -25,6 +25,7 @@ docker run -d --platform linux/amd64 --cap-add NET_RAW --name prober \
-e PROBER_TCP_TIMEOUT_SECONDS=5 \
-e PROBER_ICMP_TIMEOUT_SECONDS=5 \
-e PROBER_ICMP_COUNT=3 \
-e CONTROL_API_AGENT_TOKEN=<agent_token> \
cloud-ip-validator-prober
Рабочий пример (стенд 62.176.10.113):
@@ -42,6 +43,9 @@ docker run -d --platform linux/amd64 --cap-add NET_RAW --name prober \
PROBER_TCP_TIMEOUT_SECONDS опциональная, дефолт 5
PROBER_ICMP_TIMEOUT_SECONDS опциональная, дефолт 5
PROBER_ICMP_COUNT опциональная, дефолт 3
CONTROL_API_AGENT_TOKEN опциональная: токен агентов control-api (то же
значение, что у control-api); без него результаты
проверок будут отклонены, если токен включён
Примечания:
--platform linux/amd64 бинарник в bin/ собран под Linux x86_64
@@ -61,6 +65,10 @@ docker run -d --platform linux/amd64 -p 8090:8090 --name admin-dashboard \
-e ADMIN_DASHBOARD_CONTROL_API_TIMEOUT_SECONDS=10 \
-e ADMIN_DASHBOARD_LAST_COMPLETED_COUNT=20 \
-e ADMIN_DASHBOARD_POLL_INTERVAL_SECONDS=5 \
-e ADMIN_DASHBOARD_CONTROL_API_TOKEN=<admin_token> \
-e ADMIN_DASHBOARD_USERNAME=<login> \
-e ADMIN_DASHBOARD_PASSWORD=<password> \
-e ADMIN_DASHBOARD_SESSION_SECRET=<random_hex> \
cloud-ip-validator-admin-dashboard
Рабочий пример (стенд 62.176.10.113):
@@ -75,6 +83,14 @@ docker run -d --platform linux/amd64 -p 8090:8090 --name admin-dashboard \
ADMIN_DASHBOARD_CONTROL_API_TIMEOUT_SECONDS опциональная, дефолт 10
ADMIN_DASHBOARD_LAST_COMPLETED_COUNT опциональная, дефолт 20
ADMIN_DASHBOARD_POLL_INTERVAL_SECONDS опциональная, дефолт 5
ADMIN_DASHBOARD_CONTROL_API_TOKEN опциональная: admin-токен control-api
ADMIN_DASHBOARD_USERNAME / _PASSWORD опциональные: логин дашборда; если не
заданы оба — вход не требуется (в логе
предупреждение)
ADMIN_DASHBOARD_SESSION_SECRET опциональная: ключ подписи cookie сессии;
без неё случайный, сессии сбрасываются
рестартом
(секреты генерируются так: openssl rand -hex 32)
Примечания:
--platform linux/amd64 бинарник в bin/ собран под Linux x86_64
@@ -102,12 +118,18 @@ docker run -d --platform linux/amd64 -p 8080:8080 --name control-api \
-e OS_PROJECT_ID=<project_id> \
-e OS_REGION_NAME=<region> \
-e OS_TOKEN=<token> \
-e CONTROL_API_ADMIN_TOKEN=<admin_token> \
-e CONTROL_API_AGENT_TOKEN=<agent_token> \
cloud-ip-validator-control-api
(при auth_method: password — вместо OS_TOKEN передать OS_USERNAME,
OS_USER_DOMAIN_NAME, OS_PASSWORD; для openstack.mode: mock креденшлы не
нужны)
CONTROL_API_ADMIN_TOKEN закрывает /api/v1/admin/*, CONTROL_API_AGENT_TOKEN —
запись результатов/событий агентами и пробером; не заданный токен оставляет
соответствующий уровень открытым (в логе предупреждение).
Примечания:
--platform linux/amd64 бинарник в bin/ собран под Linux x86_64
-p 8080:8080 проброс порта control-api наружу
@@ -132,6 +154,7 @@ docker run -d --platform linux/amd64 --cap-add NET_RAW --name validator-agent \
-e VALIDATOR_AGENT_ICMP_COUNT=3 \
-e VALIDATOR_AGENT_SSH_ENABLED=false \
-e VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS=5 \
-e CONTROL_API_AGENT_TOKEN=<agent_token> \
cloud-ip-validator-validator-agent
Переменные окружения:
@@ -145,6 +168,8 @@ docker run -d --platform linux/amd64 --cap-add NET_RAW --name validator-agent \
VALIDATOR_AGENT_ICMP_COUNT опциональная, дефолт 3
VALIDATOR_AGENT_SSH_ENABLED опциональная, дефолт false
VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS опциональная, дефолт 5
CONTROL_API_AGENT_TOKEN опциональная: токен агентов control-api
(то же значение, что у control-api)
Примечания:
--platform linux/amd64 бинарник в bin/ собран под Linux x86_64
@@ -8,6 +8,10 @@ export ADMIN_DASHBOARD_CONTROL_API_TIMEOUT_SECONDS="${ADMIN_DASHBOARD_CONTROL_AP
export ADMIN_DASHBOARD_LAST_COMPLETED_COUNT="${ADMIN_DASHBOARD_LAST_COMPLETED_COUNT:-20}"
export ADMIN_DASHBOARD_POLL_INTERVAL_SECONDS="${ADMIN_DASHBOARD_POLL_INTERVAL_SECONDS:-5}"
# Tokens/passwords (CONTROL_API_AGENT_TOKEN, ADMIN_DASHBOARD_*) are deliberately
# NOT templated into the YAML: the binary reads them straight from this
# container's environment (names default in the config loader), so secrets
# never land in a file inside the container.
envsubst '${ADMIN_DASHBOARD_LISTEN_ADDR} ${ADMIN_DASHBOARD_CONTROL_API_URL} ${ADMIN_DASHBOARD_CONTROL_API_TIMEOUT_SECONDS} ${ADMIN_DASHBOARD_LAST_COMPLETED_COUNT} ${ADMIN_DASHBOARD_POLL_INTERVAL_SECONDS}' \
< /etc/admin-dashboard/admin-dashboard.yaml.tmpl > /etc/admin-dashboard/admin-dashboard.yaml
@@ -17,6 +17,13 @@ database:
openstack:
mode: "mock"
# Статические bearer-токены читаются из переменных окружения контейнера с
# этими именами (задаются в .env, см. deploy/docker/.env.example). Пусто =
# соответствующий уровень API открыт (с предупреждением в логе).
auth:
admin_token_env: "CONTROL_API_ADMIN_TOKEN" # защищает /api/v1/admin/*
agent_token_env: "CONTROL_API_AGENT_TOKEN" # защищает запись результатов агентами/пробером
orchestrator:
poll_interval_seconds: 5
self_check_timeout_seconds: 60
+15
View File
@@ -34,6 +34,11 @@ services:
networks: [backend]
volumes:
- control-api-db:/var/lib/cloud-ip-validator
environment:
# Static bearer tokens (empty = that API level stays open, with a
# warning in the log). Generate with `openssl rand -hex 32`.
CONTROL_API_ADMIN_TOKEN: "${CONTROL_API_ADMIN_TOKEN:-}"
CONTROL_API_AGENT_TOKEN: "${CONTROL_API_AGENT_TOKEN:-}"
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:8080/healthz"]
interval: 5s
@@ -55,6 +60,12 @@ services:
ADMIN_DASHBOARD_CONTROL_API_TIMEOUT_SECONDS: "${ADMIN_DASHBOARD_CONTROL_API_TIMEOUT_SECONDS:-10}"
ADMIN_DASHBOARD_LAST_COMPLETED_COUNT: "${ADMIN_DASHBOARD_LAST_COMPLETED_COUNT:-20}"
ADMIN_DASHBOARD_POLL_INTERVAL_SECONDS: "${ADMIN_DASHBOARD_POLL_INTERVAL_SECONDS:-5}"
# Admin token of control-api, and the dashboard's own login. Empty
# username/password = login disabled (warning in the log).
ADMIN_DASHBOARD_CONTROL_API_TOKEN: "${ADMIN_DASHBOARD_CONTROL_API_TOKEN:-}"
ADMIN_DASHBOARD_USERNAME: "${ADMIN_DASHBOARD_USERNAME:-}"
ADMIN_DASHBOARD_PASSWORD: "${ADMIN_DASHBOARD_PASSWORD:-}"
ADMIN_DASHBOARD_SESSION_SECRET: "${ADMIN_DASHBOARD_SESSION_SECRET:-}"
prober:
build:
@@ -72,6 +83,8 @@ services:
PROBER_TCP_TIMEOUT_SECONDS: "${PROBER_TCP_TIMEOUT_SECONDS:-5}"
PROBER_ICMP_TIMEOUT_SECONDS: "${PROBER_ICMP_TIMEOUT_SECONDS:-5}"
PROBER_ICMP_COUNT: "${PROBER_ICMP_COUNT:-3}"
# Agent token of control-api (same value as on control-api).
CONTROL_API_AGENT_TOKEN: "${CONTROL_API_AGENT_TOKEN:-}"
validator-agent:
build:
@@ -92,3 +105,5 @@ services:
VALIDATOR_AGENT_ICMP_COUNT: "${VALIDATOR_AGENT_ICMP_COUNT:-3}"
VALIDATOR_AGENT_SSH_ENABLED: "${VALIDATOR_AGENT_SSH_ENABLED:-false}"
VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS: "${VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS:-5}"
# Agent token of control-api (same value as on control-api).
CONTROL_API_AGENT_TOKEN: "${CONTROL_API_AGENT_TOKEN:-}"
@@ -9,6 +9,10 @@ export PROBER_TCP_TIMEOUT_SECONDS="${PROBER_TCP_TIMEOUT_SECONDS:-5}"
export PROBER_ICMP_TIMEOUT_SECONDS="${PROBER_ICMP_TIMEOUT_SECONDS:-5}"
export PROBER_ICMP_COUNT="${PROBER_ICMP_COUNT:-3}"
# Tokens/passwords (CONTROL_API_AGENT_TOKEN, ADMIN_DASHBOARD_*) are deliberately
# NOT templated into the YAML: the binary reads them straight from this
# container's environment (names default in the config loader), so secrets
# never land in a file inside the container.
envsubst '${PROBER_SITE_ID} ${PROBER_CONTROL_API_URL} ${PROBER_POLL_INTERVAL_SECONDS} ${PROBER_TCP_TIMEOUT_SECONDS} ${PROBER_ICMP_TIMEOUT_SECONDS} ${PROBER_ICMP_COUNT}' \
< /etc/prober/prober.yaml.tmpl > /etc/prober/prober.yaml
@@ -12,6 +12,10 @@ export VALIDATOR_AGENT_ICMP_COUNT="${VALIDATOR_AGENT_ICMP_COUNT:-3}"
export VALIDATOR_AGENT_SSH_ENABLED="${VALIDATOR_AGENT_SSH_ENABLED:-false}"
export VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS="${VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS:-5}"
# Tokens/passwords (CONTROL_API_AGENT_TOKEN, ADMIN_DASHBOARD_*) are deliberately
# NOT templated into the YAML: the binary reads them straight from this
# container's environment (names default in the config loader), so secrets
# never land in a file inside the container.
envsubst '${VALIDATOR_AGENT_VALIDATOR_ID} ${VALIDATOR_AGENT_CONTROL_API_URL} ${VALIDATOR_AGENT_POLL_INTERVAL_SECONDS} ${VALIDATOR_AGENT_SELF_CHECK_TIMEOUT_SECONDS} ${VALIDATOR_AGENT_HTTPS_TIMEOUT_SECONDS} ${VALIDATOR_AGENT_ICMP_TIMEOUT_SECONDS} ${VALIDATOR_AGENT_ICMP_COUNT} ${VALIDATOR_AGENT_SSH_ENABLED} ${VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS}' \
< /etc/validator-agent/validator-agent.yaml.tmpl > /etc/validator-agent/validator-agent.yaml