By default, Caddy's reverse proxy ignores X-Forwarded headers sent by clients and sets its own X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. You only need trusted_proxies when Caddy itself sits behind another proxy or CDN whose forwarded values you actually want to believe and use.
Safe by default
The Caddy docs say incoming headers, including Host, pass to the backend unchanged, except for X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, which Caddy sets. They also say Caddy ignores incoming X-Forwarded values by default to prevent spoofing.
That is the opposite failure mode from a proxy that appends to whatever the client sent. With Caddy at the edge, your application sees one clean address.
When you add trusted_proxies
If a CDN or load balancer sits in front of Caddy, every request arrives from that hop, and the real client address is in its forwarded header. The trusted_proxies option lists the CIDR ranges whose forwarded values can be trusted. The docs recommend setting it through the servers global option instead of per proxy, which applies it to all proxy handlers and enables client IP parsing.
{
servers {
trusted_proxies static 203.0.113.0/24
}
}
The same rule as in nginx applies: list only the addresses of hops you control or can verify. A wide range lets any client inside it set its own address.
The Host header and HTTPS upstreams
Since Caddy 2.11.0, Host is set automatically to the upstream's host when proxying to HTTPS, so manual overrides are no longer needed. X-Forwarded-Host still carries the original value. Before 2.11.0 you had to set the Host header yourself.
When TLS ends in Caddy and it proxies over plain HTTP to a port or Unix socket, Host must be set explicitly. Make sure your application does not trust Host blindly for links in password reset emails. See Django allowed hosts for the same lesson in an app.
Upstream TLS verification
Using an https upstream turns on TLS to the backend automatically. tls_server_name sets the name used to check the upstream certificate, and tls_trust_pool picks the trusted CAs. The docs flag tls_insecure_skip_verify as not for production. If you find it in a config, treat it as a finding, since it makes the hop to your backend open to impersonation.
Health checks and retries
Active health checks start only when you set health_uri or health_port. Defaults are a 30 second interval and 5 second timeout. Passive checks start when fail_duration is above zero. The default load balancing policy is random, and lb_retries is 0 by default. By default only GET requests are retried after a failed round trip, which avoids replaying a POST.
Review steps
- Find out whether anything sits in front of Caddy.
- If yes, set trusted_proxies to exactly those ranges.
- Send a spoofed X-Forwarded-For with curl and read what the backend logs.
- Grep for tls_insecure_skip_verify.
Where Safeguard fits
Safeguard covers the software behind the proxy through dependency scanning and policy gates, not proxy settings. Try the free scan at https://gold.safeguard.sh/scan.
Quick check
Does Caddy trust a client supplied X-Forwarded-For by default?
No. It ignores incoming X-Forwarded values unless the sender is in trusted_proxies.
Sources
Last reviewed: 19 October 2026.