`remote_ip` Always points to the Cloudflare IP even when `trusted_proxies` is set

1. The problem I’m having:

I’m running a WordPress site in Docker behind the Cloudflare CDN. After switching from nginx to Caddy with the following config:

{
    debug
    servers {
        # https://caddyserver.com/docs/caddyfile/options#trusted-proxies
        # https://www.cloudflare.com/ips/
        trusted_proxies static 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

        trusted_proxies_strict
        client_ip_headers CF-Connecting-IP CF-Connecting-IPv6 X-Forwarded-For
    }
}

In the server log, while client_ip is correctly set to the client request IP, the remote_ip still shows the Cloudflare IP:

{
  "request": {
    "remote_ip": "172.70.49.153", // Cloudflare IP
    "client_ip": "45.67.89.10", // Expected client IP
    "headers": {
      "X-Forwarded-For": ["116.87.139.27"], // Expected client IP
      "Cf-Connecting-Ip": ["116.87.139.27"] // Expected client IP
    }
    // ...other properties
  }
}

2. Error messages and/or full log output:

{
  "level": "debug",
  "ts": 1732891183.3585944,
  "logger": "http.reverse_proxy.transport.fastcgi",
  "msg": "roundtrip",
  "request": {
    "remote_ip": "172.70.49.153",
    "remote_port": "27700",
    "client_ip": "116.87.139.27",
    "proto": "HTTP/2.0",
    "method": "GET",
    "host": "sparanoid.blog",
    "uri": "/index.php?ver=2.2.0",
    "headers": {
      "Cf-Ipcountry": ["SG"],
      "Sec-Ch-Ua-Mobile": ["?0"],
      "Referer": ["https://sparanoid.blog/wp-admin/profile.php?updated=1"],
      "User-Agent": [
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
      ],
      "Sec-Ch-Ua": [
        "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""
      ],
      "Accept": ["text/css,*/*;q=0.1"],
      "Sec-Gpc": ["1"],
      "Sec-Ch-Ua-Platform": ["\"macOS\""],
      "Accept-Encoding": ["gzip, br"],
      "Cdn-Loop": ["cloudflare; loops=1"],
      "Dnt": ["1"],
      "Sec-Fetch-Mode": ["no-cors"],
      "Sec-Fetch-Dest": ["style"],
      "X-Forwarded-Host": ["sparanoid.blog"],
      "Accept-Language": ["en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"],
      "X-Forwarded-Proto": ["https"],
      "Priority": ["u=0"],
      "Sec-Fetch-Site": ["same-origin"],
      "Cf-Visitor": ["{\"scheme\":\"https\"}"],
      "Cookie": ["REDACTED"],
      "Cf-Ray": ["8ea35947ab68ae87-NRT"],
      "Cf-Connecting-Ip": ["116.87.139.27"],
      "X-Forwarded-For": ["116.87.139.27"]
    },
    "tls": {
      "resumed": false,
      "version": 772,
      "cipher_suite": 4865,
      "proto": "h2",
      "server_name": "sparanoid.blog"
    }
  },
  "env": {
    "SERVER_NAME": "sparanoid.blog",
    "SERVER_PROTOCOL": "HTTP/2.0",
    "SCRIPT_NAME": "/index.php",
    "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    "HTTP_SEC_CH_UA": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
    "GATEWAY_INTERFACE": "CGI/1.1",
    "PATH_INFO": "",
    "REQUEST_METHOD": "GET",
    "HTTP_X_FORWARDED_PROTO": "https",
    "SSL_PROTOCOL": "TLSv1.3",
    "HTTP_SEC_CH_UA_MOBILE": "?0",
    "HTTP_ACCEPT": "text/css,*/*;q=0.1",
    "HTTP_DNT": "1",
    "HTTP_SEC_FETCH_MODE": "no-cors",
    "QUERY_STRING": "ver=2.2.0",
    "REMOTE_PORT": "27700",
    "REQUEST_SCHEME": "https",
    "HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
    "HTTP_CF_CONNECTING_IP": "116.87.139.27",
    "SERVER_PORT": "443",
    "SSL_CIPHER": "TLS_AES_128_GCM_SHA256",
    "HTTP_CF_IPCOUNTRY": "SG",
    "HTTP_REFERER": "https://sparanoid.blog/wp-admin/profile.php?updated=1",
    "HTTP_ACCEPT_ENCODING": "gzip, br",
    "REMOTE_HOST": "172.70.49.153",
    "DOCUMENT_ROOT": "/app/public_html",
    "REQUEST_URI": "/wp-content/plugins/search-exclude/build/gutenberg/css/style.css?ver=2.2.0",
    "HTTP_CDN_LOOP": "cloudflare; loops=1",
    "SERVER_SOFTWARE": "Caddy/v2.8.4",
    "HTTP_SEC_GPC": "1",
    "HTTP_HOST": "sparanoid.blog",
    "SCRIPT_FILENAME": "/app/public_html/index.php",
    "HTTP_PRIORITY": "u=0",
    "HTTP_SEC_FETCH_SITE": "same-origin",
    "AUTH_TYPE": "",
    "CONTENT_LENGTH": "",
    "CONTENT_TYPE": "",
    "REMOTE_ADDR": "172.70.49.153",
    "HTTP_X_FORWARDED_HOST": "sparanoid.blog",
    "HTTP_X_FORWARDED_FOR": "116.87.139.27",
    "HTTP_SEC_CH_UA_PLATFORM": "\"macOS\"",
    "HTTPS": "on",
    "HTTP_SEC_FETCH_DEST": "style",
    "HTTP_CF_VISITOR": "{\"scheme\":\"https\"}",
    "HTTP_COOKIE": "",
    "HTTP_CF_RAY": "8ea35947ab68ae87-NRT",
    "REMOTE_IDENT": "",
    "REMOTE_USER": "",
    "DOCUMENT_URI": "/index.php"
  },
  "dial": "php:9000",
  "env": {
    "DOCUMENT_ROOT": "/app/public_html",
    "REQUEST_URI": "/wp-content/plugins/search-exclude/build/gutenberg/css/style.css?ver=2.2.0",
    "SERVER_PORT": "443",
    "SSL_CIPHER": "TLS_AES_128_GCM_SHA256",
    "HTTP_CF_IPCOUNTRY": "SG",
    "HTTP_REFERER": "https://sparanoid.blog/wp-admin/profile.php?updated=1",
    "HTTP_ACCEPT_ENCODING": "gzip, br",
    "REMOTE_HOST": "172.70.49.153",
    "HTTP_CDN_LOOP": "cloudflare; loops=1",
    "HTTP_SEC_GPC": "1",
    "SERVER_SOFTWARE": "Caddy/v2.8.4",
    "CONTENT_LENGTH": "",
    "CONTENT_TYPE": "",
    "HTTP_HOST": "sparanoid.blog",
    "SCRIPT_FILENAME": "/app/public_html/index.php",
    "HTTP_PRIORITY": "u=0",
    "HTTP_SEC_FETCH_SITE": "same-origin",
    "AUTH_TYPE": "",
    "HTTP_X_FORWARDED_HOST": "sparanoid.blog",
    "HTTP_X_FORWARDED_FOR": "116.87.139.27",
    "REMOTE_ADDR": "172.70.49.153",
    "HTTP_SEC_CH_UA_PLATFORM": "\"macOS\"",
    "REMOTE_USER": "",
    "DOCUMENT_URI": "/index.php",
    "HTTPS": "on",
    "HTTP_SEC_FETCH_DEST": "style",
    "HTTP_CF_VISITOR": "{\"scheme\":\"https\"}",
    "HTTP_COOKIE": "",
    "HTTP_CF_RAY": "8ea35947ab68ae87-NRT",
    "REMOTE_IDENT": "",
    "PATH_INFO": "",
    "REQUEST_METHOD": "GET",
    "SERVER_NAME": "sparanoid.blog",
    "SERVER_PROTOCOL": "HTTP/2.0",
    "SCRIPT_NAME": "/index.php",
    "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    "HTTP_SEC_CH_UA": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
    "GATEWAY_INTERFACE": "CGI/1.1",
    "HTTP_X_FORWARDED_PROTO": "https",
    "REMOTE_PORT": "27700",
    "REQUEST_SCHEME": "https",
    "SSL_PROTOCOL": "TLSv1.3",
    "HTTP_SEC_CH_UA_MOBILE": "?0",
    "HTTP_ACCEPT": "text/css,*/*;q=0.1",
    "HTTP_DNT": "1",
    "HTTP_SEC_FETCH_MODE": "no-cors",
    "QUERY_STRING": "ver=2.2.0",
    "HTTP_CF_CONNECTING_IP": "116.87.139.27",
    "HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"
  },
  "request": {
    "remote_ip": "172.70.49.153",
    "remote_port": "27700",
    "client_ip": "116.87.139.27",
    "proto": "HTTP/2.0",
    "method": "GET",
    "host": "sparanoid.blog",
    "uri": "/index.php?ver=2.2.0",
    "headers": {
      "User-Agent": [
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
      ],
      "Sec-Ch-Ua": [
        "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""
      ],
      "Cf-Ipcountry": ["SG"],
      "Sec-Ch-Ua-Mobile": ["?0"],
      "Referer": ["https://sparanoid.blog/wp-admin/profile.php?updated=1"],
      "Accept-Encoding": ["gzip, br"],
      "Cdn-Loop": ["cloudflare; loops=1"],
      "Accept": ["text/css,*/*;q=0.1"],
      "Sec-Gpc": ["1"],
      "Sec-Ch-Ua-Platform": ["\"macOS\""],
      "Priority": ["u=0"],
      "Sec-Fetch-Site": ["same-origin"],
      "Dnt": ["1"],
      "Sec-Fetch-Mode": ["no-cors"],
      "Sec-Fetch-Dest": ["style"],
      "X-Forwarded-Host": ["sparanoid.blog"],
      "Accept-Language": ["en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"],
      "X-Forwarded-Proto": ["https"],
      "Cf-Visitor": ["{\"scheme\":\"https\"}"],
      "Cookie": ["REDACTED"],
      "Cf-Connecting-Ip": ["116.87.139.27"],
      "X-Forwarded-For": ["116.87.139.27"],
      "Cf-Ray": ["8ea35947ab68ae87-NRT"]
    },
    "tls": {
      "resumed": false,
      "version": 772,
      "cipher_suite": 4865,
      "proto": "h2",
      "server_name": "sparanoid.blog"
    }
  }
}

