gateway: move verifiedProxyTo's 42-line rationale comment to docs/gateway.md

The comment-block lint (added in 79dc8ca6) now trips on this block —
genuinely pre-existing, unrelated to that change, just newly caught.
Per the lint's own suggested remedy: relocated the full per-directive
reasoning plus both footguns (session-cache keying, the Host-header
clobber that can recurse a subrequest into itself) to a new
"Dialing another vhost by name" section in docs/gateway.md, and left
a short why + pointer comment in the source. No behavior change.
This commit is contained in:
iris 2026-08-28 10:48:26 +02:00
commit c4a573d91d
2 changed files with 52 additions and 41 deletions

View file

@ -599,3 +599,47 @@ Since the gateway always terminates TLS (see [TLS modes](#tls-modes)
above), an enabled HSTS header is always served over https — there is no
TLS-less mode that could violate it.
## Dialing another vhost by name (`verifiedProxyTo`)
`vhost-lib.nix`'s `verifiedProxyTo` builds the `proxy_ssl_*` /
`proxy_set_header` block a module uses to dial another service on this
same gateway BY NAME over https, verified. One definition rather than a
copy per module: nginx verifies nothing by default
(`proxy_ssl_verify` is off), so a `proxy_pass https://…` without these
lines is encrypted and unauthenticated. That failure is invisible — it
works, and keeps working, against any certificate at all.
Every line earns its place, each confirmed against a real nginx with
the opposite arm run as a control:
- `verify` + `depth` — the chain is leaf -> intermediate -> root.
- `trusted_cert` — the bundle; nginx reads ALL certs in the file, which
the bundle's own doc warns is not true of every consumer.
- `ssl_name` — checks the HOSTNAME too. Without it a chain-only check
accepts any certificate this CA ever signed, and for an internal CA
that is every service on the hive.
- `server_name on` — sends SNI, or the far end cannot pick a cert.
**⚠️ Session-cache footgun**: `proxy_ssl_session_reuse` is left at its
default (on), deliberately — this is used on per-request auth
subrequests, so the handshake it avoids is paid on every request. Worth
knowing when testing though: the session cache is keyed by upstream
address and NOT by trust config, so two locations pointing at one
upstream with different trust do not verify independently.
**⚠️ Host-header clobber footgun**: `verifiedProxyTo` also pins `Host`
(and reinstates the rest of nginx's `recommendedProxySettings` header
set) to the target `name` rather than leaving it to be filled in later.
`name` here resolves back to THIS gateway — every consumer dials another
vhost on the same nginx, not a separate host — and nginx picks the vhost
to answer an HTTPS request from the `Host` header, not from the TLS SNI
that `proxy_ssl_name` sends. `recommendedProxySettings`'s own `Host
$host` (the CALLER's host, not the target) is textually appended by
nixpkgs AFTER a location's `extraConfig` — so it always wins over a
`proxy_set_header Host` written in the location body, and the subrequest
loops back into the ORIGINAL vhost instead of reaching the target,
recursing on its own `auth_request` until nginx's subrequest-depth limit
turns it into a plain 500. Every call site sets `recommendedProxySettings
= false` on the location for exactly this reason — nixpkgs' version
would still clobber this one.

View file

@ -87,47 +87,14 @@ in
vhostTls;
# Dial another service on this hive BY NAME over https, verified.
#
# One definition rather than a copy per module: nginx verifies
# nothing by default (`proxy_ssl_verify` is off), so a `proxy_pass
# https://…` without these lines is encrypted and unauthenticated.
# That failure is invisible — it works, and keeps working, against
# any certificate at all.
#
# Every line earns its place, each confirmed against a real nginx
# with the opposite arm run as a control:
# verify + depth — the chain is leaf -> intermediate -> root
# trusted_cert — the bundle; nginx reads ALL certs in the file,
# which the bundle's own doc warns is not true of
# every consumer
# ssl_name — checks the HOSTNAME too. Without it a chain-only
# check accepts any certificate this CA ever
# signed, and for an internal CA that is every
# service on the hive
# server_name on — sends SNI, or the far end cannot pick a cert
#
# ⚠️ `proxy_ssl_session_reuse` is left at its default (on) and that is
# deliberate: this is used on per-request auth subrequests, so the
# handshake it avoids is paid on every request. Worth knowing when
# testing though — the session cache is keyed by upstream address and
# NOT by trust config, so two locations pointing at one upstream with
# different trust do not verify independently.
#
# ⚠️ Also pins `Host` (and reinstates the rest of nginx's
# `recommendedProxySettings` header set) to `name` rather than leaving
# it to be filled in later. `name` here resolves back to THIS gateway
# — every consumer dials another vhost on the same nginx, not a
# separate host — and nginx picks the vhost to answer an HTTPS request
# from the `Host` header, not from the TLS SNI that `proxy_ssl_name`
# above sends. `recommendedProxySettings`'s own `Host $host` (the
# CALLER's host, not the target) is textually appended by nixpkgs
# AFTER a location's `extraConfig` — so it always wins over a
# `proxy_set_header Host` written in the location body, and the
# subrequest loops back into the ORIGINAL vhost instead of reaching
# the target, recursing on its own `auth_request` until nginx's
# subrequest-depth limit turns it into a plain 500. Every call site
# sets `recommendedProxySettings = false` on the location for exactly
# this reason — nixpkgs' version would still clobber this one.
# nginx verifies nothing by default (`proxy_ssl_verify` is off), so a
# `proxy_pass https://…` without this is encrypted but unauthenticated
# — invisibly, it works and keeps working against any certificate at
# all. Full reasoning for every directive here (plus two real
# footguns — session-cache keying, and a `Host`-header clobber that
# can recurse a subrequest into itself) is in docs/gateway.md's
# "Dialing another vhost by name" section — read it before touching
# this.
verifiedProxyTo = name: ''
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;