TLS and trust boundaries

Define trusted proxies

Accept client-address headers only from known upstream proxies and preserve the immediate peer separately.

10 minute lesson

~~~

You already saw that anyone can send an X-Forwarded-For header. Now consider Caddy’s position in a bigger chain. If another CDN or load balancer sits before Caddy, it may supply the original client address in exactly that header — and Caddy should believe it. The same header from an arbitrary direct client is untrusted and should be discarded.

The difference between those two cases is not the header. It’s who the immediate peer is. A trusted proxy configuration lists the peer addresses whose forwarded headers you accept.

Configure the trusted range

Trusted proxies are a server-level setting, so they go in the Caddyfile’s global options block. Configure a placeholder private lab range:

{
  servers {
    trusted_proxies static private_ranges
  }
}

The static module takes a fixed list. private_ranges is a shorthand for the RFC 1918 private networks plus loopback — reasonable for a lab where your “upstream proxy” is another local process. In production you’d list the specific ranges your CDN publishes, nothing wider.

Watch the behavior change

The lab backend logs its incoming headers. Send a forged client address through Caddy and watch what arrives upstream:

curl -H "X-Forwarded-For: 203.0.113.50" http://127.0.0.1:8080/

Run this before and after adding the trusted_proxies block, and compare what the backend logs.

Without trust configured, Caddy treats your curl as an untrusted client: it discards the incoming forwarded headers and writes its own, so the backend sees the real peer address. With loopback trusted, Caddy accepts your claim and passes 203.0.113.50 along, appending the connecting address to the chain.

That second behavior is correct when the peer really is your CDN. It’s a spoofing hole when the peer is the open internet. Send a forged header directly and compare behavior before and after a correctly modeled trusted proxy — the exercise makes the boundary visible in a way no diagram does.

Keep the real peer separate

Whatever the headers say, the TCP peer address is a separate fact. Log it separately at the backend (request.socket.remoteAddress in the lab backend). When forwarded data and the peer disagree unexpectedly, the peer is the one that can’t lie.

Two rules to carry into production. Use real provider ranges only from current official sources — CDN ranges change, and stale lists silently break attribution. And never trust every address merely to make logs look right. That turns user input into security identity, and rate limits, bans, and audit trails all inherit the lie.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →