Trusted Proxies
AuthProxy almost never sees a browser. It sees an ingress controller, a load balancer, a service-mesh
sidecar, or a CDN edge — and everything it knows about the person on the other side comes from what that
thing wrote into X-Forwarded-For and X-Forwarded-Proto.
Those are ordinary request headers. Nothing about them is signed, and nothing about them is special: any caller that can open a connection to AuthProxy can send whatever it likes in them. The question a reverse proxy has to answer, before it believes a single one, is which callers are allowed to speak for someone else — and that is a fact about your network that only your deployment knows.
This page is how you tell it.
What is at stake
Section titled “What is at stake”It is tempting to file forwarded headers under “logging detail”. They are not. Two values decide a surprising amount of AuthProxy’s behavior:
| Value | What it decides |
|---|---|
| The client address | The ipAddress in every sign-in notification — the record your application shows a user as “a new sign-in from 203.0.113.7” and acts on when it looks unfamiliar. |
| The request scheme | Whether every AuthProxy session cookie carries Secure; what the OIDC post_logout_redirect_uri claims your public origin to be; and which origins the post-logout allow-list admits. |
A caller who can set the first one writes its own audit trail. A caller who can set the second one changes whether a browser is willing to send your session cookies at all — and, in the other direction, whether a genuinely encrypted session is protected as one.
Declaring the boundary
Section titled “Declaring the boundary”Name the peers directly in front of AuthProxy:
{ "Cratis": { "AuthProxy": { "Ingress": { "TrustedProxies": [ "10.0.0.0/8", "203.0.113.7" ], "ForwardLimit": 1 } } }}Or, as environment variables in a container:
Cratis__AuthProxy__Ingress__TrustedProxies__0=10.0.0.0/8Cratis__AuthProxy__Ingress__TrustedProxies__1=203.0.113.7Cratis__AuthProxy__Ingress__ForwardLimit=1Each entry is an IP address (10.0.0.7, 2001:db8::1) or a CIDR range (10.0.0.0/8, 2001:db8::/32).
A range written against a host address inside it — 10.0.0.1/8 — means the range, the same as everywhere
else. An entry AuthProxy cannot parse fails startup and names the offending value; it is never quietly
dropped, because a trusted proxy that is silently not trusted is a boundary silently in the wrong place.
The addresses to declare are the ones AuthProxy sees as the peer, not the addresses of your users. In Kubernetes that is the ingress controller’s pod CIDR; behind a cloud load balancer it is the balancer’s subnet; behind a CDN it is the CDN’s published egress ranges.
ForwardLimit decides which address is reported as the client
Section titled “ForwardLimit decides which address is reported as the client”This is the part worth reading twice, because it is the setting most often left at whatever the sample had.
X-Forwarded-For grows left to right: the outermost proxy appends the address it accepted the connection
from, then the next one appends, and so on. AuthProxy therefore reads it from the right, consuming one
entry per hop, and ForwardLimit is how many hops it consumes. Whatever it lands on becomes the client
address — the one recorded against a sign-in.
Consider a request that reaches AuthProxy carrying X-Forwarded-For: 198.51.100.9, 203.0.113.30 from a
peer at 203.0.113.10:
ForwardLimit | Reported client address | What that means |
|---|---|---|
1 (the default) | 203.0.113.30 | One hop consumed. Correct when a single ingress sits in front — but if there are really two, you are recording your own inner proxy as the client, identically for every user. |
2 | 198.51.100.9 | Two hops consumed. Correct when a CDN sits in front of a load balancer. |
3 | 198.51.100.9 | The chain ran out first. The surplus can never be reached, which is exactly the protection. |
So set it to the number of hops your deployment actually has:
- Too low and the reported address is your own infrastructure — the audit trail is real but useless.
- Too high and the reported address is whatever the outermost caller chose to write — the audit trail is attacker-controlled, which is worse than useless.
Every hop counted must itself be a trusted peer. AuthProxy re-checks at each step, so raising ForwardLimit
without also declaring the intermediate addresses in TrustedProxies changes nothing.
Mode decides how the trusted set is arrived at. Configured is the default and is what almost every
deployment wants.
{ "Cratis": { "AuthProxy": { "Ingress": { "Mode": "LoopbackOnly" } } }}| Mode | Trusts | Use it when |
|---|---|---|
Configured | Exactly the peers in TrustedProxies | Normal deployments. This is the default. |
LoopbackOnly | Only a caller on the loopback interface | A sidecar, or local development. A deployment behind an ingress never sees loopback as the peer, so this refuses every forwarded header it receives. |
TrustAny | Every caller | Nothing but your own ingress can reach AuthProxy at all — a private network with no other route in, or a listener with no peer address at all (see below). |
A listener with no peer address
Section titled “A listener with no peer address”Configured and LoopbackOnly both decide by looking at the peer’s IP address. A Unix domain socket does
not have one — RemoteIpAddress is null — so every request over such a listener is treated as coming from
an untrusted peer. Sessions keep working, which is what makes this quiet: the visible symptom is that
X-Forwarded-Proto stops being honored, so cookies lose Secure and the public origin reverts to the
transport scheme.
If AuthProxy listens on a Unix socket, and the only thing that can write to that socket is your own
ingress, say TrustAny. That is what “the peer is trustworthy and cannot be identified by address” looks
like when it is stated rather than stumbled into.
The compatibility fallback, and the warning
Section titled “The compatibility fallback, and the warning”Configured with an empty TrustedProxies keeps the behavior AuthProxy has always had: every caller’s
forwarded headers are believed. Upgrading breaks nothing.
It is not, however, silent. At startup AuthProxy logs a warning naming the mode it is running in and the configuration key that leaves it:
warn: AuthProxy is running in Configured trusted-proxy mode with no trusted proxies configured, so it believes the X-Forwarded-For and X-Forwarded-Proto headers of every caller. Set Cratis:AuthProxy:Ingress:TrustedProxies to the addresses or CIDR ranges of the ingress in front of it, or set Cratis:AuthProxy:Ingress:Mode to LoopbackOnly or TrustAny to state the choice explicitly. A future major release will refuse to start in this state.If you truly do run somewhere nothing else can reach AuthProxy, say TrustAny rather than leaving the list
empty. The behavior is identical; the difference is that one of them is a decision somebody made and the
other is an omission nobody noticed.
What a declared boundary changes
Section titled “What a declared boundary changes”Once TrustedProxies is set, a request from a peer outside it is treated as what it is — a caller talking
directly to AuthProxy:
X-Forwarded-Foris ignored; the connection’s real address is the client address.X-Forwarded-Protois ignored; the real transport scheme decides cookieSecureand the public origin.- The geo headers a fronting CDN adds —
CF-IPCountry,CF-Region,CF-IPCity, and the conventionalX-Geo-*/X-AppEngine-*city, region and country headers — resolve to an emptylocationin the sign-in notification, because from an untrusted caller they are values it chose rather than facts about where it is.
A request from a peer inside the boundary is honored exactly as before, for as many hops as
ForwardLimit allows.
Two headers are never honored, from any peer: X-Forwarded-Host and X-Forwarded-Prefix. AuthProxy
consumes only the address and the scheme, so neither the request host nor the path base can be moved by a
header. The RFC 7239 Forwarded header is likewise not consumed.
Configuring it from Aspire
Section titled “Configuring it from Aspire”The Aspire hosting integration writes the same configuration keys:
var authProxy = builder.AddAuthProxy("authproxy") .WithTrustedProxies("10.0.0.0/8", "203.0.113.7") .WithForwardLimit(2);WithTrustedProxies appends, so the peers can be declared wherever each one is known. An entry that is
neither an address nor a CIDR range is refused when the app host builds, rather than being carried to the
proxy and refused there.
Checking your work
Section titled “Checking your work”The address AuthProxy settled on is the one it reports in the sign-in notification, so the quickest
end-to-end check is to sign in and look at what your SignIn:NotifyUrl endpoint received:
ipAddressshould be the address of the machine you signed in from — not an address inside your cluster.locationshould be populated if, and only if, a fronting CDN is adding geo headers.
If ipAddress is one of your own proxies, raise ForwardLimit by one and add that proxy’s address to
TrustedProxies. If it is an address you can influence from outside, ForwardLimit is too high.