Skip to content

fix(auth): reject self-asserted principal headers from external callers (ARN-167) - #452

Open
rita-aga wants to merge 2 commits into
mainfrom
claude/arn-167-auth-bypass
Open

fix(auth): reject self-asserted principal headers from external callers (ARN-167)#452
rita-aga wants to merge 2 commits into
mainfrom
claude/arn-167-auth-bypass

Conversation

@rita-aga

@rita-aga rita-aga commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Closes ARN-167 (CRITICAL) — the TemperPaw side of systemic Class A, mirroring the kernel fix in temper PR #343 (ARN-170). Draft for review — do not merge.

Problem

crates/temperpaw/src/auth.rs marked ANY request carrying x-temper-principal-kind + x-temper-principal-id as PreAuthenticatedRequest and trusted the client-supplied principal — no cookie, no bearer, no origin check. The server binds 0.0.0.0, so any external caller could self-assert any principal, including admin.

Fix

An ingress edge that strips all client-asserted identity headers (x-temper-*, x-agent-id, x-tenant-id) unless the request originates from a genuine loopback peer — checked via the actual TCP peer address (ConnectInfo), never a client-supplied forwarding header. Only a loopback-origin internal call (the WASM-agent/transport path) may self-assert its principal; external callers have those headers removed before Cedar or the kernel bearer check can trust them. Missing ConnectInfo → treated as non-loopback (strip) — the safe default.

Red→green

external_self_asserted_principal_headers_are_rejected — an external request with x-temper-principal-kind: admin is no longer treated as authenticated/admin (was: PreAuthenticated). Loopback-origin internal calls still self-assert as before.

Notes

  • ADR-0066. Mirrors the kernel auth-edge design (strip client identity, derive only from a trusted origin/credential).
  • Deploy note: confirm the internal WASM-agent/transport calls actually traverse loopback in the deployed topology, and run live e2e (external spoof rejected, internal call works) before this is truly done.

🤖 Generated with Claude Code

Greptile Summary

This PR closes the Class A self-asserted-identity bypass (ARN-167) by replacing the previous loopback-gated pre-authentication branch with a credential-only boundary: the outer middleware now strips the entire x-temper-*, x-agent-id, and x-tenant-id header family from every request before any auth decision, and the removed pre-auth code path is not replaced with another network-location check.

  • auth.rs ingress strip: strip_client_identity_headers runs unconditionally before public-path bypass or credential checks; session cookies re-inject identity server-side via inject_auth_headers, bearer tokens pass to the kernel middleware for resolution, and requests with neither credential get 401.
  • paw-transport: PawApiClient.build_request no longer synthesizes admin identity headers for loopback URLs; the uses_internal_loopback helper is deleted and the api_key-based bearer path is now the sole auth mechanism.
  • setup.rs / startup.rs: All five call sites that manually set x-temper-principal-kind: admin have been cleaned up; internal HTTP helpers now authenticate through auth.apply() or the api_key bearer credential exclusively.

Confidence Score: 5/5

Safe to merge after the noted deploy-topology verification (internal WASM-agent/transport calls traverse loopback and carry bearer credentials in the deployed environment).

The credential-only boundary is implemented correctly end-to-end: the strip runs before every auth branch (including public-path bypass), session and bearer paths re-inject identity from server state, and the pre-auth header branch is fully deleted rather than patched. All four reviewer focus areas are verified by dedicated tests.

No files require special attention. The only open item is the operational verification called out in the PR description: confirming that internal WASM-agent/transport calls in the deployed topology carry a bearer token before merging to production.

Important Files Changed