3. Caddy version:

Caddy/v2.8.4

4. How I installed and ran Caddy:

Caddy is running in Docker Compose with userland-proxy disabled.

a. System environment:

  • Docker version 26.1.4, build 5650f9b

b. Custom build Caddy image:

# https://hub.docker.com/_/caddy
FROM caddy:2.8.4-builder AS builder

RUN xcaddy build \
  --with github.com/caddyserver/cache-handler \
  --with github.com/caddy-dns/route53 \
  --with github.com/caddy-dns/cloudflare \
  --with github.com/mholt/caddy-ratelimit \
  --with github.com/fvbommel/caddy-dns-ip-range \
  --with github.com/WeidiDeng/caddy-cloudflare-ip \
  --with github.com/xcaddyplugins/caddy-trusted-cloudfront \
  --with github.com/xcaddyplugins/caddy-trusted-gcp-cloudcdn

FROM caddy:2.8.4

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

c. Service/unit/compose file:

  caddy:
    image: caddy-custom:latest
    restart: always
    cap_add:
      - NET_ADMIN
    env_file: ./caddy.env
    build:
      context: .
      dockerfile: Dockerfile-caddy
    ports:
      - 80:80
      - 443:443
      - 443:443/udp
    volumes:
      - ./config/caddy:/etc/caddy
      - ./data/caddy_data:/data
      - ./data/caddy_config:/config
      - /srv/www:/app

