Secure Caddy Setup Behind Cloudflare: Accurate Client IP Detection and IP-Based Access Control

Deploying Caddy behind Cloudflare’s reverse proxy requires special attention so that you can accurately detect the real visitor IP (rather than the Cloudflare edge IP), and use it for access filtering. This guide walks through the correct configuration, common pitfalls, and how to fix them.

Why Real IP Doesn’t Show by Default

  • Caddy only trusts client IP headers (like X-Forwarded-For or CF-Connecting-IP) if the incoming request comes from a trusted proxy. Otherwise, it uses the proxy IP as both remote_ip and client_ip.
  • By default, Cloudflare allows clients to send custom X-Forwarded-For headers—which can be spoofed—unless Cloudflare rewrites them
  • If trusted_proxies_strict is not enabled, Caddy picks the first untrusted IP in a left‑to‑right parsing of the header—this can be incorrect if proxies append IPs.

Core Global Options for Caddyfile

{
  debug
  servers {
    trusted_proxies static <all Cloudflare IP ranges here>
    trusted_proxies_strict
    client_ip_headers Cf-Connecting-Ip X-Forwarded-For
  }
}

Why each part matters:

  • debug enables verbose logs to troubleshoot IP matching.
  • trusted_proxies static lists Cloudflare IP CIDRs only—Caddy will only trust headers from these ranges.
  • trusted_proxies_strict forces right‑to‑left header parsing—secure behavior when proxies append multiple IPs.
  • client_ip_headers prioritizes CF-Connecting-IP, with fallback to X-Forwarded-For.

Enabling IP-Based Access Control

example.com {
  @allowed client_ip 203.0.113.0/24 198.51.100.45
  @blocked not client_ip 203.0.113.0/24 198.51.100.45

  respond @blocked "Forbidden" 403

  reverse_proxy localhost:8123
} 

Here, any request whose parsed client_ip matches your approved IPs gets proxied; all others are blocked early with a 403 response.

Common Errors & How to Fix Them

  • Error: “server block without any key is global configuration…” – Fix: The global `{ … }` block must be at the very top of your Caddyfile.
  • Error: Real visitor IPs still not showing – Fix: Ensure you fetched all Cloudflare ranges into trusted_proxies—missing one invalidates header trust.
  • Unexpected 403 for all clients – Fix: Check logs under `DEBUG` level to verify `client_ip` vs `remote_ip` values; often headers are ignored due to untrusted proxy.

Example Debug Log Output

{"remote_ip":"172.x.x.x","client_ip":"203.x.x.x","request":{…},"status":403, …}

If client_ip still shows the proxy, Caddy is not treating that proxy as trusted. Add it to your list.

Optional—Automate Cloudflare IP Updates

Maintaining Cloudflare IP lists manually can be error-prone. You can automate using the caddy-cloudflare-ip plugin, which fetches and refreshes ranges periodically within Caddy.

Checklist

  1. Place global block at top with trusted_proxies, strict, and header config.
  2. Insert site block with a client_ip-based allowlist and blocked response.
  3. Enable debug and tail logs to ensure correct IP detection.
  4. Configure Cloudflare to rewrite incoming headers—prevent spoofing.
  5. (Optional) Automate IP list updates via plugin or script.

Why This Setup Works

This approach guarantees:

  • Only Cloudflare edge servers can supply trusted IP headers.
  • If Cloudflare is configured to overwrite client headers, no spoofing is possible.
  • trusted_proxies_strict correctly identifies the true visitor IP amid chains of proxies.
  • Your own client IP ranges are used only to decide access—not to define trust boundaries.

It protects your origin server from unauthorized access while reliably identifying end users’ real IP addresses behind Cloudflare.

Appreciate your comments.

Leave a Reply

Your email address will not be published. Required fields are marked *