2026-07-23 04:13:12 +00:00
|
|
|
# The loopback LB bridge's own config — NOT the host Caddy (that one still
|
|
|
|
|
# terminates TLS for thermograph.org and proxies to 127.0.0.1:8137 exactly as
|
|
|
|
|
# before; it needs no change for the stack cutover).
|
|
|
|
|
#
|
|
|
|
|
# This Caddy runs as a PLAIN container (deploy-stack.sh manages it) because
|
|
|
|
|
# only plain containers can bind a specific host IP: Swarm port configs
|
|
|
|
|
# publish on 0.0.0.0 (routing mesh or host mode alike), which would expose
|
|
|
|
|
# the plaintext app un-fronted — hazard #6. It joins the stack's attachable
|
|
|
|
|
# overlay and proxies to the service VIPs; Swarm's VIP round-robins across
|
|
|
|
|
# however many web replicas the autoscaler is running, so this bridge never
|
|
|
|
|
# needs to know the replica count.
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
auto_https off
|
|
|
|
|
admin off
|
lb: trust the host Caddy's X-Forwarded-Proto
Every OAuth redirect URI the backend builds is currently http://, whatever
scheme the user actually arrived with.
There are two Caddy hops in front of the app: the host Caddy terminates TLS
and sets X-Forwarded-Proto: https, then proxies to 127.0.0.1:8137 over plain
HTTP; the stack LB is the second hop. Caddy preserves an incoming
X-Forwarded-* header only when the immediate peer is a trusted proxy, and
trusted_proxies was never set here -- so the LB overwrote the header with the
scheme of the connection it had just received, which is http.
accounts/oauth.py:_redirect_uri builds the callback from x-forwarded-proto,
and that URI must match what is registered in the provider's console.
Measured on vps2, same request to each hop:
direct to web, XFP=https -> https://thermograph.org/api/v2/...
through the LB, XFP=https -> http://thermograph.org/api/v2/...
through the PATCHED LB -> https://thermograph.org/api/v2/...
through the PATCHED LB, no XFP-> http://thermograph.org/api/v2/...
The last line is the point of using trusted_proxies rather than hardcoding
`header_up X-Forwarded-Proto https`: the LB still reports the truth when
nothing in front of it claims otherwise.
private_ranges covers 127.0.0.1/8 and the Docker bridge ranges, which is where
the host Caddy reaches this container from. The LB binds loopback only,
precisely so it cannot be reached un-fronted, so the only party that can set
these headers is the host Caddy -- trusting the local peer widens nothing.
Applied to beta's LB too. Beta is where a provider change gets tested before
it reaches thermograph.org, so it needs the same behaviour or the test is not
a test.
2026-08-01 16:06:11 +00:00
|
|
|
|
|
|
|
|
# WITHOUT THIS, EVERY OAUTH REDIRECT URI THE BACKEND BUILDS IS http://.
|
|
|
|
|
#
|
|
|
|
|
# There are two Caddy hops in front of the app: the HOST Caddy terminates TLS
|
|
|
|
|
# and sets X-Forwarded-Proto: https, then proxies to 127.0.0.1:8137 over plain
|
|
|
|
|
# HTTP; this LB is the second hop. Caddy only preserves an incoming
|
|
|
|
|
# X-Forwarded-* header when the immediate peer is a TRUSTED proxy — otherwise
|
|
|
|
|
# it overwrites the header with the scheme of the connection it just received,
|
|
|
|
|
# which here is http. So the app saw http:// no matter how the user arrived.
|
|
|
|
|
#
|
|
|
|
|
# accounts/oauth.py:_redirect_uri builds the callback from x-forwarded-proto,
|
|
|
|
|
# and that URI must match what is registered in the provider's console.
|
|
|
|
|
# Discord tolerates the http:// form; GOOGLE REJECTS a non-HTTPS redirect URI
|
|
|
|
|
# for a web client outright, so Google sign-in could not work at all until
|
|
|
|
|
# this was fixed — independently of whether the credentials were configured.
|
|
|
|
|
#
|
|
|
|
|
# Measured on vps2 before the fix, same request to each hop:
|
|
|
|
|
# direct to web, XFP=https -> https://thermograph.org/api/v2/...
|
|
|
|
|
# through this LB, XFP=https -> http://thermograph.org/api/v2/...
|
|
|
|
|
#
|
|
|
|
|
# private_ranges covers 127.0.0.1/8 and the Docker bridge ranges, which is
|
|
|
|
|
# where the host Caddy reaches this container from. Nothing outside the box
|
|
|
|
|
# can connect here — the LB is bound to loopback precisely so it cannot be
|
|
|
|
|
# reached un-fronted (hazard #6, above) — so trusting the local peer does not
|
|
|
|
|
# widen anything: the only party that can set these headers is the host Caddy.
|
|
|
|
|
servers {
|
|
|
|
|
trusted_proxies static private_ranges
|
|
|
|
|
}
|
2026-07-23 04:13:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:8137 {
|
|
|
|
|
reverse_proxy web:8137 {
|
|
|
|
|
# Fail fast to the client if the VIP has no healthy task; Swarm's own
|
|
|
|
|
# task healthchecks handle ejecting dead replicas from the VIP.
|
|
|
|
|
lb_try_duration 5s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:8080 {
|
|
|
|
|
reverse_proxy frontend:8080 {
|
|
|
|
|
lb_try_duration 5s
|
|
|
|
|
}
|
|
|
|
|
}
|