Filename Overview
crates/temperpaw/src/auth.rs Core security fix: removes the header-only PreAuthenticatedRequest branch entirely and unconditionally strips all x-temper-*, x-agent-id, and x-tenant-id headers before any auth decision. New tests cover external rejection, loopback rejection, full identity-family stripping, and bearer-bound kernel identity.
crates/paw-transport/src/lib.rs Removes uses_internal_loopback helper and conditional admin-header synthesis; build_request now only adds Authorization: Bearer {key} when an api_key is configured. Tests converted from integration to unit.
crates/temperpaw/src/setup.rs Removes five manual x-temper-principal-kind: admin header additions; auth delegated entirely to auth.apply() bearer credential.
crates/temperpaw/src/startup.rs Removes x-temper-principal-kind: admin from all four OData helpers; each now relies solely on the optional api_key for bearer auth.
docs/adrs/0066-reject-self-asserted-principal-headers.md New ADR documenting the credential-only identity boundary and rejection of loopback-as-identity.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as External/Loopback Client
    participant M as auth::middleware
    participant S as Session Handler
    participant B as bearer_auth_check
    participant H as Route Handler

    C->>M: HTTP request (any headers)
    Note over M: strip_client_identity_headers()<br/>removes x-temper-*, x-agent-id, x-tenant-id

    alt Public path
        M->>H: pass through (no identity injected)
    else Valid session cookie
        M->>S: claims_from_headers()
        S-->>M: SessionClaims
        Note over M: inject_auth_headers()<br/>x-temper-principal-kind: admin<br/>x-temper-principal-id: email<br/>x-tenant-id: state.tenant()
        M->>H: PreAuthenticatedRequest extension set
    else Bearer token present
        Note over M: ensure_tenant_header(state.tenant())
        M->>B: forward with bearer
        Note over B: resolves principal from token<br/>injects kernel identity headers
        B->>H: authenticated request
    else No credential
        M-->>C: 401 UNAUTHORIZED
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as External/Loopback Client
    participant M as auth::middleware
    participant S as Session Handler
    participant B as bearer_auth_check
    participant H as Route Handler

    C->>M: HTTP request (any headers)
    Note over M: strip_client_identity_headers()<br/>removes x-temper-*, x-agent-id, x-tenant-id

    alt Public path
        M->>H: pass through (no identity injected)
    else Valid session cookie
        M->>S: claims_from_headers()
        S-->>M: SessionClaims
        Note over M: inject_auth_headers()<br/>x-temper-principal-kind: admin<br/>x-temper-principal-id: email<br/>x-tenant-id: state.tenant()
        M->>H: PreAuthenticatedRequest extension set
    else Bearer token present
        Note over M: ensure_tenant_header(state.tenant())
        M->>B: forward with bearer
        Note over B: resolves principal from token<br/>injects kernel identity headers
        B->>H: authenticated request
    else No credential
        M-->>C: 401 UNAUTHORIZED
    end
Loading

Comments Outside Diff (1)

  1. crates/temperpaw/src/auth.rs, line 174-177 (link)

    P2 Missing test for external bearer + forged identity headers — the bearer path is reached after the unconditional strip, so an external caller sending both Authorization: Bearer … and x-temper-principal-kind: admin will have the identity header stripped and the kernel will resolve the principal from the token alone. This is the correct behaviour, but there is no test that proves it. Adding a test that presents a bearer token alongside forged x-temper-* headers and verifies that those headers do not survive to downstream would complete coverage for the bearer path.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/temperpaw/src/auth.rs
    Line: 174-177
    
    Comment:
    Missing test for external bearer + forged identity headers — the bearer path is reached after the unconditional strip, so an external caller sending both `Authorization: Bearer …` and `x-temper-principal-kind: admin` will have the identity header stripped and the kernel will resolve the principal from the token alone. This is the correct behaviour, but there is no test that proves it. Adding a test that presents a bearer token alongside forged `x-temper-*` headers and verifies that those headers do not survive to downstream would complete coverage for the bearer path.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code Fix in Codex Fix in Cursor

Reviews (4): Last reviewed commit: "fix(auth): reject loopback self-asserted..." | Re-trigger Greptile

…rs (ARN-167)

TemperPaw trusted any request carrying x-temper-principal-kind/-id as a
PreAuthenticatedRequest, so any external client could self-assert admin.
Add an ingress edge that strips all client-asserted identity headers
(x-temper-*, x-agent-id, x-tenant-id) unless the request originates from a
genuine loopback peer (checked via ConnectInfo, never a forwarding header).
Only a loopback-origin internal call may self-assert its principal; external
callers have those headers removed before Cedar/bearer can trust them.

