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

+7
View File
@@ -78,6 +78,13 @@
</a>
</nav>
{{if .AuthEnabled}}
<form class="sidebar-user" method="post" action="/logout">
<span class="sidebar-user-name" title="{{.User}}">{{.User}}</span>
<button type="submit" class="btn btn-ghost btn-sm">Выйти</button>
</form>
{{end}}
<div class="sidebar-foot">
<span class="sidebar-foot-status"><span class="pulse-dot"></span>control-api</span>
<button type="button" class="theme-toggle" id="themeToggle" aria-label="Переключить тему" title="Переключить тему">
+29
View File
@@ -0,0 +1,29 @@
{{define "login_page"}}
<!doctype html>
<html lang="ru">
<head>{{template "html_head" .}}</head>
<body>
<div class="bg-grid"></div>
<main class="login-shell">
<div class="login-card">
<form class="panel" method="post" action="/login" autocomplete="on">
<div class="panel-head"><h2>Cloud IP Validator — вход</h2></div>
<div class="panel-body">
{{if .Error}}<div class="alert alert-warning" role="alert">{{.Error}}</div>{{end}}
<input type="hidden" name="next" value="{{.Next}}">
<div class="field">
<label for="login-username">Логин</label>
<input type="text" id="login-username" name="username" autocomplete="username" autofocus required>
</div>
<div class="field">
<label for="login-password">Пароль</label>
<input type="password" id="login-password" name="password" autocomplete="current-password" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Войти</button>
</div>
</form>
</div>
</main>
</body>
</html>
{{end}}