Skip to content

SRE-727: allow-list proxied Kratos endpoints - #9243

Open
claude[bot] wants to merge 2 commits into
mainfrom
claude/sre-727-kratos-endpoint-allowlist
Open

SRE-727: allow-list proxied Kratos endpoints#9243
claude[bot] wants to merge 2 commits into
mainfrom
claude/sre-727-kratos-endpoint-allowlist

Conversation

@claude

@claude claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Requested by Ciaran Morinan · Slack thread

🌟 What is the purpose of this PR?

Before. The Node API proxied everything under /auth straight through to Kratos's public API, with no filtering on path or method. Every endpoint that port serves was reachable on our own origin — including the native/API-client flow variants that mint bearer session tokens, session enumeration and revocation, the error store, FedCM, and Kratos's health and version endpoints, none of which the frontend uses. Because the rule was "forward the whole prefix", any endpoint a future Kratos release adds would become publicly reachable the moment the image was bumped, with no code change and nothing to review.

After. One auditable constant lists the 19 endpoints the frontend actually uses — the login, registration, recovery, verification and settings flows, logout, /sessions/whoami, and the OIDC callback — each annotated with the frontend call site that needs it. Requests match on path and method, against a canonicalised path that rejects percent-escapes, backslashes and traversal segments outright, so there is no gap between what is checked and what is forwarded. Anything else gets a 404 and never reaches Kratos. Query strings, which carry the flow ids, are untouched. Denials are counted and logged with method and path — never the query string, which carries verification and recovery codes — so an endpoint that should have been listed shows up as a log line rather than a silent auth-flow breakage.

This is hardening follow-up to INC-27 (Kratos email verification bypass), and it brings Kratos in line with the Hydra proxy, which is already selectively mounted.

🔗 Related links

  • INC-27 — Kratos email verification bypass, the incident this hardens against (internal)
  • SRE-727 — the tracking ticket (internal)

🚫 Blocked by

  • Nothing

🔍 What does this change?

  • Adds a default-deny allow-list module for the Kratos proxy: the 19 permitted path/method pairs, path canonicalisation, the predicate, and the Express guard that returns a 404 (not a 403, which would confirm an endpoint exists).
  • Registers that guard in the /auth middleware chain, last before the proxy — so cors has already answered any OPTIONS preflight, and probes for unlisted endpoints are still rate limited.

Two entries a reviewer should check specifically:

  • GET /self-service/logout — Ory's updateLogoutFlow is a GET despite being the state-changing half, so method-matching it as POST would silently break logout.
  • /self-service/methods/oidc/callback/:provider — the only entry with no frontend caller. The IdP redirects the browser there, and Kratos is configured to advertise this proxy as its OIDC redirect base, so denying it would break SSO on return from the provider. POST is allowed alongside GET for form_post-mode providers.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

Behaviour changes worth knowing:

  • HEAD is now denied everywhere, including on /sessions/whoami. No frontend code uses it.
  • An OIDC provider id outside [a-z0-9_-] would 404, which is not the case today.
  • A future Kratos version that moves an endpoint we depend on will now fail closed rather than silently work. This is intended, and diagnosable from the denial log.

🐾 Next steps

  • Any future Kratos upgrade should include a pass over the allow-list, since a moved or renamed endpoint now fails closed.

🛡 What tests cover this?

  • 78 new unit tests covering the allow-list, path canonicalisation and the guard.
  • 6 end-to-end tests against real Express and cors, verifying that the mount strips /auth, that query strings survive, that the OPTIONS preflight is answered before the guard, and that unlisted paths and method mismatches never reach the proxy. These were validation-only and are not included in the diff.
  • Typecheck, eslint (--report-unused-disable-directives) and formatting all clean, with no new findings on the modified index.ts relative to its state on main.

