Authentication Security Audit

Last audit date: 2026-04-23 (phase 6 security hardening refresh)

Scope

This audit covers the authentication, authorization, and request-enforcement paths in MCP Hangar after the phase 6 security hardening work:

  • API key and JWT/OIDC authentication
  • Role-based access control (RBAC) and policy:write authorization
  • Tool access policies (TAP)
  • HTTP and WebSocket auth enforcement
  • Browser-oriented CSRF defense-in-depth on session suspension
  • Core package loading between src/ and src/mcp_hangar/

Current Security Posture

Authentication

  • API requests rely on request.state.auth; WebSocket / generic ASGI paths use scope["auth"].
  • HTTP and WebSocket auth enforcement now share one core implementation in src/mcp_hangar/server/api/middleware.py.
  • WebSocket connections support ?token= bearer mapping for clients that cannot set custom headers.
  • Trusted proxy resolution is centralized through TrustedProxyResolver, preventing spoofed X-Forwarded-For from untrusted peers.

Authorization

  • /api/mcp_servers/{id}/l7_policy (L7 egress policy push) no longer trusts a magic internal header.
  • Policy push now requires an authenticated principal plus policy:write authorization.
  • Failed policy pushes emit PolicyPushRejected audit events.
  • The agent role was retired along with the (now-discontinued) cluster agent product; policy:write remains a valid permission and is now granted via the admin role.

Browser CSRF Defense

  • CSRF enforcement is intentionally scoped to browser-style session suspension requests.
  • POST /sessions/{session_id}/suspend requires X-Requested-With only when the request looks browser-originated (Origin, Referer, or Cookie present).
  • API key clients, bearer-token clients, and non-browser API callers bypass the CSRF check.
  • This keeps REST API automation compatible while still defending against browser-triggered session suspension.

WebSocket Security

  • Authentication failures close the socket with code 1008 before the connection is used.
  • Origin validation happens before websocket.accept() to mitigate cross-site WebSocket hijacking.
  • Per-connection backpressure is enforced with bounded queues.

Module Boundary (enterprise internal module)

There is no paid tier or license split: MCP Hangar ships as a single MIT-licensed package. enterprise here is an internal module name (it houses the approvals service and related optional integrations), not a commercial edition or licensed add-on. This section covers the import hygiene around that module boundary.

  • Core bootstrap/router code no longer scatters direct enterprise.* imports across server modules.
  • Optional integrations are resolved via src/mcp_hangar/server/bootstrap/enterprise.py.
  • The boundary exposes provider hooks for:
    • auth CQRS registration
    • API route extension
    • event store creation
    • observability adapter creation
    • legacy bootstrap compatibility exports
  • Entry points are supported when available; the monorepo layout uses a controlled fallback loader so development remains functional without breaking the core boundary.

Findings Status

FindingStatusNotes
K-1 L7 policy push auth bypassFixed/api/mcp_servers/{id}/l7_policy requires authenticated principal + policy:write; rejection events emitted
K-2 WebSocket auth / CSWSH gapsFixedShared auth enforcement, pre-accept Origin validation, bounded queue backpressure
K-3 Unsafe unauthenticated HTTP exposureFixedNon-loopback HTTP bind blocked without auth unless explicitly overridden
K-4 CORS / host / CSRF hardeningFixedExplicit CORS config, TrustedHostMiddleware, browser-scoped CSRF defense
W-1/W-2 Header identity spoofing via proxiesFixedTrusted proxy resolution centralized and required for forwarded identity trust
W-3 SSRF on remote endpointsFixed (scoped)SSRF validation blocks private/link-local targets for servers registered at runtime (REST API or discovery), and re-checks at connect time. Not covered: a remote server declared in config.yaml, which never reaches the registration handler (before 2.5.1 the connect-time re-check was also lost after a restart and on a follower replica; 2.5.1 carries it on the stored record). See cookbook 23
W-4 Unbounded suspended-session cacheFixedTTL-bounded cache with max size
W-5 JWT algorithm confusionFixedMixed symmetric/asymmetric algorithm families rejected
A-5 Core importing enterprise directlyFixedServer bootstrap/router path moved behind single core enterprise boundary
A-7 Divergent HTTP/WS auth middlewareFixedCore shared auth middleware path now handles both

Recommendations

ItemStatusNotes
API key hash-only storagePassRaw keys are not persisted
JWT algorithm-family validationPassMixed HS*/RS*/ES*/PS* families rejected
Trusted proxy validationPassOnly configured proxies may influence forwarded source identity
WebSocket origin validationPassPerformed before accept
Shared auth logic across protocolsPassOne core implementation reduces drift
Core/enterprise import boundaryPassCentralized in server/bootstrap/enterprise.py
Repo-wide Ruff cleanlinessFollow-upuv run ruff check src/ tests/ still reports historical unrelated test lint debt
Manual exploit verificationPassReplay confirmed K-1 returns 401, K-2 closes with 1008, K-3 aborts non-loopback startup with SystemExit(1), and K-4 rejects hostile hosts with 400 under strict CORS
TLS / mTLS at deployment edgeManualMust be enforced by deployment topology / reverse proxy

Verification Evidence

  • uv run pytest tests/ -x -q -- pass
  • uv run mypy src/ -- pass
  • Manual exploit replay of K-1..K-4 -- pass (/api/mcp_servers/{id}/l7_policy spoof returns 401, unauthenticated WebSocket closes 1008, non-loopback no-auth HTTP exits 1, hostile Host rejected with 400)
  • Focused security and boundary suites cover:
    • tests/security/test_critical.py
    • tests/security/test_identity_network.py
    • tests/unit/test_bootstrap_enterprise_boundary.py
    • tests/unit/test_bootstrap_enterprise_loading.py
    • tests/unit/test_api_auth_enforcement.py
    • tests/unit/test_agent_policy.py
    • tests/unit/test_ws_auth.py

Open Items

  • Repo-wide Ruff debt in historical tests remains outside the scope of this hardening pass.
  • If the optional-integrations module is split into a separately installed distribution later, the fallback loader can be retired in favor of entry-point-only discovery.