forward-headers-strategy defaults differently by environment
Most settings on this site have one default. This one has two, and which you get is decided at startup by the environment rather than by anything in your configuration.
From the Boot reference: “If your application runs in a supported Cloud Platform, the
server.forward-headers-strategy property defaults to NATIVE. In all other instances,
it defaults to NONE.”
So the same artifact, with the same application.properties, honours X-Forwarded-For
and X-Forwarded-Proto in one environment and ignores them completely in another.
server.forward-headers-strategypropertyWhether X-Forwarded-* headers are honoured, and by which layer. The default is conditional rather than fixed — Boot resolves it from the platform it detects at startup, so the same artifact behaves differently in two environments with identical configuration.
- accepts
NONE|NATIVE|FRAMEWORK- default
NATIVE on a detected cloud platform, NONE everywhere else- set in
application.properties
server.tomcat.remoteip.internal-proxiespropertyWhich upstream addresses Tomcat will accept forwarded headers from. Defaults to a regex covering loopback and the RFC 1918 private ranges, so NATIVE on Tomcat trusts a proxy on a private address and ignores one that reaches it from a public address. Setting it empty trusts every proxy — the reference says explicitly not to do that in production.
- accepts
<regex>|(empty)- set in
application.properties
spring.main.cloud-platformpropertyOverrides platform detection. Setting it pins the behaviour above instead of leaving it to whatever environment variables happen to be present, which is the way to stop this default moving under you.
- accepts
NONE|KUBERNETES|CLOUD_FOUNDRY|HEROKU|SAP|NOMAD|AZURE_APP_SERVICE|AWS_ECS- set in
application.properties
What “supported Cloud Platform” covers
Section titled “What “supported Cloud Platform” covers”Boot detects the platform from environment variables at startup. The CloudPlatform
values in 4.1.0 are CLOUD_FOUNDRY, HEROKU, SAP, NOMAD, KUBERNETES,
AZURE_APP_SERVICE, AWS_ECS and NONE.
KUBERNETES is the one that matters, because it is where this stops being trivia. A
service deployed to a cluster gets NATIVE and reads forwarded headers. The same
service running on a developer’s machine, in a plain docker run, or on a VM behind
nginx gets NONE and does not — so request.getRemoteAddr() returns the proxy and
request.isSecure() returns false, and every symptom looks like a proxy
misconfiguration rather than a defaulting rule.
The direction that catches people is usually the second one: something works in the cluster, then does not work in staging on a VM, and the configuration files are identical because the difference was never in them.
# Say what you mean, in every environment.server.forward-headers-strategy=FRAMEWORK
# And pin the platform so detection cannot move the answer.spring.main.cloud-platform=NONESetting the strategy explicitly is the whole fix. Which value you choose matters less than the fact that it is written down:
NATIVEhands the job to the embedded server — Tomcat’sRemoteIpValve, or the Jetty and Reactor Netty equivalents. It is efficient and it brings Tomcat’s own trust rules with it, described below.FRAMEWORKinstalls Spring’sForwardedHeaderFilter(orForwardedHeaderTransformeron the reactive stack), which behaves the same way on every server.NONEignores the headers.
NATIVE carries a second default you did not set
Section titled “NATIVE carries a second default you did not set”Choosing NATIVE on Tomcat does not mean “trust X-Forwarded-For”. It means “trust
X-Forwarded-For from an address matching server.tomcat.remoteip.internal-proxies”,
which defaults to a regex covering loopback and the RFC 1918 private ranges.
That is a sensible default and it is also a second thing that varies by deployment. A
proxy on the same private network is trusted; a proxy that reaches the application from
a public address is not, and the headers are silently ignored — the same visible symptom
as NONE, from a different cause.
The reference gives the escape hatch and immediately warns against it: “You can trust
all proxies by setting the internal-proxies to empty (but do not do so in
production).” An empty value means any client that can reach the port can assert its own
address. Narrow the regex to the proxy you run instead of widening it to everything.
# what did Boot actually resolve at startup?curl -s -u user:PASSWORD http://localhost:8080/actuator/env \ | grep -o '"server.forward-headers-strategy":{[^}]*}'Reading it back from the running application is the point — the value you care about is
the resolved one, not the one in the file, and on a cloud platform those differ by
design. This needs the env endpoint exposed, which is not the default and should not
be left on; see Actuator endpoint exposure and
what /env actually shows before enabling it.
The behavioural check needs no Actuator at all: log request.getRemoteAddr() and
request.isSecure() on a test endpoint, call it through the proxy, and see whether they
describe the client or the proxy.
Turning the strategy on where it was previously NONE changes getRemoteAddr(),
isSecure(), getServerName() and every absolute URL the application generates, all at
once. Redirects and links start using the external host and scheme, which is usually the
reason for enabling it — and which will break anything that was quietly relying on the
internal hostname.
isSecure() becoming true also activates any requiresChannel().requiresSecure() rule
and lets Secure cookies through for the first time. If the proxy is not in fact
terminating TLS, you have just told the application it is.
Pinning spring.main.cloud-platform has effects beyond this setting, since other
auto-configuration consults the same detection. Change it deliberately and check what
else moves rather than setting it purely to stabilise this one property.
Related
Section titled “Related”- Security headers — HSTS depends on the scheme this decides
- Actuator endpoint exposure — what the verify step above needs, and why it stays off
- Application vs server — the proxy half of this control