TemperPaw side of systemic Class A; kernel counterpart is ARN-170 (temper #343).
ADR-0066.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rita-aga
rita-aga marked this pull request as ready for review July 7, 2026 09:08
@rita-aga

rita-aga commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread crates/temperpaw/src/auth.rs Outdated
Comment thread crates/temperpaw/src/auth.rs Outdated
@rita-aga

Copy link
Copy Markdown
Collaborator Author

ARN-165 / ARN-167 principle audit — request changes

This blocks the simplest direct non-loopback header spoof, but it preserves the underlying identity vulnerability by treating loopback as authentication.

Blocking findings

  1. Every local process can self-assert authority. auth.rs:151-160 strips authority headers only for non-loopback requests; :185-191 marks any loopback caller with two self-selected headers as preauthenticated, and :207-213 uses peer IP as the trust proof. The caller still chooses kind, ID, tenant, scopes, delegation, and verified-agent attributes.
  2. The attack is reachable in this architecture. The pinned Temper WASM host preserves guest-supplied authority headers and can inject the internal API key. paw-codex-worker constructs caller-selected headers/admin fallback, and checked-in agent instructions tell agents to curl localhost with an admin header. A compromised module, worker, SSRF path, or prompt-driven local agent retains escalation.
  3. The ADR explicitly punts the unforgeable credential. Lines 79-88 call the root fix future hardening. Under ARN-167 and the requested no-band-aid principle, that is the acceptance requirement.
  4. Tests bypass the real listener. The new tests manually insert ConnectInfo into oneshot requests. They do not prove actual TCP behavior, bearer/cookie + forged headers, or the downstream SecurityContext.
  5. Readability regresses. auth.rs grows from roughly 934 to 1,131 lines, well beyond the 500-line project rule.

Required direction

Remove header-only loopback preauthentication. Trusted Rust callers need a scoped internal service credential, Unix-domain/in-process capability, or trusted request extension. Temper must strip guest authority headers and inject identity only from verified credentials or WasmInvocationContext; guest headers must never override it. Add real-listener external/loopback tests and downstream identity assertions, then split ingress sanitization/materialization into focused modules.

Both unresolved IPv4-mapped-IPv6 threads are duplicates. Normalize mapped addresses and test them, but note that this is currently an internal availability regression, not the root privilege bypass.

The branch is clean and targeted tests pass, but it uses a stale Temper revision. Rebase and run cross-repo/live verification only after the trust redesign.

@nerdsane

Copy link
Copy Markdown
Owner

@greptile-apps please review the latest ARN-167 head 152936c. Focus areas: loopback no longer grants pre-auth, all client identity/tenant headers are stripped before credential resolution, PawApiClient no longer synthesizes admin headers, and internal calls use bearer/session-derived identity only.

@nerdsane

Copy link
Copy Markdown
Owner

ARN-167 current-head validation for 152936c:\n\n- Re-reviewed the prior Class A blocker: loopback is no longer a trust root. Header-only loopback identity is removed; TemperPaw strips client-assertable identity/tenant headers for every peer, and internal transport calls now use bearer credentials instead of raw admin principal headers.\n- Local validation from isolated worktree:\n - cargo fmt --check\n - git diff --check origin/main...HEAD\n - cargo test -p temperpaw auth::tests -- --nocapture (11/11 auth tests passed)\n - cargo test -p paw-transport tests::paw_api_client_ -- --nocapture (3/3 request-construction tests passed)\n - cargo clippy -p temperpaw -p paw-transport --all-targets -- -D warnings\n\n@greptile-apps please review the current head 152936c with focus on: no self-asserted identity headers from any peer, loopback not used as auth, PawApiClient bearer behavior, and whether any public path can still forward x-temper-* / x-agent-id / x-tenant-id into the kernel.

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.

2 participants