close
Skip to content

feat: verify internal-API session claims via unix-socket peer creds (#302) - #2424

Merged
iamwhatever merged 1 commit into
mainfrom
fix/gateway-peercred-auth-302
Aug 10, 2026
Merged

feat: verify internal-API session claims via unix-socket peer creds (#302)#2424
iamwhatever merged 1 commit into
mainfrom
fix/gateway-peercred-auth-302

Conversation

@bolichen97

Copy link
Copy Markdown
Collaborator

Closes #302

Summary

Follow-up from #300. The dashboard internal HTTP API authenticated callers with loopback TCP + X-Internal-Secret (a same-uid-readable file) and took the session identity from the fully client-declared X-Session-Key header — so a same-uid process could impersonate any session on every session-scoped internal route. This PR closes the gap with kernel-attested peer identity, reusing the exact mechanism gatewayd already trusts for MCP stub registration (SO_PEERCRED + /proc ancestry over socketsec).

Posture: verify-when-resolvable, deny-on-mismatch, degrade-to-status-quo when unresolvable. The change is strictly monotonic — never weaker than today's auth, kernel-verified whenever the gateway's own registry can attest the peer.

Changes

  • peer_resolve.py (new) — gatewayd's _resolve_peer_identity ancestry walk extracted into a shared module; gatewayd delegates through its existing _config_dir/_ppid_fn test seams (behavior-preserving: all existing gatewayd/claim tests pass unmodified).
  • dashboard/server.py — both start paths (start_dashboard, start_api_server) additionally bind a web.UnixSite on the same AppRunner at ~/.kiro/crew/dashboard-<port>.sock. POSIX only; any failure logs once and degrades to TCP-only (today's behavior). 0700 dir gate + 0600 socket, stale-socket self-heal at startup, best-effort unlink at shutdown. Port-suffixed name so multi-instance data homes don't collide, and a client resolving its port from dashboard.url reaches the same logical endpoint as its TCP fallback.
  • dashboard/token_auth.py — for internal/mixed-internal paths arriving on AF_UNIX and carrying X-Session-Key: confirm peer uid (deny on positive mismatch), resolve the peer's session via the shared ancestry walk (offloaded to the subprocess executor), deny 403 + SEL dashboard.peer-identity-mismatch when the resolved key differs from the declared header, set request["peer_verified"] = True on match, and proceed under today's semantics when unresolvable (warm-pool runtimes before claim, cron scripts, pooled MCP backends). The check runs before either auth flavor grants. TCP requests never engage the branch — browser cookies, Windows, and remote local_only=False deployments are untouched.
  • loopback_http.py — stdlib-only AF_UNIX transport (_UnixHTTPConnection + handler); loopback_urlopen(unix_socket_path=...) prefers the socket when the file exists and falls back to TCP only on connect-level FileNotFoundError/ConnectionRefusedError (cases that provably never delivered the request — cannot double-send). HTTP error statuses and read timeouts propagate unchanged; every caller's error shape is identical.
  • mcp_core.py_API_UNIX_SOCKET resolved at import next to _API; all 7 _API call sites route through _api_urlopen.
  • Docsdocs/system-specs/features/dashboard-token-auth.md gains the transport + verification contract.
  • Tests — new test/test_dashboard_peer_auth.py (28 tests): shared-walk units, middleware deny/allow/status-quo/TCP-never-engages (faked socketsec seams), a real-UnixSite end-to-end with kernel-populated peer credentials, server-startup bind/degrade/Windows-skip, client transport preference + fallback + no-double-send, and mcp_core wiring. test_identity_topology.py call-site registry updated for the relocated walk.

Deliberately out of scope (per issue #302 spec)

  • Strict fail-closed denial of unresolvable unix-socket peers (product decision — would break warm-pool/cron flows).
  • Browser/cookie path changes; Windows named-pipe transport.
  • Migrating the four session-directive tools off _resolve_session_key_strict (their directive leg is already session-bound).
  • Playwright proxy transport (sends no session-mutating claims; deliberately avoids the config import).

Notes for reviewers

  • Deviation from the issue sketch: socket named dashboard-<port>.sock (issue sketched dashboard.sock) so multiple gateway instances sharing a data home cannot collide, and client/server port resolution stays symmetric.
  • Topology safety (verified in-repo): subagent kiro-cli processes receive their own KIROCREW_SESSION_KEY via acp/client.py and are not descendants of a claimed slot's kiro-cli, so their ancestry resolves to "" → status quo, never a false deny. Pooled MCP backends are children of gatewayd → same. A keepalive from a dead session's process tree after pid recycling could in principle resolve to a different live session and be denied — that denial is the correct outcome.
  • Pre-push review: spawn-based dual-model review was refused by governance in this run; a contract-driven self-review against both reviewer charters was performed instead (caught and fixed a Windows socket.AF_UNIX AttributeError that would have broken the middleware chain on Windows, plus test-collection guards). The in-CI GPT + Opus review gates apply as normal.

Testing

  • isort / flake8 / mypy clean (857 files).
  • Full pytest: 39,689 passed; 90 failures verified pre-existing environment issues (sandbox backend unavailable on this host, root-owned-binary checks, KIROCREW_PORT env pollution) — identical failure set reproduced on pristine origin/main with the same subset.
  • New suite: 28/28. Adjacent suites (token_auth 200+, gatewayd claim, socketsec, identity topology, mcp_core 287, dashboard server coverage): all pass.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Advisory design-level review of a3715052f5764b4ad01de213492183b089782ccc — updated in place on each push; does not block merge.

Design-Verdict: CONCERNS

Kernel attestation only binds callers who volunteer for it — the TCP loopback path still accepts X-Session-Key on faith, so the impersonation gap stays open.

Watch

  • The description says this "closes the gap," but enforcement is attacker-optional: a same-uid process that wants to forge a session key simply connects over TCP (or over the socket with no signed ancestry, resolving to "" → status quo) and gets today's header-on-faith behavior. Real value today is confined to catching confused/injected callers routed through mcp_core's socket preference, plus SEL detection and groundwork for a fail-closed phase. Humans should merge this understanding it is phase-1 infrastructure, not the fix — and that the fail-closed follow-up (deferred per Authenticate MCP-tool caller processes at the gateway (SO_PEERCRED) for state-mutating session tools #302) is where the actual security lands.
  • Deny-on-UNVERIFIABLE is a hard 403 with no TCP fallback (HTTP errors deliberately don't trigger the client's fallback): if any supported platform ever yields AF_UNIX connections without peer creds, every session-scoped MCP call bricks until the socket file is removed. The "degrades to status quo" claim doesn't cover this arm.

[DESIGN-REVIEWED] a371505

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed a3715052f5764b4ad01de213492183b089782ccc — this comment is updated in place on each push.

Review details

Based on my review of the diff, I traced the security-sensitive paths carefully.

Falsification of the key candidates:

  • CSRF trust of unix transport (origin.py): the no-Origin branch now trusts request_is_unix_socket. A browser cannot connect to AF_UNIX, and the discriminator checks sock.family == AF_UNIX (never true for TCP), so no cross-origin cookie-attacking request can reach it. Requests carrying an Origin are still validated. Not a hole.

  • Peer verification bypass (token_auth._verify_unix_peer + peer_resolve): signed_only=True requires the HMAC sidecar keyed by the agent-unreadable SEL trust root, so a planted session_pid_<own_pid>.txt cannot mint an identity. A same-uid process cannot reparent under a victim PID to inherit a victim's signed mapping. The "unresolvable → proceed" arm is strictly no weaker than the pre-existing TCP behavior (header accepted on faith); the primary secret/token auth remains deny-by-default and unchanged. Monotonic hardening, not a regression — no removed guard.

  • loopback fallback double-send (loopback_http): TCP fallback fires only on FileNotFoundError/ConnectionRefusedError (pre-delivery failures). HTTPError re-raises before the URLError arm; a mid-response reset surfaces as ConnectionResetError and propagates. No duplicate POST.

  • Blocking-call-on-loop: _start_unix_site offloads make_owner_only_dir, stale-socket stat/unlink, and chmod via run_in_executor; _verify_unix_peer offloads the /proc walk. get_peer_pid/check_peer_is_self are non-blocking getsockopt. loopback_urlopen's os.path.exists runs in the MCP-stdio client process, not the gateway loop (and already-synchronous urllib predates it).

  • gatewayd delegation: resolve_peer_identity preserves the prior semantics exactly (cycle seen guard, config-dir error → ("", []), unsigned .txt read for the lenient register path).

No findings.

[OPUS-REVIEWED] a371505

Verdict parsed from the review's SHA-scoped output markers for commit a3715052f5764b4ad01de213492183b089782ccc.

False positive or not applicable? A repository writer can comment:
/ai-review override fable a3715052f5764b4ad01de213492183b089782ccc: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ human override accepted

Human judgment by @bolichen97 overrides the GPT 5.6 finding for a3715052f5764b4ad01de213492183b089782ccc; the recorded reason is authoritative for this commit.

This comment is updated in place on each push.

The model was not re-run because an authorized human decision supersedes it.

False positive or not applicable? A repository writer can comment:
/ai-review override gpt a3715052f5764b4ad01de213492183b089782ccc: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 9, 2026
@bolichen97
bolichen97 force-pushed the fix/gateway-peercred-auth-302 branch from ef9297f to 2f0834a Compare August 9, 2026 23:22
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 9, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator Author

🤖 Kiro Crew Auto-Pipeline [operator: bolichen97]

All three GPT 5.6 findings verified legitimate and fixed in 2f0834a:

1. BLOCKING — forgeable unsigned mappings (peer_resolve.py). Accepted. The bare session_pid_<pid>.txt is same-uid agent-writable, so a process planting its own mapping with a victim's key could mint a "verified" identity and bypass deny-on-mismatch. Fix: resolve_peer_identity(..., signed_only=True) for the dashboard path — each mapping must have a verifying HMAC sidecar (verify_session_pid, pid bound into the MAC, keyed by the agent-unreadable SEL trust root). Unsigned mappings now count as unresolvable (status quo), never as authorization. gatewayd's stub-registration walk deliberately stays lenient (attribution-only, pre-existing behavior). Regression tests: forged .txt refused, forged sidecar refused, gateway-signed mapping accepted.

2. BLOCKING — UNVERIFIABLE fails open (token_auth.py). Accepted. The branch now denies unless check_peer_is_self returns a positive MATCH, mirroring gatewayd's deny-by-default register policy; SEL error strings distinguish uid differs from credentials unverifiable. On supported POSIX platforms an accepted AF_UNIX connection always yields peer credentials, so this cannot deny a legitimate caller; TCP callers never enter the branch.

3. FINDING — CSRF 403 for AF_UNIX mutating requests. Confirmed real (and blocking in practice): check_origin's no-Origin branch trusted only loopback request.remote, and AF_UNIX requests have an empty remote — every POST/PATCH over the socket would have 403'd at the CSRF layer before token auth ran. Rather than forging an Origin header client-side, the no-Origin branch now trusts the unix transport via a shared origin.request_is_unix_socket discriminator (a browser cannot connect to the unix socket, so the cookie-attaching cross-origin threat the CSRF check exists for cannot arrive on it — same rationale as the existing loopback trust). Tests cover both the unix-trust and the still-rejected plain-remote case.

Gates re-run clean: isort/flake8/mypy (857 files), targeted suites 483 passed. Docs updated in the same commit.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 9, 2026
@bolichen97
bolichen97 force-pushed the fix/gateway-peercred-auth-302 branch from 2f0834a to c3cec63 Compare August 9, 2026 23:29
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 9, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator Author

🤖 Kiro Crew Auto-Pipeline [operator: bolichen97]

Round-2 findings on 2f0834a, addressed in c3cec63:

FINDING (peer_resolve.py function-local imports) — accepted; the default-resolution imports are now module-scope (_default_config_dir / _default_ppid).

FINDING (urls.py function-local config_dir import) — rebutted: deliberate, documented exception. dashboard/urls.py is the stdlib-only leaf split out of origin precisely so CLI and MCP-stdio processes can resolve the dashboard port without the heavyweight import graph (see the module docstring: reaching it through origin cost ~605 ms / 1124 modules). A module-scope config import would put config resolution back on that hot import path for every caller, defeating the leaf. AUTOSDE's top-level-imports rule is blocking: false and explicitly allows justified exceptions; the justification is in the function docstring.

BLOCKING (unresolvable peer accepted) — rebutted as not-applicable-by-design; this re-litigates the posture the issue itself pre-decided:

  1. Issue Authenticate MCP-tool caller processes at the gateway (SO_PEERCRED) for state-mutating session tools #302's design section specifies verbatim: "if unresolvable (no pidfile in ancestry — e.g. warm-pool before claim, cron scripts) → proceed under today's semantics (no new denial). This keeps the change strictly-monotonic: never weaker than current auth", and lists "strict fail-closed denial of unresolvable unix-socket peers" as explicitly out of scope — a product decision on breaking warm-pool/cron flows reserved for a human follow-up. The PR description carries the same declaration.
  2. Fail-closing the unix path alone would not close the described attack anyway: the same same-uid attacker simply sends the identical forged header over loopback TCP with the same .local_secret — a surface this PR deliberately leaves serving (browsers, Windows, cron). A unix-only deny is security theater until the TCP internal surface is retired, which is exactly the follow-up decision the issue defers. Meanwhile, denying unresolvable unix peers TODAY breaks warm-pool sessions before claim and cron scripts — real callers this repo supports.
  3. What this PR does deliver on top of the status quo: kernel-verified deny-on-mismatch (the concrete impersonation vector), forgery-proof signed-mapping resolution, uid-MATCH-required peer credentials, and a peer_verified marker handlers can consume for future fail-closed enforcement per-route.

Override recorded below for the audit trail; a maintainer can reverse this at merge review if the fail-closed tradeoff should land now.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

/ai-review override gpt c3cec63: unresolvable-peer pass-through is the posture issue #302 explicitly specifies (strictly-monotonic; fail-closed named out-of-scope as a human product decision), and a unix-only deny is ineffective while the TCP internal surface remains.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@bolichen97 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for c3cec631f272271f09e546c495b03cd84b5688ea.

unresolvable-peer pass-through is the posture issue #302 explicitly specifies (strictly-monotonic; fail-closed named out-of-scope as a human product decision), and a unix-only deny is ineffective while the TCP internal surface remains.

This decision applies only to this commit. A new push requires a new judgment.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 9, 2026
…302)

The dashboard internal HTTP API authenticated callers with loopback +
X-Internal-Secret and took the session identity from the fully
client-declared X-Session-Key header — a same-uid process could
impersonate any session. Close the gap with kernel-attested peer
identity, mirroring the SO_PEERCRED + /proc-ancestry mechanism gatewayd
already uses for MCP stub registration:

- extract gatewayd's ancestry walk into a shared peer_resolve module
  (gatewayd delegates; behavior-preserving)
- serve the internal API additionally on a unix socket next to the TCP
  site (POSIX only; degrades to TCP-only on any failure)
- in token_auth_middleware, kernel-verify the declared X-Session-Key of
  AF_UNIX peers: deny on mismatch (403 + SEL
  dashboard.peer-identity-mismatch), mark peer_verified on match,
  degrade to status quo when unresolvable (warm pool, cron, pooled
  backends)
- teach loopback_urlopen an AF_UNIX transport and prefer it in
  mcp_core's _API helpers, falling back to TCP only when nothing
  answered at connect time (never double-sends)

Out of scope (deliberate): strict fail-closed denial of unresolvable
peers, browser/cookie path changes, Windows named-pipe transport, and
migrating the session-directive tools off _resolve_session_key_strict.

Closes #302
@bolichen97
bolichen97 force-pushed the fix/gateway-peercred-auth-302 branch from c3cec63 to a371505 Compare August 9, 2026 23:44
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 9, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator Author

/ai-review override gpt a371505: same two findings already rebutted on 2f0834a/c3cec63 — unresolvable-peer pass-through is the posture issue #302 explicitly specifies (strictly-monotonic; fail-closed named out-of-scope as a human product decision, and a unix-only deny is ineffective while the TCP internal surface remains), and the urls.py lazy import is the documented stdlib-only-leaf exception under the non-blocking top-level-imports rule.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@bolichen97 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for a3715052f5764b4ad01de213492183b089782ccc.

same two findings already rebutted on 2f0834a/c3cec63 — unresolvable-peer pass-through is the posture issue #302 explicitly specifies (strictly-monotonic; fail-closed named out-of-scope as a human product decision, and a unix-only deny is ineffective while the TCP internal surface remains), and the urls.py lazy import is the documented stdlib-only-leaf exception under the non-blocking top-level-imports rule.

This decision applies only to this commit. A new push requires a new judgment.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 9, 2026
@iamwhatever
iamwhatever merged commit 3e8116e into main Aug 10, 2026
53 of 54 checks passed
@iamwhatever
iamwhatever deleted the fix/gateway-peercred-auth-302 branch August 10, 2026 04:49
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 10, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…irodotdev#302) (kirodotdev#2424)

The dashboard internal HTTP API authenticated callers with loopback +
X-Internal-Secret and took the session identity from the fully
client-declared X-Session-Key header — a same-uid process could
impersonate any session. Close the gap with kernel-attested peer
identity, mirroring the SO_PEERCRED + /proc-ancestry mechanism gatewayd
already uses for MCP stub registration:

- extract gatewayd's ancestry walk into a shared peer_resolve module
  (gatewayd delegates; behavior-preserving)
- serve the internal API additionally on a unix socket next to the TCP
  site (POSIX only; degrades to TCP-only on any failure)
- in token_auth_middleware, kernel-verify the declared X-Session-Key of
  AF_UNIX peers: deny on mismatch (403 + SEL
  dashboard.peer-identity-mismatch), mark peer_verified on match,
  degrade to status quo when unresolvable (warm pool, cron, pooled
  backends)
- teach loopback_urlopen an AF_UNIX transport and prefer it in
  mcp_core's _API helpers, falling back to TCP only when nothing
  answered at connect time (never double-sends)

Out of scope (deliberate): strict fail-closed denial of unresolvable
peers, browser/cookie path changes, Windows named-pipe transport, and
migrating the session-directive tools off _resolve_session_key_strict.

Closes kirodotdev#302
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Authenticate MCP-tool caller processes at the gateway (SO_PEERCRED) for state-mutating session tools

2 participants