在最近一次网站部署中,考虑到安全因素,希望将源站完全隐藏在 CF 后,只允许 CF 访问,并尽可能的减少信息泄漏。
获取真实客户端 IP
由于经过 CloudFlare 代理后,Nginx 看到的 remote ip
其实是 CF 的 IP 地址。因此需要通过 Nginx 的 ngx_http_realip_module
,还原出真实的客户端 IP。这一步在 CloudFlare 有详细的文档说明。
简单来说,当发现请求 IP 为 CF 的 IP 段时,读取 CF-Connecting-IP
头,并将其中的 IP 设置为客户端 IP。CF 的 IP 段可以通过 API 获取。以下是一个脚本可以生成相关的配置:
#!/bin/bash
# 脚本:获取 Cloudflare IP 列表并生成 Nginx 配置
# 输出文件
IP_CONFIG_FILE="cf_ips.conf"
# 清空输出文件
> "$IP_CONFIG_FILE"
# Cloudflare IP 列表 API 端点
CLOUDFLARE_API_URL="https://api.cloudflare.com/client/v4/ips"
# 生成 set_real_ip_from 配置
echo "# Cloudflare IPv4 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv4_cidrs[]' | while read -r ip; do
echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done
echo -e "\n# Cloudflare IPv6 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv6_cidrs[]' | while read -r ip; do
echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done
# 添加 real_ip_header 配置
echo -e "\n# Set real IP header" >> "$IP_CONFIG_FILE"
echo "real_ip_header CF-Connecting-IP;" >> "$IP_CONFIG_FILE"
脚本运行后会生成 cf_ips.conf
文件,内含 set_real_ip_from
和 real_ip_header
配置,在 http
段 include
该文件,即可自动为所有 server
自动设置真实客户端 IP。
限制仅 CloudFlare IP 访问
解决了真实客户端 IP 的问题后,接下来就是如何做到仅限 CF IP 访问。由于该 Nginx 托管了多个 server
,因此并不希望直接从防火墙层面拒绝所有非 CF IP 访问。那么从 Nginx 侧,第一时间想到的就是用 ngx_http_access_module
的方式,仅放过 CF IP 段:
# Cloudflare IPv4 Allow
allow 173.245.48.0/20;
allow 103.21.244.0/22;
allow 103.22.200.0/22;
allow 103.31.4.0/22;
allow 141.101.64.0/18;
allow 108.162.192.0/18;
allow 190.93.240.0/20;
allow 188.114.96.0/20;
allow 197.234.240.0/22;
allow 198.41.128.0/17;
allow 162.158.0.0/15;
allow 104.16.0.0/13;
allow 104.24.0.0/14;
allow 172.64.0.0/13;
allow 131.0.72.0/22;
# Cloudflare IPv6 Allow
allow 2400:cb00::/32;
allow 2606:4700::/32;
allow 2803:f800::/32;
allow 2405:b500::/32;
allow 2405:8100::/32;
allow 2a06:98c0::/29;
allow 2c0f:f248::/32;
# Default deny
deny all;
但是添加了这段配置后,发现不论是否通过 CF 访问,所有请求均会被拦截,返回 403。搜索相关文章1后发现,这是因为 ngx_http_realip_module
已经先执行完毕了,此时 ngx_http_access_module
拿到的已经是被修改了的真实客户端 IP,自然无法通过检测,被拒绝访问。
为了解决该问题,我们需要使用其他手段来进行访问控制。查阅相关资料后发现,ngx_http_geo_module
是个非常好的选择。相关配置如下:
geo $realip_remote_addr $is_cf {
default 0;
# Cloudflare IPv4 Allow
173.245.48.0/20 1;
103.21.244.0/22 1;
103.22.200.0/22 1;
103.31.4.0/22 1;
141.101.64.0/18 1;
108.162.192.0/18 1;
190.93.240.0/20 1;
188.114.96.0/20 1;
197.234.240.0/22 1;
198.41.128.0/17 1;
162.158.0.0/15 1;
104.16.0.0/13 1;
104.24.0.0/14 1;
172.64.0.0/13 1;
131.0.72.0/22 1;
# Cloudflare IPv6 Allow
2400:cb00::/32 1;
2606:4700::/32 1;
2803:f800::/32 1;
2405:b500::/32 1;
2405:8100::/32 1;
2a06:98c0::/29 1;
2c0f:f248::/32 1;
}
其中,$realip_remote_addr
是 ngx_http_realip_module
提供的变量,存储了原始的客户端地址。通过 CF 访问的情况下,这个变量存储的就是原始的 CF 地址。$is_cf
是用于储存中间结果的变量。如果原始客户端地址在 CF IP 段,则会被设置为 1,否则为 0。
有了这个变量之后,我们就可以使用 ngx_http_rewrite_module
来进行判断,并拒绝访问。在需要的地方增加如下代码:
if ($is_cf = 0) {
return 444;
}
即可拒绝掉所有非 CF 访问的情况。
使用 OpenResty
如果在使用 OpenResty,那么有更简单的方式2可以做这个事情。借助 access_by_lua_block
,我们可以直接在 lua 里访问相关变量进行判断,并拒绝非 CF 的访问:
access_by_lua_block {
if ngx.var.remote_addr == ngx.var.realip_remote_addr then
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
原理其实也很简单,如果 ngx.var.remote_addr
和 ngx.var.realip_remote_addr
是相同的,则证明 ngx_http_realip_module
没有起作用,那么说明原始访问 IP 一定不在 CF IP 段内。
注意事项
- 有条件的情况下(即 Nginx 所有 server 都托管在 CF),应使用防火墙直接拒绝所有非 CF IP 的 TCP 链接
- 由于
if
指令只能使用在server
,location
使用,因此该方法在多个server
内,需要重复配置 - 由于
ssl_reject_handshake
无法在if
内使用,因此如果攻击者猜对了 server_name,那么证书信息依然会泄漏,存在被探测到源站的可能