d. My complete Caddy config:

{
    debug
    servers {
        # https://caddyserver.com/docs/caddyfile/options#trusted-proxies
        # https://www.cloudflare.com/ips/
        trusted_proxies static 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

        trusted_proxies_strict
        client_ip_headers CF-Connecting-IP CF-Connecting-IPv6 X-Forwarded-For
    }
}

sparanoid.blog {
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}
	header {
		-Server
	}
	encode zstd gzip
	root * /app/public_html

	# debug only
	log {
		output stdout
		format console
	}

	php_fastcgi php:9000 {
		header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
	}
	file_server
}

5. Others:

It partially works since client_ip, X-Forwarded-For, and Cf-Connecting-Ip are correctly set to the user request IP. However, WordPress and most other applications read the remote IP as the true client IP.

For example, in the WordPress dashboard with plugins like Simple History, it gets IP from $_SERVER["REMOTE_ADDR"]:

  • _server_remote_addr: 172.70.49.x
  • _server_http_x_forwarded_for_0: 116.87.139.x

With nginx using this configuration, $_SERVER["REMOTE_ADDR"] correctly points to the client IP:

set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
...other CF IPs from https://www.cloudflare.com/ips/

real_ip_header CF-Connecting-IP;

Is my Caddy configuration missing something, or is this the expected behavior where remote_ip remains unchanged? Please let me know.