Skip to content

fix(api): warn at the FORCE_HTTPS redirect itself (TRANSPORT-001) (#3047) - #3067

Open
bdunncompany wants to merge 1 commit into
mainfrom
fix/3047-log-at-redirect
Open

fix(api): warn at the FORCE_HTTPS redirect itself (TRANSPORT-001) (#3047)#3067
bdunncompany wants to merge 1 commit into
mainfrom
fix/3047-log-at-redirect

Conversation

@bdunncompany

Copy link
Copy Markdown
Collaborator

Closes #3047. Picking this up since it was unassigned and is the other half of the outage I reported in #2987 — happy to hand it back if you'd rather take it.

The gap #2988 left

trustsForwardedHeadersFrom only warns when the peer is not trusted. The variant in this issue stays completely silent: a trusted proxy that never sends X-Forwarded-Proto. The trust gate passes, effectiveRequestScheme finds no scheme header and falls back to the internal plain-HTTP proxy→API hop, and under FORCE_HTTPS every non-health route 308s while /health (exempt) keeps returning 200 — all from a perfectly correct TRUSTED_PROXY_CIDRS.

Per your framing in the issue, the signal belongs at the redirect rather than the trust gate: one line at the point of user-visible impact instead of N warnings that each have to anticipate a different cause. This also covers the untrusted-peer and TRUST_PROXY_HEADERS=false cases #2988 already handles.

What the warning says

Enough to disambiguate the causes without reading the source:

field why
peer the immediate TCP peer, to compare against TRUSTED_PROXY_CIDRS
trustedProxy false = CIDR list doesn't cover this peer, or TRUST_PROXY_HEADERS is off
xForwardedProto (absent) with trustedProxy=true is exactly the variant no clientIp.ts warning can reach
canonicalHost distinguishes the 400 branch from the 308

It also states the symptom — fleet-wide 308s while /health stays 200 — because that combination is what makes this look like anything except a proxy misconfiguration. In the original incident every signal an operator normally checks stayed green for ~50 minutes.

Also warns on the 400 branch. An unrecognized Host is the same misconfiguration through a different lens and was equally silent.

Suppression is per peer on a 15-minute window with a bounded map, matching clientIp.ts. A redirect storm is precisely the situation here, so an unsuppressed log would flood.

Deliberately not included

Suggestion 1 (boot-time cross-check of FORCE_HTTPS against the proxy-trust settings) and suggestion 4 (a /health trusted-peer field). Both change boot or health-check semantics, which reads like your call rather than mine — happy to do either on your say-so. Suggestion 3 (release-note flag) is independent of code.

Local verification

  • security.test.ts: 31 pass, 7 new — the redirect warn, the (absent) variant, the 400 branch, per-peer suppression, health-path exemption, and FORCE_HTTPS off.
  • Confirmed the tests catch the regression: removing the call fails 4 of them.
  • Full @breeze/api suite: 1235 files, 19452 tests passed, 0 failures.
  • tsc --noEmit and eslint clean on both changed files.

Closes #3047.

#2988 put the `[proxy-trust]` warning in `trustsForwardedHeadersFrom`, which
only fires when the peer is NOT trusted. That leaves the variant this issue
describes completely silent: a *trusted* proxy that never sends
`X-Forwarded-Proto`. The trust gate passes, `effectiveRequestScheme` finds no
scheme header to honor and falls back to the internal plain-HTTP proxy->API
hop, and under FORCE_HTTPS every non-health route 308s while `/health`
(exempt) keeps returning 200 — reached from a perfectly correct
TRUSTED_PROXY_CIDRS.

Per the issue, the signal belongs at the redirect rather than the trust gate:
one line at the point of user-visible impact, instead of N warnings that each
have to anticipate a different cause. This covers the untrusted-peer and
TRUST_PROXY_HEADERS=false cases #2988 already handles as well.

The warning names what an operator needs to disambiguate the causes without
reading the source:

  - `peer` — the immediate TCP peer, so it can be compared to TRUSTED_PROXY_CIDRS
  - `trustedProxy` — false means the CIDR list does not cover this peer, or
    TRUST_PROXY_HEADERS is off
  - `xForwardedProto` — `(absent)` with `trustedProxy=true` is precisely the
    variant no `clientIp.ts` warning can reach
  - `canonicalHost` — distinguishes the 400 branch from the 308

It also states the symptom (fleet-wide 308s while /health stays 200), because
that combination is what makes the fault look like anything other than a proxy
misconfiguration.

Warned on the 400 branch too: an unrecognized Host is the same
misconfiguration seen through a different lens, and was equally silent.

Suppression is per peer on a 15-minute window with a bounded map, matching
`clientIp.ts` — a redirect storm is exactly the situation here, so an
unsuppressed log would flood.

Not addressed (carried in #3047 and left for a maintainer call): suggestion 1
(boot-time cross-check of FORCE_HTTPS against the proxy-trust settings) and
suggestion 4 (a `/health` trusted-peer field). Both change boot or health-check
semantics, which felt like your call rather than mine.

## Local verification
- `security.test.ts`: 31 pass, 7 new covering the redirect warn, the
  `(absent)` variant, the 400 branch, per-peer suppression, health-path
  exemption, and FORCE_HTTPS off
- confirmed the tests catch it: removing the call fails 4 of them
- full `@breeze/api` suite: 1235 files, 19452 tests passed, 0 failures
- `tsc --noEmit` and eslint clean
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9f64359
Status: ✅  Deploy successful!
Preview URL: https://53556990.breeze-9te.pages.dev
Branch Preview URL: https://fix-3047-log-at-redirect.breeze-9te.pages.dev

View logs

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.

[API] TRANSPORT-001: a trusted proxy that never sends X-Forwarded-Proto still 308-loops silently — log at the redirect, not only at the trust gate

1 participant