❓ How to test this?

  1. Check out the branch and start the API.
  2. Exercise the normal auth flows — sign in, sign up, email verification, recovery, change password, logout, and SSO if configured. All should behave exactly as before.
  3. Request an unlisted endpoint under /auth (for example one of the /self-service/*/api flow variants, or /version). Confirm it returns 404 and that a warning naming the method and path appears in the API log.

📹 Demo

Not applicable — no user-visible UI change.

The /auth proxy forwarded every path under the prefix to Kratos's public
API without filtering on path or method, so everything that port serves
was reachable on our own origin, and a Kratos version bump could widen
that surface with no code change.

Add a default-deny guard in front of the proxy listing the 19 endpoints
the frontend uses, matched on path and method against a canonicalised
path that rejects percent-escapes, backslashes and traversal segments.
Anything else gets a 404 and never reaches Kratos. Denials are counted
and logged with method and path, query strings excluded.

Hardening follow-up to INC-27.
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
hash Ignored Ignored Preview Aug 19, 2026 11:11am
hashdotdesign-tokens Ignored Ignored Preview Aug 19, 2026 11:11am
petrinaut-docs Ignored Ignored Preview Aug 19, 2026 11:11am
petrinaut Skipped Skipped Aug 19, 2026 11:11am

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.64%. Comparing base (27d9fc0) to head (017093a).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9243      +/-   ##
==========================================
+ Coverage   59.63%   59.64%   +0.01%     
==========================================
  Files        1420     1421       +1     
  Lines      138767   138805      +38     
  Branches     6555     6565      +10     
==========================================
+ Hits        82753    82791      +38     
  Misses      54950    54950              
  Partials     1064     1064              
Flag Coverage Δ
apps.hash-api 14.50% <100.00%> (+0.50%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude
claude Bot marked this pull request as ready for review August 19, 2026 10:22
@cursor

cursor Bot commented Aug 19, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes authentication proxy behavior: mis-listed or renamed Kratos routes would fail closed and break login/SSO until the allow-list is updated; incorrect hardening could also block legitimate flows.

Overview
Replaces open /auth → Kratos forwarding with a default-deny allow-list of the browser auth surface the frontend uses (self-service flows, whoami, logout, OIDC callback), each rule scoped to GET or POST only.

Adds kratos-endpoint-allowlist with canonicaliseKratosProxyPath (rejects percent-encoding, traversal, and non-canonical paths so checks match what is forwarded), isAllowedKratosProxyRequest, and guardKratosProxy — denied requests get 404 (not 403), warn logs with method and sanitized path (no query string), and the proxy is never invoked (no connection to Kratos).

index.ts wraps the existing createProxyMiddleware Kratos handler in guardKratosProxy and documents that the guard runs after rate limiters and cors so OPTIONS preflights and probes stay rate-limited.

Extensive Vitest coverage for allow/deny cases, canonicalisation, guard behavior, and an Express + real proxy stack proving blocked traffic never hits the stub Kratos server.

Reviewed by Cursor Bugbot for commit 017093a. Bugbot is set up for automated code reviews on this repo. Configure here.

@claude
claude Bot requested a review from CiaranMn August 19, 2026 10:22
Address review feedback on the Kratos endpoint allow-list.

The guard is no longer a separate middleware in the `/auth` chain. It now
wraps the proxy, so `index.ts` mounts a single `kratosProxy` and the check
sits with the proxy it guards.

The check stays ahead of the proxy rather than moving into its `proxyReq`
hook. That hook is emitted from the outgoing request's `socket` event, so by
the time it runs a connection to Kratos is already open, and it fires on a
later tick than the proxy's own `try`/`catch` — a throw there becomes an
uncaught exception rather than a 404.

Also trim the allow-list comments: drop the file paths and line numbers,
which go stale, in favour of a short reason per endpoint.
@vercel
vercel Bot temporarily deployed to Preview – petrinaut August 19, 2026 11:11 Inactive
@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Both addressed in 017093a.

2. Comments. Dropped every file path and line number — each entry now just says why it's needed (Sign-in., Signup., Password recovery., Session check., …), and the "deliberately not listed" essay is gone. Kept the two non-obvious notes briefly: that Ory's updateLogoutFlow is a GET despite being the state-changing half, and that the OIDC callback has no frontend caller because the IdP redirects the browser to it.

1. Merging into the proxy. Done, but not via proxyReq — the guard now wraps the proxy, so index.ts mounts a single kratosProxy and the check lives with the thing it guards. proxyReq itself doesn't work for rejection, for two reasons I checked against http-proxy 1.18.1's source:

  • It's emitted from proxyReq.on('socket') in the stream pass, i.e. after http.request() has already been called and a connection to Kratos is established. Rejecting there still costs a TCP connection to Kratos per denied request — I measured 23 connections for 22 denied requests, versus 0 with the check ahead of the proxy. That's most of what the allow-list is for.
  • throw specifically can't work: the socket event fires a tick later than the try/catch around proxy.web() in http-proxy-middleware, so it surfaces as an uncaught exception and takes the process down rather than becoming a 404. proxyReq.destroy() + res.sendStatus(404) does produce the right status, but every denial then routes through the proxy's error path as an ECONNRESET — error-level log noise, and a second res.end() on an already-ended response.

Wrapping gets the structural win you were after — one moving part in the chain, check adjacent to the proxy — with nothing reaching Kratos. Preflights still work, since cors remains ahead of the guard, and the CORS header is still set on the 404.

Tests: 84 pass. Added six that drive a real Express app and a real http-proxy-middleware instance against a stub Kratos, asserting a denied request returns 404 with zero inbound connections to the target; previously only the matcher was covered in isolation.


Generated by Claude Code

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

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-api Affects the HASH API (app) area/apps type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

2 participants