Commit 8a9bab4
authored
admin: P1 foundation (auth, router, cluster, listener) — no writes yet (#623)
## Summary
First PR toward the admin dashboard designed in
`docs/design/2026_04_24_proposed_admin_dashboard.md` (merged as #611).
Introduces the **read-only foundation**: listener wiring, auth, router,
middleware, cluster info / healthz endpoints. No write endpoints are
included — per the design doc P1 DoD they ship together with
`AdminForward` and the 3.3.2 acceptance criteria in a follow-up.
### What is in scope
- `internal/admin/` package: config validation, JWT (HS256 + 2-key
rotation), strict-prefix router, middleware chain (body limit, session
auth, role gate, CSRF double-submit, audit slog), login/logout with
per-IP rate limiter, cluster + healthz handlers, `Server` facade.
- `main_admin.go`: flag wiring, config-to-`admin.Config` translation,
TLS and loopback enforcement, errgroup lifecycle registration.
- Full unit tests and two in-process integration tests (plaintext
listener + self-signed TLS).
### What is NOT in scope (deferred to follow-up PRs)
- `AdminForward` internal gRPC RPC and follower→leader forwarding
(Section 3.3.2 acceptance criteria 1–6).
- Adapter internal entrypoints taking `AuthPrincipal` (DynamoDB
`CreateTable`/`DeleteTable`, S3
`CreateBucket`/`DeleteBucket`/`PutBucketAcl`).
- Any write endpoint on the admin surface.
- React SPA + `go:embed` (design P3).
This keeps the first PR focused and reviewable; the DoD remains
respected because no write endpoint ships without the acceptance
criteria being green.
### Security posture
- Admin is **off by default** (`-adminEnabled=false`), default bind is
loopback.
- Non-loopback bind without TLS is a **hard startup failure** unless the
explicit opt-out flag is set.
- Session cookie: `HttpOnly` + `Secure` + `SameSite=Strict` +
`Path=/admin` + `Max-Age=3600`.
- CSRF: double-submit cookie; `localStorage` is never used.
- Role overlap between `read_only_access_keys` and `full_access_keys` is
a hard startup failure (no silent last-writer-wins).
- JWT signing key is cluster-shared and rotatable (primary + previous);
missing key with admin enabled is a hard startup failure.
- Login is rate-limited to 5 req/min per IP, constant-time secret
comparison.
- POST/PUT bodies are capped at 64 KiB via `http.MaxBytesReader`.
- Every state-changing admin request is logged with `admin_audit` slog
attributes.
## Test plan
- [x] `go test -race ./internal/admin/... .` — green
- [x] `golangci-lint run ./... --timeout=5m` — 0 issues
- [x] In-process boot of admin listener over plaintext; `/admin/healthz`
returns `ok`
- [x] In-process boot with self-signed TLS; `/admin/healthz` over HTTPS
returns `ok`
- [x] Invalid config (non-loopback without TLS, missing signing key,
duplicate role assignment, wrong-length base64 key) rejected at startup
with a descriptive error
- [x] Login happy path issues both cookies with the hardened attributes
verified via regex
- [x] Rate-limiter test: 6th login attempt from the same IP returns
`429` + `Retry-After: 60`; different IPs are independent
- [x] JWT verifier accepts tokens signed by the previous key during
rotation and rejects them after rotation completes
- [ ] Follow-up PR: AdminForward + write endpoints + acceptance-criteria
test matrix
## Related
- Design doc: #611 (merged)
- Follow-up tracks AdminForward + write endpoints + SPA.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Optional admin HTTP API server with secure session tokens, CSRF
protection, and audit logging
* /admin/api/v1/auth/login and /admin/api/v1/auth/logout with hardened
cookies and rate limiting
* /admin/api/v1/cluster for cluster/status queries, /admin/healthz and
static asset/SPA serving
* Role-based access (read-only vs full) and support for signing-key
rotation
* **Tests**
* Extensive end-to-end and unit tests covering auth, CSRF, rate
limiting, routing, config validation, and JWT handling
<!-- end of auto-generated comment: release notes by coderabbit.ai -->26 files changed
Lines changed: 4531 additions & 3 deletions
File tree
- internal
- admin
- raftengine/etcd
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
0 commit comments