No3. Cloudflare 真實訪客 IP:Nginx real_ip 與 Apache2 RemoteIPTrustedProxyList
網站使用 Cloudflare Proxy 後,真正和 Origin 建立 TCP 連線的是 Cloudflare Edge,不是訪客的瀏覽器。因此,Nginx 或 Apache2 在沒有額外設定時,Access Log 記錄 Cloudflare IP 是正常結果。
Cloudflare 會透過 CF-Connecting-IP 將訪客 IP 傳給 Origin,但這個值仍然是 HTTP Header。Origin 不能只因為 Header 名稱看起來像 Cloudflare,就直接把它當成可信的 Client IP。
安全的處理方式包含兩項設定:
Cloudflare CIDR
├── 可以連到 Origin
└── 可以透過 CF-Connecting-IP 提供 Visitor IP
其他來源
├── 不應直接連到 Origin
└── 即使自行加入 CF-Connecting-IP,也不能改寫 Client IP
本文以 Ubuntu 24.04 VM 為環境,分別示範 Nginx real_ip 與 Apache2 mod_remoteip。兩種 Web Server 擇一設定即可。
前半篇固定採用下列前提:GCP Firewall 已限制只有 Cloudflare CIDR 可以連入 Origin 的 80/443,Nginx 與 Apache2 不負責來源阻擋,只處理 Trusted Proxy 與 Visitor IP。來源限制若設在 Web Server,會在 Nginx、Apache2 與 systemd 設定完成並驗證後獨立說明。
Visitor
│
▼
Cloudflare Edge
│ Cloudflare source IP
▼
GCP Firewall(只允許 Cloudflare CIDR)
│
▼
Origin VM(Nginx 或 Apache2)
Cloudflare Proxy 後的 Client IP
連線經過 Cloudflare 後,會同時出現三種 IP:
| 名稱 | 取得位置 | 用途 |
|---|---|---|
| Visitor IP | CF-Connecting-IP |
Access Log、GeoIP、Rate Limit 與 Application Log |
| Connection peer IP | TCP connection | 確認實際連入 Origin 的 Proxy |
| Web Server Client IP | Nginx $remote_addr 或 Apache2 %a |
Web Server 經 trusted proxy 規則處理後採用的 Client IP |
假設訪客 IP 是 198.51.100.25,Cloudflare Edge IP 是 173.245.48.10:
Visitor 198.51.100.25
│
▼
Cloudflare 173.245.48.10
│
│ CF-Connecting-IP: 198.51.100.25
▼
Origin VM
尚未啟用 real-IP module 時,Web Server 看到的 Client IP 是 173.245.48.10。啟用後則應得到:
Visitor IP:198.51.100.25
Peer IP:173.245.48.10
Peer IP 不應被 Visitor IP 完全取代。它仍是判斷 request 是否由 Cloudflare 傳入,以及排查 Proxy 路徑的重要資料。
CF-Connecting-IP 的信任條件
一般 HTTP client 也能自行建立同名 Header:
curl \
--header 'CF-Connecting-IP: 198.51.100.77' \
'http://127.0.0.1/'
因此,CF-Connecting-IP 能否改寫 Client IP,必須由 TCP peer 決定:
- Origin 只接受 Cloudflare 或其他明確核准的來源。
- Nginx
set_real_ip_from或 Apache2RemoteIPTrustedProxyList只包含 Cloudflare 官方 CIDR。 - Application 使用 Web Server 已驗證的 Client IP,不再自行信任任意
X-Forwarded-For。
Cloudflare 將 CF-Connecting-IP 定義為連到 Cloudflare 的訪客 IP。相較之下,X-Forwarded-For 可能包含多個 Proxy hop,也可能保留 client 原先傳入的內容;本文的單層 Cloudflare 架構直接使用 CF-Connecting-IP。Cloudflare:HTTP request headers
下列設定不能視為等價:
# 錯誤:允許任何來源提供 Client IP
set_real_ip_from 0.0.0.0/0;
set_real_ip_from ::/0;
# 不完整:只有指定 Header,沒有 Trusted Proxy
RemoteIPHeader CF-Connecting-IP
Cloudflare CIDR 可能調整,官方建議 Origin allowlist 與 Proxy trust 使用最新的 Cloudflare IP ranges。Cloudflare:Cloudflare IP addresses
Pseudo IPv4
Cloudflare 的 Pseudo IPv4 若設為 Overwrite Headers,IPv6 訪客的 CF-Connecting-IP 會被改成 Class E IPv4,原始 IPv6 則位於 CF-Connecting-IPv6。需要保留原始 IPv6 時,必須先確認目前 Cloudflare Network 設定;不能看到 IPv4 格式就假設訪客真的使用 IPv4。Cloudflare:Pseudo IPv4 headers
本文其餘範例假設未使用 Overwrite Headers。
Nginx real_ip 設定
確認 Real IP Module 與設定位置
在 Origin VM 執行:
sudo nginx -V 2>&1 | grep -- '--with-http_realip_module'
sudo nginx -T
/第一個指令應找到 --with-http_realip_module。Ubuntu 官方 Nginx package 通常已包含此 module;自訂編譯版本仍應實際確認。
接著確認 /etc/nginx/nginx.conf 的 http context 內包含:
include /etc/nginx/conf.d/*.conf;
本文把固定 real-IP 設定放在 /etc/nginx/conf.d/cloudflare-real-ip.conf,每日產生的 Cloudflare CIDR 則放在 /etc/nginx/snippets/。
建立 Nginx Trusted Proxy Updater
安裝工具:
sudo apt update
sudo apt install --yes curl diffutils jq util-linux
建立 updater:
sudo nano /usr/local/sbin/update-cloudflare-nginx-real-ip
內容如下:
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ "$EUID" -ne 0 ]]; then
echo 'Run this updater as root.' >&2
exit 1
fi
TRUSTED_FILE='/etc/nginx/snippets/cloudflare-real-ip-trusted.conf'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"
cleanup() {
rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT
curl \
--fail \
--silent \
--show-error \
--connect-timeout 5 \
--max-time 20 \
--retry 3 \
--output "$WORK_DIRECTORY/response.json" \
"$API_URL"
jq --exit-status '
.success == true
and ((.result.ipv4_cidrs | type) == "array")
and ((.result.ipv4_cidrs | length) > 0)
and ((.result.ipv6_cidrs | type) == "array")
and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null
jq --raw-output '
.result.ipv4_cidrs[],
.result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
| sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"
[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]
awk '{ print "set_real_ip_from " $0 ";" }' \
"$WORK_DIRECTORY/cloudflare-ips.lst" \
> "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf"
install -d -m 0755 /etc/nginx/snippets
if [[ -f "$TRUSTED_FILE" ]] \
&& cmp --silent "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" "$TRUSTED_FILE"; then
echo 'Cloudflare trusted proxy list unchanged.'
exit 0
fi
install -m 0644 \
"$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" \
"$TRUSTED_FILE"
nginx -t
systemctl reload nginx
echo 'Cloudflare trusted proxy list updated for Nginx.'
設定權限並先產生第一份清單:
sudo chown root:root /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo /usr/local/sbin/update-cloudflare-nginx-real-ip
cloudflare-real-ip-trusted.conf 會是:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# 其餘 Cloudflare IPv4、IPv6 CIDR
設定 Client IP 與 Access Log
建立 /etc/nginx/conf.d/cloudflare-real-ip.conf:
sudo nano /etc/nginx/conf.d/cloudflare-real-ip.conf
include /etc/nginx/snippets/cloudflare-real-ip-trusted.conf;
real_ip_header CF-Connecting-IP;
log_format cloudflare_realip
'visitor=$remote_addr peer=$realip_remote_addr '
'cf_ip=$http_cf_connecting_ip cf_ray=$http_cf_ray '
'"$request" $status $body_bytes_sent';
set_real_ip_from 決定哪些 connection peer 有權提供 CF-Connecting-IP。Nginx 完成改寫後:
$remote_addr是 Visitor IP。$realip_remote_addr保留原始 connection peer。$http_cf_connecting_ip是收到的 Header,適合用來交叉檢查,不應單獨作為授權依據。
real_ip_recursive 預設為 off。本文使用單值的 CF-Connecting-IP,不需要啟用 X-Forwarded-For chain 的遞迴解析。NGINX:Real IP module
在網站的 server block 指定新的 log format:
server {
listen 443 ssl;
server_name www.example.com;
access_log /var/log/nginx/www.example.com.access.log cloudflare_realip;
# 既有 TLS、root 或 proxy_pass 設定。
}
檢查並套用:
sudo nginx -t
sudo systemctl reload nginx
若 Nginx 後方還有 Application server,應將已驗證的 $remote_addr 明確傳給下游,避免 Application 再解析 client 自行建立的 X-Forwarded-For chain:
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:8000;
}
Apache2 mod_remoteip 設定
啟用 mod_remoteip
在 Origin VM 執行:
sudo apache2ctl -S
sudo a2enmod remoteip
sudo apache2ctl -M | grep remoteip
正常會看到:
remoteip_module (shared)
Apache 2.4 的 mod_remoteip 會讓還原後的 Visitor IP 參與 logging、authorization 與其他 module 的處理;原始 connection peer 仍可透過 %{c}a 或 expression 內的 CONN_REMOTE_ADDR 取得。Apache HTTP Server:mod_remoteip
建立 Apache2 Trusted Proxy Updater
安裝工具:
sudo apt update
sudo apt install --yes curl diffutils jq util-linux
建立 updater:
sudo nano /usr/local/sbin/update-cloudflare-apache-real-ip
內容如下:
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ "$EUID" -ne 0 ]]; then
echo 'Run this updater as root.' >&2
exit 1
fi
TRUSTED_FILE='/etc/apache2/cloudflare-ips.lst'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"
cleanup() {
rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT
curl \
--fail \
--silent \
--show-error \
--connect-timeout 5 \
--max-time 20 \
--retry 3 \
--output "$WORK_DIRECTORY/response.json" \
"$API_URL"
jq --exit-status '
.success == true
and ((.result.ipv4_cidrs | type) == "array")
and ((.result.ipv4_cidrs | length) > 0)
and ((.result.ipv6_cidrs | type) == "array")
and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null
jq --raw-output '
.result.ipv4_cidrs[],
.result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
| sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"
[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]
if [[ -f "$TRUSTED_FILE" ]] \
&& cmp --silent "$WORK_DIRECTORY/cloudflare-ips.lst" "$TRUSTED_FILE"; then
echo 'Cloudflare trusted proxy list unchanged.'
exit 0
fi
install -m 0644 \
"$WORK_DIRECTORY/cloudflare-ips.lst" \
"$TRUSTED_FILE"
apache2ctl configtest
systemctl reload apache2
echo 'Cloudflare trusted proxy list updated for Apache2.'
設定權限並產生第一份清單:
sudo chown root:root /usr/local/sbin/update-cloudflare-apache-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-apache-real-ip
sudo /usr/local/sbin/update-cloudflare-apache-real-ip
/etc/apache2/cloudflare-ips.lst 的格式是一行一個 CIDR:
103.21.244.0/22
173.245.48.0/20
# 其餘 Cloudflare IPv4、IPv6 CIDR
RemoteIPTrustedProxyList 會在 Apache 啟動或 reload 時讀取這份檔案。
設定 RemoteIPHeader 與 Access Log
建立 /etc/apache2/conf-available/cloudflare-real-ip.conf:
sudo nano /etc/apache2/conf-available/cloudflare-real-ip.conf
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxyList /etc/apache2/cloudflare-ips.lst
LogFormat "visitor=%a peer=%{c}a cf_ip=%{CF-Connecting-IP}i cf_ray=%{CF-Ray}i \"%r\" %>s %b" cloudflare_realip
啟用設定:
sudo a2enconf cloudflare-real-ip
sudo apache2ctl configtest
sudo systemctl reload apache2
在目標 VirtualHost 使用新的 log format:
<VirtualHost *:443>
ServerName www.example.com
CustomLog ${APACHE_LOG_DIR}/www.example.com.access.log cloudflare_realip
# 既有 TLS、DocumentRoot 或 ProxyPass 設定。
</VirtualHost>
再次檢查並套用:
sudo apache2ctl configtest
sudo systemctl reload apache2
設定完成後:
%a是mod_remoteip還原的 Visitor IP。%{c}a是實際連入 Apache 的 Cloudflare peer IP。- CGI、PHP 或其他使用 Apache request client address 的程式,通常會在
REMOTE_ADDR取得還原後的 IP;實際結果仍應在 Application log 驗證。
只有 RemoteIPHeader、沒有 RemoteIPTrustedProxyList,等於沒有明確限制誰能提供 Client IP。Trusted Proxy List 必須保留。Apache HTTP Server:RemoteIPTrustedProxyList
systemd 每日更新 Trusted Proxy List
GCP Firewall 的 Cloudflare CIDR 仍由 Cloud Run Job 與 Cloud Scheduler 維護。本節的 systemd timer 只更新 VM 內的 Nginx 或 Apache2 trusted proxy list。
建立 service:
sudo nano /etc/systemd/system/cloudflare-trusted-proxy-update.service
Nginx 範例:
[Unit]
Description=Update Cloudflare trusted proxy list
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/flock --nonblock /run/cloudflare-trusted-proxy-update.lock /usr/local/sbin/update-cloudflare-nginx-real-ip
TimeoutStartSec=120
使用 Apache2 時,將 ExecStart 的最後一個路徑改成:
/usr/local/sbin/update-cloudflare-apache-real-ip
建立 timer:
sudo nano /etc/systemd/system/cloudflare-trusted-proxy-update.timer
[Unit]
Description=Daily Cloudflare trusted proxy list update
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=30m
[Install]
WantedBy=timers.target
檢查並啟用:
sudo systemd-analyze verify \
/etc/systemd/system/cloudflare-trusted-proxy-update.service \
/etc/systemd/system/cloudflare-trusted-proxy-update.timer
sudo systemctl daemon-reload
sudo systemctl start cloudflare-trusted-proxy-update.service
sudo systemctl enable --now cloudflare-trusted-proxy-update.timer
systemctl list-timers cloudflare-trusted-proxy-update.timer
sudo journalctl \
--unit=cloudflare-trusted-proxy-update.service \
--lines=50 \
--no-pager
API request、JSON schema 或非空檢查失敗時,script 會在寫入設定前結束。清單沒有變動時不會 reload Web Server。
驗證 Visitor IP、Peer IP 與 Header Spoofing
正常 Cloudflare 路徑
從一般外部網路送出帶有唯一識別值的 request:
REAL_IP_TEST_ID="real-ip-test-$(date -u +%Y%m%dT%H%M%SZ)-${RANDOM}"
curl --noproxy '*' \
--silent \
--show-error \
--output /dev/null \
"https://www.example.com/healthz?real_ip_test=${REAL_IP_TEST_ID}"
printf 'Access Log 應出現:%s\n' "$REAL_IP_TEST_ID"
Nginx:
sudo grep --fixed-strings 'real-ip-test-REPLACE_WITH_ACTUAL_ID' \
/var/log/nginx/www.example.com.access.log
Apache2:
sudo grep --fixed-strings 'real-ip-test-REPLACE_WITH_ACTUAL_ID' \
/var/log/apache2/www.example.com.access.log
預期格式:
visitor=198.51.100.25 peer=173.245.48.10 cf_ip=198.51.100.25 cf_ray=...
檢查重點不是 IP 是否剛好和範例相同,而是:
visitor與測試端的公開 IP 相符。peer位於 Cloudflare 官方 CIDR。visitor與cf_ip相符。CF-Ray有值,且 request 確實出現在 Origin Access Log。
/healthz 必須替換成明確不經 Cloudflare cache 的 endpoint;只看到 Cloudflare response,不能證明 request 已抵達 Origin。
VM 本機偽造 Header
在 Origin VM 從 loopback 送出偽造 Header:
curl --noproxy '*' \
--insecure \
--silent \
--show-error \
--dump-header - \
--output /dev/null \
--resolve 'www.example.com:443:127.0.0.1' \
--header 'CF-Connecting-IP: 198.51.100.77' \
'https://www.example.com/'
--insecure 僅用於 VM 本機、自己管理的 Origin 測試。正式監控不應略過 TLS certificate validation。
若 127.0.0.1 沒有被加入 trusted proxy list,預期結果如下:
- Nginx
$remote_addr仍是127.0.0.1。 - Apache2
%a仍是127.0.0.1。 CF-Connecting-IP雖然存在,但不能改寫 Client IP。
若 log 顯示 Visitor IP 被改成 198.51.100.77,代表 trusted proxy 範圍過大,或另一層 Application 又自行解析了 Header。
驗證矩陣
| 測試 | Visitor IP | Peer IP | 預期結果 |
|---|---|---|---|
| 經 Cloudflare 的新 request | 外部測試端 IP | Cloudflare Edge IP | Request 成功並同時記錄兩種 IP |
| 外部 direct Origin | 不適用 | 外部測試端 IP | 被 GCP Firewall 阻擋,通常為 timeout |
VM 本機偽造 CF-Connecting-IP |
127.0.0.1 |
127.0.0.1 |
Header 不得改寫 Client IP |
| API 回傳失敗或空清單 | 沿用 Web Server 已載入的設定 | 不變 | updater failed,不 reload |
來源限制位於 Web Server 時
前面的 Nginx 與 Apache2 設定都假設 GCP Firewall 已經只允許 Cloudflare CIDR。Firewall 直接使用封包的 source IP,不受 Web Server 改寫 Client IP 的影響。
無法管理 GCP Firewall、來源限制改設在 Web Server 時,必須處理 real-IP module 的執行結果:
real-IP module 執行前:Client IP = Cloudflare peer
real-IP module 執行後:Client IP = Visitor IP
Nginx allow Cloudflare-CIDR 與 Apache2 Require ip Cloudflare-CIDR 都可能改為比對 Visitor IP,使正常的 Cloudflare request 被拒絕。這種架構的來源 gate 必須明確使用原始 connection peer。
Nginx 使用 $realip_remote_addr
Nginx 的 $remote_addr 會在 Real IP Module 執行後變成 Visitor IP;$realip_remote_addr 則保留實際連入 Nginx 的 connection peer。來源限制因此要用 $realip_remote_addr 判斷,不能沿用上一篇直接比對 $remote_addr 的 allow/deny。
完整的 /etc/nginx/conf.d/cloudflare-real-ip.conf 如下:
include /etc/nginx/snippets/cloudflare-real-ip-trusted.conf;
real_ip_header CF-Connecting-IP;
geo $realip_remote_addr $cloudflare_peer {
default 0;
include /etc/nginx/snippets/cloudflare-peer-geo.conf;
}
log_format cloudflare_realip
'visitor=$remote_addr peer=$realip_remote_addr '
'cf_ip=$http_cf_connecting_ip cf_ray=$http_cf_ray '
'"$request" $status $body_bytes_sent';
cloudflare-real-ip-trusted.conf 與 cloudflare-peer-geo.conf 都由本節稍後的完整 Updater 產生。前者決定哪些 peer 有權提供 CF-Connecting-IP;後者將同一份 Cloudflare CIDR 映射成 $cloudflare_peer=1。
需要保護的 server block 使用以下設定:
server {
listen 443 ssl;
server_name www.example.com;
access_log /var/log/nginx/www.example.com.access.log cloudflare_realip;
if ($cloudflare_peer = 0) {
return 403;
}
# 既有 TLS、root 或 proxy_pass 設定。
}
這裡的 if 只執行固定的 return。geo 可以指定 $realip_remote_addr 作為輸入,依 Cloudflare CIDR 判斷原始 peer。NGINX:Geo module
上一篇若已在這個 server block 加入 Cloudflare allow 清單與 deny all,必須移除,避免它們改用還原後的 Visitor IP 判斷。
Nginx Updater 完整版
用以下內容完整取代 /usr/local/sbin/update-cloudflare-nginx-real-ip:
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ "$EUID" -ne 0 ]]; then
echo 'Run this updater as root.' >&2
exit 1
fi
TRUSTED_FILE='/etc/nginx/snippets/cloudflare-real-ip-trusted.conf'
PEER_GEO_FILE='/etc/nginx/snippets/cloudflare-peer-geo.conf'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"
cleanup() {
rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT
curl \
--fail \
--silent \
--show-error \
--connect-timeout 5 \
--max-time 20 \
--retry 3 \
--output "$WORK_DIRECTORY/response.json" \
"$API_URL"
jq --exit-status '
.success == true
and ((.result.ipv4_cidrs | type) == "array")
and ((.result.ipv4_cidrs | length) > 0)
and ((.result.ipv6_cidrs | type) == "array")
and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null
jq --raw-output '
.result.ipv4_cidrs[],
.result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
| sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"
[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]
awk '{ print "set_real_ip_from " $0 ";" }' \
"$WORK_DIRECTORY/cloudflare-ips.lst" \
> "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf"
awk '{ print $0 " 1;" }' \
"$WORK_DIRECTORY/cloudflare-ips.lst" \
> "$WORK_DIRECTORY/cloudflare-peer-geo.conf"
install -d -m 0755 /etc/nginx/snippets
if [[ -f "$TRUSTED_FILE" && -f "$PEER_GEO_FILE" ]] \
&& cmp --silent "$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" "$TRUSTED_FILE" \
&& cmp --silent "$WORK_DIRECTORY/cloudflare-peer-geo.conf" "$PEER_GEO_FILE"; then
echo 'Cloudflare Nginx IP files unchanged.'
exit 0
fi
install -m 0644 \
"$WORK_DIRECTORY/cloudflare-real-ip-trusted.conf" \
"$TRUSTED_FILE"
install -m 0644 \
"$WORK_DIRECTORY/cloudflare-peer-geo.conf" \
"$PEER_GEO_FILE"
nginx -t
systemctl reload nginx
echo 'Cloudflare trusted proxy and peer allowlist updated for Nginx.'
設定執行權限並套用:
sudo chown root:root /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo /usr/local/sbin/update-cloudflare-nginx-real-ip
sudo nginx -t
sudo systemctl reload nginx
原本的 systemd service 不必修改,因為 Updater 路徑維持不變。每天更新時,Trusted Proxy List 與 peer allowlist 會從同一份 API response 一起產生。
Nginx 設定總結
| 設定 | 比對或輸出 | 用途 |
|---|---|---|
set_real_ip_from |
Cloudflare CIDR | 限制誰能用 CF-Connecting-IP 改寫 Client IP |
$remote_addr |
Visitor IP | 提供 Access Log、Application 與分析使用 |
$realip_remote_addr |
Connection peer | 保留實際連入 Nginx 的來源 IP |
geo $realip_remote_addr |
Cloudflare CIDR | 判斷 connection peer 是否屬於 Cloudflare |
$cloudflare_peer = 0 |
非 Cloudflare peer | 回覆 403,拒絕直連 Origin |
這套設定同時處理兩件事:只有 Cloudflare 可以宣告 Visitor IP,且只有 Cloudflare connection peer 可以進入網站。兩項判斷共用同一批 CIDR,但用途與 Nginx 變數不同。
Apache2 使用 CONN_REMOTE_ADDR
Apache2 的 %a 與一般 Require ip 會使用 mod_remoteip 改寫後的 Visitor IP;CONN_REMOTE_ADDR 則保留底層 connection peer。來源限制要以 CONN_REMOTE_ADDR 搭配 -ipmatch 判斷。
/etc/apache2/conf-available/cloudflare-real-ip.conf 維持完整的 Real IP 與 log 設定:
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxyList /etc/apache2/cloudflare-ips.lst
LogFormat "visitor=%a peer=%{c}a cf_ip=%{CF-Connecting-IP}i cf_ray=%{CF-Ray}i \"%r\" %>s %b" cloudflare_realip
在目標 VirtualHost 載入 connection peer 的來源規則:
<VirtualHost *:443>
ServerName www.example.com
CustomLog ${APACHE_LOG_DIR}/www.example.com.access.log cloudflare_realip
<Location "/">
Include /etc/apache2/cloudflare-peer-require.conf
</Location>
# 既有 TLS、DocumentRoot 或 ProxyPass 設定。
</VirtualHost>
CONN_REMOTE_ADDR 是 connection peer;-ipmatch 依 IP 或 CIDR 比對。Apache HTTP Server:Expression parser
上一篇若已加入直接比對 Client IP 的 Require ip 規則,必須移除。相同 <Location>、<Directory> 或 Proxy 區段若另有 authorization 規則,也要檢查合併後的結果。
Apache2 Updater 完整版
用以下內容完整取代 /usr/local/sbin/update-cloudflare-apache-real-ip:
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ "$EUID" -ne 0 ]]; then
echo 'Run this updater as root.' >&2
exit 1
fi
TRUSTED_FILE='/etc/apache2/cloudflare-ips.lst'
PEER_REQUIRE_FILE='/etc/apache2/cloudflare-peer-require.conf'
API_URL='https://api.cloudflare.com/client/v4/ips'
WORK_DIRECTORY="$(mktemp -d)"
cleanup() {
rm -rf -- "$WORK_DIRECTORY"
}
trap cleanup EXIT
curl \
--fail \
--silent \
--show-error \
--connect-timeout 5 \
--max-time 20 \
--retry 3 \
--output "$WORK_DIRECTORY/response.json" \
"$API_URL"
jq --exit-status '
.success == true
and ((.result.ipv4_cidrs | type) == "array")
and ((.result.ipv4_cidrs | length) > 0)
and ((.result.ipv6_cidrs | type) == "array")
and ((.result.ipv6_cidrs | length) > 0)
' "$WORK_DIRECTORY/response.json" >/dev/null
jq --raw-output '
.result.ipv4_cidrs[],
.result.ipv6_cidrs[]
' "$WORK_DIRECTORY/response.json" \
| sort --unique > "$WORK_DIRECTORY/cloudflare-ips.lst"
[[ -s "$WORK_DIRECTORY/cloudflare-ips.lst" ]]
{
echo '<RequireAny>'
while IFS= read -r cidr; do
printf " Require expr \"%%{CONN_REMOTE_ADDR} -ipmatch '%s'\"\n" "$cidr"
done < "$WORK_DIRECTORY/cloudflare-ips.lst"
echo '</RequireAny>'
} > "$WORK_DIRECTORY/cloudflare-peer-require.conf"
if [[ -f "$TRUSTED_FILE" && -f "$PEER_REQUIRE_FILE" ]] \
&& cmp --silent "$WORK_DIRECTORY/cloudflare-ips.lst" "$TRUSTED_FILE" \
&& cmp --silent "$WORK_DIRECTORY/cloudflare-peer-require.conf" "$PEER_REQUIRE_FILE"; then
echo 'Cloudflare Apache2 IP files unchanged.'
exit 0
fi
install -m 0644 \
"$WORK_DIRECTORY/cloudflare-ips.lst" \
"$TRUSTED_FILE"
install -m 0644 \
"$WORK_DIRECTORY/cloudflare-peer-require.conf" \
"$PEER_REQUIRE_FILE"
apache2ctl configtest
systemctl reload apache2
echo 'Cloudflare trusted proxy and peer allowlist updated for Apache2.'
設定執行權限並套用:
sudo chown root:root /usr/local/sbin/update-cloudflare-apache-real-ip
sudo chmod 0755 /usr/local/sbin/update-cloudflare-apache-real-ip
sudo /usr/local/sbin/update-cloudflare-apache-real-ip
sudo apache2ctl configtest
sudo systemctl reload apache2
原本的 systemd service 同樣不必修改。每天更新時,cloudflare-ips.lst 與 cloudflare-peer-require.conf 會由同一份 API response 產生。
Apache2 設定總結
| 設定 | 比對或輸出 | 用途 |
|---|---|---|
RemoteIPTrustedProxyList |
Cloudflare CIDR | 限制誰能用 CF-Connecting-IP 改寫 Client IP |
%a |
Visitor IP | 提供 Access Log、Application 與分析使用 |
%{c}a |
Connection peer | 在 Access Log 保留實際連入 Apache2 的來源 IP |
CONN_REMOTE_ADDR -ipmatch |
Cloudflare CIDR | 依 connection peer 執行來源限制 |
<RequireAny> |
任一 Cloudflare CIDR 相符 | 允許 request;其餘來源回覆 403 |
Apache2 的 Trusted Proxy List 與來源限制共用同一批 CIDR,但判斷對象不同:RemoteIPTrustedProxyList 控制 Header trust,CONN_REMOTE_ADDR 控制實際 connection peer。
完成後重新執行兩項測試:正常 Cloudflare request 必須成功;VM 本機偽造 CF-Connecting-IP 的 request 必須回覆 403。
設定邊界與常見錯誤
Application 不應再次解析未驗證 Header
Nginx $remote_addr、Apache2 %a 或 Application 的 REMOTE_ADDR,應是 trusted proxy 驗證後的結果。若 Framework 又直接取用 X-Forwarded-For 第一個值,可能重新引入 Header spoofing。
Visitor IP 適合用於 log、異常偵測、Rate Limit 輸入與 GeoIP,但不應作為永久帳號識別或唯一授權條件。NAT、VPN、行動網路與共用 Proxy 都會讓多位使用者共用 IP,IPv6 privacy address 也可能改變。
Cloudflare Workers 與多層 Proxy
本文假設 Cloudflare 後方直接連到 Nginx 或 Apache2。若中間還有 Google Cloud Load Balancer、HAProxy、Ingress Controller、Service Mesh 或另一層 CDN,Web Server 的 TCP peer 將不再是 Cloudflare Edge;此時 trusted proxy chain、Header 覆寫位置與 X-Forwarded-For 解析方向都必須重新設計。
Cloudflare Workers subrequest 也有額外的 CF-Connecting-IP 行為。使用 Worker 改寫 request 或跨 zone 呼叫時,應依 Cloudflare Header 文件重新確認,不可直接套用本文的單層模型。
IPv4 與 IPv6 必須同時維護
Updater 會同時寫入 Cloudflare IPv4 與 IPv6 CIDR。即使 Origin 目前只有 IPv4,保留 IPv6 trusted list 不會自行建立 IPv6 listener;未來啟用 IPv6 時則不會漏掉 Proxy trust。
set_real_ip_from 0.0.0.0/0、::/0,或把整個 VPC、所有 RFC 1918 網段加入 trusted list,都會擴大 Header 的信任來源。只有實際會轉送 Cloudflare request 的 peer 才應列入。
Full (strict) 不處理 Client IP
Full (strict) 驗證的是 Cloudflare 連到 Origin 時,Origin 提供的 certificate 是否有效;它不負責還原 Visitor IP,也不限制誰能提供 CF-Connecting-IP。Client IP、來源限制與 Origin TLS 是三個不同控制點。
完成設定後,Origin 應能同時回答:
誰實際連進來? → peer IP
Cloudflare 代表誰轉送? → visitor IP
誰有權提供 visitor IP? → trusted proxy list