套了 Cloudflare 之后网站 520、日志里全是 CF 的 IP:一次完整排查复盘
个人站长几乎没有不套 Cloudflare 的——免费、有 CDN、有基础 DDoS 防护、隐藏源站 IP。但"套上"和"用对"是两回事。我用 CF 这么多年,被问得最多的三个问题是:为什么 SSL 模式选 Flexible 之后无限重定向、为什么 Nginx 访问日志里全是 172.71.x.x 这种 IP、为什么后台看到所有访客的 IP 都一样导致封禁功能失效。这三个问题的根因其实是同一个:你没有告诉 Nginx "我这个站是站在代理后面的"。
SSL 模式选错会导致重定向死循环
Cloudflare 的 SSL/TLS 模式有四档,很多人只记得"要选 Full (Strict)",但不知道选错会具体发生什么。
- Off:CF 到源站是明文 HTTP,用户到 CF 是 HTTP。基本不用,除非你压根没证书。
- Flexible:用户到 CF 走 HTTPS,CF 到源站走 HTTP。这是最危险的选项。
- Full:两段都加密,但 CF 不校验源站证书是否有效(自签也认)。
- Full (Strict):两段都加密,且 CF 校验源站证书必须有效、未过期、域名匹配。正确选择。
Flexible 模式下的经典事故是这样发生的:你的源站 Nginx 上配了 HTTP 到 HTTPS 的强制跳转,比如:
server {
listen 80;
server_name www.example.com;
return 301 https://$host$request_uri;
}而 CF 在 Flexible 模式下回源时用的是 HTTP。于是循环成立:浏览器请求 https://www.example.com → CF 以 HTTP 回源 → Nginx 看到是 HTTP,返回 301 跳到 HTTPS → CF 把 301 原样返回给浏览器(或者自己再发起一次 HTTP 回源)→ 无限循环,最终浏览器报 ERR_TOO_MANY_REDIRECTS。
解决方案有两个,任选其一:
- 把 SSL 模式改成 Full (Strict),让 CF 用 HTTPS 回源,源站的跳转规则就不会被触发。这是推荐做法,前提是源站有有效证书(Let's Encrypt 完全够用)。
- 如果因为某些原因必须用 Flexible(比如源站实在没法上证书),那就在源站判断请求是否来自 CF,来自 CF 的请求不做跳转:
server {
listen 80;
server_name www.example.com;
# 如果请求来自 Cloudflare 回源,且 X-Forwarded-Proto 是 http,直接放行不跳转
if ($http_x_forwarded_proto = "http") {
# 这就是 CF 的回源请求,不要再跳 HTTPS 了
set $skip_redirect 1;
}
if ($skip_redirect != 1) {
return 301 https://$host$request_uri;
}
}不过说实话,第二种方案本质上是在给错误的架构打补丁。只要源站能申请 Let's Encrypt 证书(免费,用 acme.sh 或 certbot 三分钟搞定),就应该直接上 Full (Strict)。这里顺带提一个配套设置:把 "Always Use HTTPS" 打开,同时关掉源站的 80 跳转逻辑(或者只在非 CF 来源时跳),让重定向的责任集中在 CF 这一层。
为什么访问日志里全是 172.71.x.x
套了 CF 之后,所有进入源站的连接都来自 Cloudflare 的边缘节点 IP,所以 Nginx 的 $remote_addr 看到的是 CF 的 IP,而不是真实访客。这是预期行为,不是 bug。真实的访客 IP 在 CF 加的一个请求头里:CF-Connecting-IP。
要恢复真实 IP,需要 Nginx 的 ngx_http_realip_module 模块。先确认编译进去了:
nginx -V 2>&1 | tr ' ' '\n' | grep realipDebian/Ubuntu 官方源里的 Nginx 默认都带这个模块,通常不用自己编译。然后配置:
# 定义可信代理来源(CF 的 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;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
# 从哪个头里取真实 IP
real_ip_header CF-Connecting-IP;
# 可选:如果链路里还有一层自己的反代,用递归模式
# real_ip_recursive on;关键点有几个:
第一,set_real_ip_from 必须准确且完整。如果你只写了几个常见段,漏掉了某些,从漏掉的那些 IP 进来的请求,真实 IP 就不会被还原——但更危险的是反过来:如果你的 set_real_ip_from 写得太宽(比如写了 0.0.0.0/0),那么任何人都可以通过伪造 CF-Connecting-IP 头来假冒 IP,你的封禁系统、限流系统、甚至后台的"仅允许我的 IP 登录"都会瞬间失效。这是一个真实存在的严重安全漏洞,务必避免。
第二,CF 的 IP 段会变。上面这份列表是某个时间点的快照,CF 官方会不定期更新。正确做法是定期从官方接口拉取并生成配置:
#!/bin/bash
# 自动从 Cloudflare 官方获取最新 IP 段并生成 include 文件
CF4=$(curl -s https://www.cloudflare.com/ips-v4)
CF6=$(curl -s https://www.cloudflare.com/ips-v6)
{
echo "# auto-generated $(date +%F)"
echo "$CF4" | sed 's/^/set_real_ip_from /;s/$/;/'
echo "$CF6" | sed 's/^/set_real_ip_from /;s/$/;/'
echo "real_ip_header CF-Connecting-IP;"
} > /etc/nginx/conf.d/cloudflare-realip.conf
nginx -t && nginx -s reload放进 crontab 每周跑一次。注意 nginx -t 一定要在 reload 之前,否则生成的文件有语法错误会让 Nginx 拒绝重载(还好不会导致服务中断)。
第三,如果 CDN 不止一层。比如你前面是 CF,CF 回源到你自建的 Gost 转发节点,再转到源站。这种情况下最后那一跳的 $remote_addr 是 Gost 节点的 IP,而 Gost 通常会帮你透传 CF-Connecting-IP。此时要把 Gost 节点的 IP 也加进 set_real_ip_from,并开启 real_ip_recursive on;,让 Nginx 沿着 X-Forwarded-For 往前找。
日志格式也要同步改造
有了真实 IP,日志格式里还要把它输出出来,否则后续分析还是抓瞎。定义一个包含真实 IP 的日志格式:
log_format main_realip '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'cf=$http_cf_connecting_ip rt=$request_time '
'urt=$upstream_response_time';
access_log /var/log/nginx/access.log main_realip;这样每行日志里既有真实的 $remote_addr(已被 realip 模块改写),也保留了原始的 CF-Connecting-IP 头用于交叉验证。如果这两个值不一致,通常说明 realip 配置有问题,或者有人在伪造头。
配套的加固:如何真正隐藏源站 IP
既然用了 CF 就是为了隐藏源站 IP,那下面这些泄漏渠道必须堵住:
1. 只允许 CF 的 IP 访问源站。在防火墙上限制 80/443 端口只对 CF 段开放,这样即使有人通过 DNS 历史记录拿到了你的源站 IP,也连不上:
# 用 ipset + iptables 批量放行 CF 段
ipset create cf4 hash:net
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do ipset add cf4 "$ip"; done
iptables -I INPUT -p tcp --dport 443 -m set --match-set cf4 src -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j DROP注意:这个操作非常危险,一定要先确认你的 SSH 端口(22 或自定义)没有被这条 DROP 规则误伤,且规则要在会话中先测试再加 -A。更稳妥的做法是用 iptables-save 备份当前规则,或者先用 at 定时任务设一个"10 分钟后自动恢复规则"的保险,确认没问题再取消。
2. 检查证书透明度日志(CT Logs)。你为源站申请的 Let's Encrypt 证书会被记录在 CT Log 里,任何人搜你的域名都能看到,进而可能通过历史 DNS 解析查到源站 IP。所以更彻底的方案是:源站用 CF 签发的 Origin Certificate(15 年有效期,只被 CF 信任,不进入公开 CT Log)。在 CF 后台 SSL/TLS → Origin Server 里生成,装到源站 Nginx 即可。
3. 别忘了 MX、邮件头、以及你自己在其他地方暴露的信息。有时候源站 IP 不是被扫描出来的,是你自己在某个技术论坛的发帖、或者域名注册的 whois 信息里暴露的。
4. 防止通过子域名绕过。CF 只保护了通过它解析的域名。如果你还有 direct.example.com 直接解析到源站 IP,那就等于开了后门。检查所有 DNS 记录,把所有指向源站的 A 记录都套上 CF 代理(橙色云朵)。一条漏网之鱼就够了。
验证真实 IP 是否生效
改完之后必须实测,不能凭感觉。用 curl 从外部请求,然后去 Nginx 日志里看记录的是谁:
# 请求一次
curl -s -o /dev/null https://www.example.com/
# 看日志最后一行的 IP(应该是你自己的公网 IP,而不是 CF 的)
tail -1 /var/log/nginx/access.log更严格的验证是伪造一个 CF-Connecting-IP 头直接请求源站 IP(绕过 CF),看会不会被记录成伪造的值:
curl -s -o /dev/null -H "CF-Connecting-IP: 1.2.3.4" http://YOUR_ORIGIN_IP/ -H "Host: www.example.com"
tail -1 /var/log/nginx/access.log如果日志里出现了 1.2.3.4,说明你的源站可以被任意伪造 IP 欺骗,必须立刻检查:要么 set_real_ip_from 范围太宽,要么防火墙没有限制源站只能被 CF 访问。这两个问题通常同时出现,修掉第一个(收窄 IP 白名单)就能解决。
顺带说一个 Wordpress / Typecho 后台的坑
很多站长会在后台配"IP 白名单"来保护 /admin,套了 CF 之后这个功能会失效——因为后台看到的 IP 全是 CF 的。修好 realip 之后这个问题自然解决,但还有一个更隐蔽的情况:有些程序读的是 X-Forwarded-For 而不是 CF-Connecting-IP。X-Forwarded-For 是一个可以被客户端追加的头,格式是逗号分隔的 IP 链,最左边是客户端声称的 IP,最右边是最后一级代理记录的 IP。如果你的程序直接取第一个值来判断用户 IP,攻击者只要发一个带伪造 XFF 的请求就能绕过。正确的取值逻辑是从右往左数,跳过所有可信代理,第一个不可信的 IP 才是真实客户端 IP。这就是 real_ip_recursive on; 做的事情。所以建议同时下发两个头,兼容不同程序:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;注意 $proxy_add_x_forwarded_for 会把客户端传来的 XFF 原样保留再追加 $remote_addr,这正是标准做法。千万别用 $http_x_forwarded_for 直接透传,那等于把伪造权交给客户端。
小结
Cloudflare 套上之后的三个高频问题——重定向循环、日志 IP 不对、封禁功能失效——根因都是"源站不知道自己身处代理之后"。修法是三件事:SSL 模式改成 Full (Strict),用 real_ip_module 配合 CF 官方 IP 段还原真实 IP,再用防火墙把源站限制为只接受 CF 回源。做完整套之后,你的封禁、限流、访问统计才真正有意义。最后再强调一遍那个安全红线:set_real_ip_from 绝不能写 0.0.0.0/0,那等于给所有人发了一张伪造 IP 的通行证。