fix(archreview-p1): SEC-02/12/20 dashboard + observability hardening

- SEC-02: DashboardAuthorizationHandler restricts the loopback and
  Authentication:Mode=Disabled bypasses to read-only. They now satisfy only a
  Viewer-bearing requirement (AnyDashboardRole), never AdminOnly, so anonymous
  localhost can view the dashboard but cannot reach API-key CRUD or session
  Close/Kill at the policy layer (previously guarded only by service re-checks).
- SEC-12: DashboardSessionAdminService emits canonical AuditEvents through
  IAuditWriter (actions dashboard-close-session / dashboard-kill-worker, category
  SessionAdmin) on Success/Failure/Denied, mirroring the API-key audit path so
  destructive session actions leave durable, queryable rows.
- SEC-20: drop the unbounded session_id tag from the exported
  mxgateway.heartbeats.failed counter (per-session detail stays in the snapshot/log).

Docs updated same-change: CLAUDE.md (read-only loopback + 5-min bearer),
GatewayDashboardDesign.md (bypass scoping + session-admin audit), Metrics.md.
Server build clean; 30/30 targeted + 295/295 Dashboard/Security/App/Metrics sweep.
This commit is contained in:
Joseph Doherty
2026-07-09 07:47:51 -04:00
parent c2df4a0aae
commit 74e815c4d7
10 changed files with 347 additions and 20 deletions
+19 -5
View File
@@ -257,6 +257,15 @@ gated by a confirmation dialog before it reaches `ISessionManager`.
is killed immediately with no graceful-shutdown attempt. The session is
removed from the registry and the open-session slot is released either way.
Both actions write a canonical `AuditEvent` through `IAuditWriter` into the
`audit_event` store (category `SessionAdmin`, actions `dashboard-close-session`
and `dashboard-kill-worker`) in addition to the operational `ILogger` line — so a
worker killed mid-production leaves a durable, queryable row rather than only a
rotatable log entry. The event records the LDAP actor, the session id (`Target`),
the remote address, and an outcome of `Success`, `Failure`, or `Denied`
(an unauthorized attempt is still audited as `Denied`). This mirrors the API-key
management audit path.
### Workers page
Show:
@@ -436,11 +445,16 @@ Three authorization policies are registered:
cookie OR a `MxGateway.Dashboard.HubToken` bearer (used by WebSocket upgrades
where the cookie can't be forwarded).
Two environmental bypasses still apply: `MxGateway:Authentication:Mode = Disabled`
authorizes every request, and `MxGateway:Dashboard:AllowAnonymousLocalhost`
(default `true`) authorizes any loopback request without a role check. Remote
requests always require an authenticated principal carrying at least the
Viewer role.
Two environmental bypasses still apply, both scoped to **read-only** access:
`MxGateway:Authentication:Mode = Disabled` and `MxGateway:Dashboard:AllowAnonymousLocalhost`
(default `true`, loopback only) each satisfy a requirement that includes the Viewer
role (`AnyDashboardRole`) but **never** the `AdminOnly` requirement. Destructive/admin
surfaces (API-key CRUD, session Close, worker Kill) still demand a real `Administrator`
role claim, so the "anonymous localhost is read-only" contract holds at the policy layer
rather than only in downstream service re-checks. Remote requests (outside the `Disabled`
bypass) always require an authenticated principal carrying at least the Viewer role. The
loopback test trusts `Connection.RemoteIpAddress`; if forwarded-headers middleware is ever
added upstream, `DashboardAuthorizationHandler.IsLoopbackRequest()` must be revisited.
### DisableLogin dev bypass
+1 -1
View File
@@ -44,7 +44,7 @@ All counters are `Counter<long>`. Tag values come from the call sites listed und
| `mxgateway.faults` | `category` | Faults reported by session, event, or worker code paths. The category is a `SessionManagerErrorCode` or `WorkerClientErrorCode` name. |
| `mxgateway.workers.killed` | `reason` | Forced terminations of worker processes. |
| `mxgateway.workers.exited` | `reason` | Clean or fault-driven worker exits. |
| `mxgateway.heartbeats.failed` | `session_id` | Worker heartbeat misses tracked per session. |
| `mxgateway.heartbeats.failed` | *(none)* | Aggregate worker heartbeat misses. The exported counter carries **no** `session_id` tag — per-session attribution is unbounded cardinality (every session mints a new time series), so it is kept out of the exporter; use the dashboard snapshot or the log scope for per-session detail. |
| `mxgateway.grpc.streams.disconnected` | `reason` | Detachments of the dashboard or client gRPC event stream. |
| `mxgateway.retries.attempted` | `area` | Resilience retries executed by gateway components. |