Skip to content

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.


It is tempting to file forwarded headers under “logging detail”. They are not. Two values decide a surprising amount of AuthProxy’s behavior:

ValueWhat it decides
The client addressThe 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 schemeWhether 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.


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:

Terminal window
Cratis__AuthProxy__Ingress__TrustedProxies__0=10.0.0.0/8
Cratis__AuthProxy__Ingress__TrustedProxies__1=203.0.113.7
Cratis__AuthProxy__Ingress__ForwardLimit=1

Each 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:

ForwardLimitReported client addressWhat that means
1 (the default)203.0.113.30One 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.
2198.51.100.9Two hops consumed. Correct when a CDN sits in front of a load balancer.
3198.51.100.9The 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"
}
}
}
}
ModeTrustsUse it when
ConfiguredExactly the peers in TrustedProxiesNormal deployments. This is the default.
LoopbackOnlyOnly a caller on the loopback interfaceA sidecar, or local development. A deployment behind an ingress never sees loopback as the peer, so this refuses every forwarded header it receives.
TrustAnyEvery callerNothing 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).

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.


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-For is ignored; the connection’s real address is the client address.
  • X-Forwarded-Proto is ignored; the real transport scheme decides cookie Secure and the public origin.
  • The geo headers a fronting CDN adds — CF-IPCountry, CF-Region, CF-IPCity, and the conventional X-Geo-* / X-AppEngine-* city, region and country headers — resolve to an empty location in 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.


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.


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:

  • ipAddress should be the address of the machine you signed in from — not an address inside your cluster.
  • location should 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.