feat(security): opt-in authentication for /metrics (SEC-14)

New MxGateway:Metrics:RequireAuthentication (default false). When true,
/metrics requires an API key with the admin scope (verified through the
cached verifier and the shared API-key failure limiter) or a dashboard
login with a real dashboard role; anonymous localhost and
Authentication:Mode=Disabled do not qualify. An anonymous scrape gets a
plain 401 Bearer challenge, a throttled peer 429. /health stays anonymous
because the off-repo Overview dashboard polls /health/active.

MapZbMetrics returns IEndpointConventionBuilder, so the policy is attached
with RequireAuthorization.
This commit is contained in:
Joseph Doherty
2026-10-22 06:37:31 -04:00
parent ed3a2c9c8b
commit 65bca804bb
10 changed files with 619 additions and 3 deletions
+29
View File
@@ -419,6 +419,35 @@ model requires otherwise.
| `MxGateway:Security:AuditRetentionDays` | `90` | Days of canonical audit history kept in the `audit_event` table. The audit drain sweeps once at startup and hourly thereafter, deleting rows older than this window; without it the table grows without bound inside the same SQLite file the authentication hot path reads. Rows whose timestamp SQLite cannot parse are never swept. Must be greater than zero — retention can be widened but not switched off. |
| `MxGateway:Security:ApiKeyFailureTrackedPeers` | `4096` | Maximum distinct partitions tracked by the failure counter (a bounded LRU) so a spray of unique tokens cannot grow memory without limit. It cannot be used to flush an active block either: only a validly shaped `mxgw_<keyId>_<secret>` token mints a key-id partition (everything else lands on the sender's transport-peer partition), each address may mint at most 32 key-id partitions before the overflow collapses onto that address's fallback partition, and eviction prefers fully expired windows, never removing an over-limit partition until the map exceeds twice this cap. Must be greater than zero. |
## Metrics Options
Bound from `MxGateway:Metrics`. Governs the Prometheus `/metrics` scrape endpoint
(mounted by the shared `ZB.MOM.WW.Telemetry` package). The health endpoints
(`/healthz`, `/health/ready`, `/health/active`) are not affected by this section and
always stay anonymous: the family Overview dashboard polls `/health/active` from
another repo without credentials, so gating it would break that consumer.
| Option | Default | Description |
|--------|---------|-------------|
| `MxGateway:Metrics:RequireAuthentication` | `false` | When `true`, `/metrics` requires an authenticated caller (SEC-14). A scrape is accepted when it presents an API key carrying the `admin` scope as `Authorization: Bearer mxgw_<keyId>_<secret>`, or — with no authorization header — a dashboard login cookie holding a real dashboard role (`Administrator` or `Viewer`). Anonymous localhost access and `Authentication:Mode=Disabled` do **not** satisfy it. An anonymous scrape gets `401` with `WWW-Authenticate: Bearer`; an authenticated caller without `admin` scope or a dashboard role gets `403`; a peer throttled by the API-key failure limiter (`MxGateway:Security:ApiKeyFailure*`, shared with the gRPC path) gets `429`. The default keeps the endpoint anonymous so existing scrapers keep working. |
Configure a Prometheus scraper against an authenticated endpoint with a dedicated
admin-scoped key, for example:
```yaml
scrape_configs:
- job_name: mxgateway
authorization:
type: Bearer
credentials_file: /etc/prometheus/mxgateway.key # contains mxgw_<keyId>_<secret>
static_configs:
- targets: ["gateway-host:5130"]
```
The setting is read from configuration when endpoints are mapped, so changing it
requires a restart. With dev-only `Dashboard:DisableLogin=true` every request is
auto-authenticated as an Administrator, which also satisfies `/metrics`.
## Galaxy Options
| Option | Default | Description |