Nginx主动防御配置实战:构建Cloudflare后的第二道安全防线
1. 项目概述:为什么你的网站需要“双保险”?
如果你正在使用 Cloudflare 作为 CDN 和防护盾,并且后端服务器运行着 Nginx,那么恭喜你,你已经站在了一个非常稳固的起点上。但很多朋友可能就止步于此了:在 Nginx 里配置一下set_real_ip_from和real_ip_header,让日志能记录到访客的真实 IP,就觉得万事大吉。这其实只完成了安全加固的第一步,相当于只给大门装了把锁,却没给窗户上栓。
Cloudflare 提供了强大的边缘安全能力,比如它的 WAF(Web 应用防火墙)和速率限制。但安全永远是纵深防御。将一部分关键的安全策略下沉到你的源站 Nginx 上,能带来几个核心好处:第一,减轻对单一服务商的绝对依赖,即使 Cloudflare 的某个规则临时失效或配置有误,你源站的防线依然在;第二,应对那些可能绕过 CDN、直接攻击你源站 IP 的“聪明”攻击者;第三,对于一些精细化的、业务特定的防护需求(比如针对某个特定 API 接口的频次限制),在 Nginx 层实现往往更灵活、响应更迅速。
所以,这个“双保险”策略的核心思想是:利用 Cloudflare 做第一道、也是覆盖面最广的防线,同时用 Nginx 构建第二道、更贴近业务的精细化防线。今天要聊的,就是除了获取真实 IP 之外,那些能实实在在帮你挡住爬虫、缓解甚至阻止 CC(Challenge Collapsar,即 HTTP 洪水)攻击的 Nginx 配置。这些配置大多不复杂,但组合起来,效果显著。
2. 核心思路拆解:从被动记录到主动防御
在深入配置之前,我们先理清防御的逻辑。攻击和爬虫的行为通常有迹可循,我们的配置就是针对这些“痕迹”设置关卡。
2.1 识别攻击与爬虫的常见特征
- 高频请求(CC攻击的典型特征):在极短时间内,从一个或少量 IP 向同一个 URL(特别是动态页面、登录接口、搜索接口)发起大量请求,意图耗尽服务器资源(CPU、数据库连接、带宽)。
- 非常规 User-Agent:一些低级爬虫或扫描器会使用明显异常、缺失或默认的 User-Agent 字符串(如
python-requests/2.28.2,Go-http-client/1.1, 或干脆为空)。 - 缺失关键请求头:正常的浏览器请求会携带一系列标准的 HTTP 头,如
Accept、Accept-Language、Accept-Encoding等。而许多自动化工具发起的请求可能会缺失这些头,或者其值非常规。 - 针对特定漏洞的扫描路径:攻击者会批量请求诸如
/wp-admin/,/phpmyadmin/,/config.json,.env等常见的管理后台、配置文件路径,试探是否存在未授权访问或已知漏洞。
2.2 Nginx 防御策略分层
基于以上特征,我们可以在 Nginx 构建一个分层的防御体系:
- 连接层限制:控制单个 IP 的连接数和请求速率,这是应对 CC 洪水最直接的手段。
- 请求特征过滤:根据 User-Agent、请求头等特征,直接拦截掉明显的恶意请求。
- 路径与资源保护:屏蔽对敏感路径的访问,并对消耗资源的特定接口(如登录、搜索)实施更严格的频率限制。
- 验证与质询:对于可疑但不确认的请求,可以返回一个简单的验证(如 429 状态码告知稍后重试),或者结合更复杂的验证码方案(但这通常需要后端应用配合)。
接下来,我们就进入实操环节,看看如何用 Nginx 的配置实现这些策略。
3. 基础环境与关键配置准备
在开始布置“机关”之前,确保你的 Nginx 已经正确配置了从 Cloudflare 接收真实 IP,这是所有后续 IP 相关限制的基础。
3.1 获取并配置 Cloudflare IP 列表
Cloudflare 的 IP 段是公开的,并且会更新。我们需要告诉 Nginx,来自这些 IP 的请求头中的CF-Connecting-IP才是真实用户 IP。 通常,你可以在 Nginx 配置的http块内引入这个 IP 列表。最可靠的方式是从 Cloudflare 官方获取。
# 进入你的 Nginx 配置目录,例如 /etc/nginx cd /etc/nginx # 下载 Cloudflare 的 IPv4 和 IPv6 地址列表 curl -s https://www.cloudflare.com/ips-v4 -o cf-ips-v4.txt curl -s https://www.cloudflare.com/ips-v6 -o cf-ips-v6.txt然后,在你的nginx.conf或conf.d/下的某个配置文件(如real-ip.conf)中,进行如下配置:
# 定义从 Cloudflare IP 来的请求,使用 CF-Connecting-IP 头作为真实IP # 包含 IPv4 列表 include /etc/nginx/cf-ips-v4.txt; # 包含 IPv6 列表 include /etc/nginx/cf-ips-v6.txt; # 注意:上面两行需要配合一个自定义的配置文件内容。 # 更常见的做法是,将下载的 IP 列表内容,通过 `set_real_ip_from` 指令逐个(或通过文件包含)声明。 # 这里提供一个更实用的方法,使用 `geo` 模块配合 `map` 来灵活处理: geo $realip_remote_addr $cloudflare_ip { default 0; # 你可以手动添加几个关键段,或者用脚本将下载的列表格式化成如下样式: # 103.21.244.0/22 1; # 103.22.200.0/22 1; # ... 所有 Cloudflare IP 段 # 也可以动态包含,但 Nginx 原生不支持 include 变量,通常需要借助 OpenResty 或定期更新配置文件。 } # 一个更直接、兼容性更好的方法是,在 http 块中使用多个 set_real_ip_from # 例如,在 http 块中: real_ip_header CF-Connecting-IP; # 然后为每个 Cloudflare IP 段添加 set_real_ip_from set_real_ip_from 103.21.244.0/22; set_real_ip_from 103.22.200.0/22; # ... 添加所有 IP 段 # 为了维护方便,建议使用自动化脚本定期更新这个配置文件。实操心得:维护 Cloudflare IP 列表是个小麻烦。我个人的做法是写一个简单的 Shell 脚本,用
cron每周自动从 Cloudflare 官网拉取最新列表,并格式化后重写到 Nginx 的配置片段(如/etc/nginx/cloudflare-ips.conf),然后在主配置中用include指令引入。这样就能确保 IP 列表始终最新。
3.2 确认真实 IP 获取成功
配置完成后,重载 Nginx (nginx -s reload),然后检查你的访问日志(例如/var/log/nginx/access.log)。日志格式中$remote_addr现在应该显示的是用户的真实 IP,而不是 Cloudflare 的边缘节点 IP。这是后续所有基于 IP 的限流和过滤生效的前提。
4. 主动防御配置实战
现在,我们开始构筑核心防线。以下配置通常放在http块或server块中,根据你的需要选择作用域。
4.1 连接层与请求速率限制(应对CC攻击核心)
这是最有效的一招,利用 Nginx 的limit_conn_zone和limit_req_zone模块。
限制同一 IP 并发连接数:防止单个 IP 建立大量连接耗尽服务器资源。
http { # 定义限制连接数的共享内存区,键为真实IP,大小10MB,该区域可以记录约16万个IP状态 limit_conn_zone $binary_remote_addr zone=perip_conn:10m; server { location / { # 每个 IP 在同一时间最多允许 10 个连接 limit_conn perip_conn 10; # 当超过限制时,返回 503 (Service Temporarily Unavailable) 错误 limit_conn_status 503; ... # 其他配置 } } }$binary_remote_addr是经过前面real_ip模块处理后的真实客户端 IP 的二进制格式,占用空间更小。zone=perip_conn:10m定义了一个名为perip_conn的共享内存区,大小为 10MB。根据经验,1MB 大约可以存储 1.6 万个 IP 的状态信息,10MB 对于大多数站点足够了。limit_conn perip_conn 10;在location中应用限制。
限制同一 IP 请求速率:这是防 CC 的关键,限制每秒/每分钟能处理的请求数。
http { # 定义限制请求速率的共享内存区,键为真实IP,大小10MB,速率限制为每秒10个请求(r/s) limit_req_zone $binary_remote_addr zone=perip_req:10m rate=10r/s; server { location / { # 应用限流,使用“漏桶”算法,突发请求队列大小为5个 limit_req zone=perip_req burst=5 nodelay; limit_req_status 429; # 超过限制时返回 429 (Too Many Requests) ... # 其他配置 } # 对于特别敏感的接口,可以设置更严格的限制 location /api/login { limit_req_zone $binary_remote_addr zone=login_req:10m rate=2r/m; limit_req zone=login_req burst=1 nodelay; limit_req_status 429; } } }rate=10r/s表示平均每秒允许 10 个请求。你可以根据业务承受能力调整,对于纯静态资源可以放宽,对于动态接口要收紧。burst=5允许处理突发流量。假设速率是 10r/s,如果一瞬间来了 15 个请求,前 10 个会被立即处理,接下来的 5 个会进入队列延迟处理(如果设置了nodelay,则会立即处理但占用 burst 额度,超过 burst 则拒绝)。burst设得太高会削弱限流效果,太低可能误伤正常用户的短暂高峰。nodelay参数意味着对于突发队列中的请求,不延迟处理,而是立即处理,但会快速消耗掉 burst 额度。这对于用户体验更友好,但防护力度稍弱。是否需要nodelay需要根据业务权衡。- 特别注意:
limit_req_zone通常定义在http块,而limit_req应用在server或location块。可以为不同的路径设置不同的zone和rate。
注意事项:速率限制的数值需要根据实际业务流量进行压测和调整。设置过严会误伤正常用户(尤其在秒杀、抢购场景),设置过松则起不到防护作用。建议先在日志中观察正常用户的请求频率分布。
4.2 基于请求特征的过滤(拦截低阶爬虫与扫描器)
屏蔽异常或空 User-Agent:
server { # 如果 User-Agent 为空、为常见爬虫工具标识,则直接返回 444(Nginx 直接关闭连接) if ($http_user_agent ~* “^$|python|java|curl|wget|go-http|^java|httpclient|apachebench|^$”) { return 444; } # 更精确的做法是,只允许常见的浏览器和良性爬虫(如各大搜索引擎) # if ($http_user_agent !~* “(chrome|firefox|safari|msie|edge|bingbot|googlebot|baiduspider|yandex|sogou)” ) { # return 444; # } # 注意:白名单策略过于激进,可能会挡住一些合法的非浏览器客户端(如 API 调用),请谨慎使用。 }- 使用
~*进行不区分大小写的正则匹配。 return 444;是 Nginx 的一个特殊状态码,表示无条件关闭连接,不发送任何响应头,对于攻击者来说最“省资源”。
- 使用
屏蔽缺失关键请求头的请求:
server { # 检查 Accept 头,正常的浏览器请求都会包含它 if ($http_accept = “”) { return 444; } # 可以同时检查多个头,但要注意某些场景下(如图片 src 请求)可能缺失某些头 # if ($http_accept_language = “” ) { # return 444; # } }
实操心得:
if指令在 Nginx 的location上下文中有一些性能陷阱和副作用(例如可能导致try_files等指令失效),但它用于简单的返回操作(如return 444,return 403)通常是安全且高效的。对于复杂的逻辑判断,建议使用map指令。
4.3 敏感路径与资源保护
屏蔽对常见敏感文件和目录的访问:
server { location ~* ^/(\.git|\.env|\.svn|\.htaccess|wp-admin|phpmyadmin|admin|backup|config|sql|\.bak)$ { deny all; return 404; # 或者 403 } # 屏蔽对特定后缀文件的直接访问 location ~* \.(log|ini|conf|sql|tar|gz)$ { deny all; return 404; } }- 使用正则表达式
~*匹配这些路径,直接deny all。
- 使用正则表达式
对高消耗接口实施精准限流: 如前文所示,可以为
/api/login,/api/search,/submit_comment等动态接口单独设置更严格的limit_req规则。这比全局限流更有效,不影响静态资源的正常访问。
4.4 利用map指令创建黑白名单
map指令比if更高效,适合创建映射关系,例如根据 IP 或 User-Agent 设置一个变量。
http { # 创建一个 IP 黑名单映射 map $binary_remote_addr $is_blacklisted_ip { default 0; # 将已知恶意 IP 设为 1 123.123.123.123 1; 111.111.111.0/24 1; # 支持 CIDR 格式的网段 # 可以从文件加载,但需要 reload 配置 # include /etc/nginx/blocked_ips.conf; } # 创建一个恶意 User-Agent 映射 map $http_user_agent $is_bad_ua { default 0; ~*”scanner|hack|exploit” 1; “~*” 是一个正则匹配操作符 } server { # 在 server 或 location 中判断 if ($is_blacklisted_ip) { return 444; } if ($is_bad_ua) { return 444; } } }维护一个动态的 IP 黑名单是高级玩法。你可以编写一个脚本,分析 Nginx 日志,将短时间内触发大量 429/503 错误的 IP,或者匹配了恶意请求特征的 IP,自动追加到黑名单配置文件中,然后让 Nginx 重载配置。这实现了简单的自动化封禁。
5. 高级策略与日志分析
5.1 设置日志格式记录限流信息
为了监控限流是否生效,以及分析攻击情况,我们需要在日志中记录相关变量。
http { log_format main ‘$remote_addr - $remote_user [$time_local] “$request” ‘ ‘$status $body_bytes_sent “$http_referer” ‘ ‘“$http_user_agent” “$http_x_forwarded_for” ‘ ‘“$limit_req_status” “$limit_conn_status”‘; # $limit_req_status 记录请求限流状态:PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN 等 # $limit_conn_status 记录连接限流状态:PASSED, REJECTED 等 access_log /var/log/nginx/access.log main; }这样,在日志中就能清晰地看到哪些请求被限流了(状态为 REJECTED),便于后续分析和调整阈值。
5.2 结合ngx_http_geo_module按国家/地区限制
如果你的业务只针对特定地区,可以屏蔽高风险地区的 IP 访问。 首先,你需要一个 IP 地理信息数据库(如 MaxMind 的 GeoLite2)。安装相应模块和数据库后,可以配置:
http { geoip2 /path/to/GeoLite2-Country.mmdb { $geoip2_country_code country iso_code; } map $geoip2_country_code $allowed_country { default yes; CN yes; # 允许中国 US yes; # 允许美国 RU no; # 禁止俄罗斯 # … 其他地区 } server { if ($allowed_country = no) { return 444; } } }5.3 谨慎使用if与性能考量
正如之前提到的,Nginx 的if指令在其上下文中是“邪恶”的,它可能会破坏其他指令的预期行为。最佳实践是:
- 对于简单的返回操作(
return 444,return 403),if是安全的。 - 对于设置变量、重写 URI 等复杂逻辑,尽量使用
map、server块匹配、location嵌套等方式替代。 - 所有基于正则表达式的匹配(
~,~*)都有性能开销,规则越多越复杂,性能影响越大。应将最可能匹配的、最严格的规则放在前面。
6. 常见问题与排查技巧实录
6.1 限流规则不生效?
- 检查真实 IP 获取:确保
$binary_remote_addr已经是用户真实 IP。查看日志确认。 - 检查作用域:
limit_req_zone和limit_conn_zone必须定义在http块内。limit_req和limit_conn应用在server或location块。 - 检查配置语法:使用
nginx -t测试配置文件语法。 - 检查共享内存区大小:如果 IP 数量非常多,10m 可能不够,观察 Nginx 错误日志是否有
limiting connections相关的zone空间不足的警告。
6.2 误伤了正常用户怎么办?
- 调整阈值:这是最直接的方法。观察正常业务高峰期的请求频率,将
rate和burst值设置得比峰值稍高一些。 - 使用白名单:为已知的、可信的 IP 或 API 网关 IP 设置白名单,绕过限流规则。可以通过
map指令设置一个$is_trusted变量,在location中判断if ($is_trusted) { limit_req off; }。 - 分层限流:不要全局一刀切。对静态资源(如图片、CSS、JS)放宽或取消限流,只对动态 API 接口进行严格限制。
6.3 如何应对分布式 CC 攻击?单个 IP 限流对分布式攻击(海量不同 IP)效果有限。这时需要结合:
- Cloudflare 的防护:开启 Cloudflare 的 Under Attack 模式或调整其 WAF 规则,利用其全球网络的优势进行清洗。
- Nginx 层面:可以考虑更复杂的策略,如限制每个 IP 对特定 URL 的访问速率(上文已提),或者使用
limit_req_zone的键改为$server_name$request_uri来限制针对某个具体页面的总并发请求(但这会影响所有用户)。更高级的方案需要集成 Lua 模块(OpenResty)或结合外部防火墙(如 Fail2ban)进行动态封禁。
6.4 配置完成后如何测试?可以使用工具如ab(Apache Benchmark) 或wrk从本地机器进行简单的压力测试,观察是否会触发 429 或 503 错误。
# 示例:在10秒内,并发100,对目标URL发起请求 ab -t 10 -c 100 http://your-website.com/api/test同时,监控服务器的load average、nginx进程的 CPU 和内存使用情况,以及访问日志中$limit_req_status的变化。
6.5 动态黑名单的维护手动维护黑名单不现实。一个简单的自动化思路是:
- 定期(如每分钟)扫描 Nginx 日志,找出在短时间内(如10秒)返回 429/503 状态码超过 N 次(如50次)的 IP。
- 将这些 IP 写入一个文件(如
/etc/nginx/blockips.conf),格式为deny IP;。 - 在 Nginx 配置的
http块中,通过include /etc/nginx/blockips.conf;引入。 - 执行
nginx -s reload使配置生效(注意:频繁 reload 有性能损耗,生产环境慎用)。 - 可以设置一个过期时间,比如将封禁 IP 写入文件时记录时间戳,另一个定时任务清理超过一定时间(如24小时)的封禁记录。
这套组合拳打下来,你的网站从 Nginx 层面就已经具备了相当可观的主动防御能力。它不能替代专业的 WAF 或云防护服务,但作为一道成本极低、可控性极高的内部防线,能在 Cloudflare 之外为你提供至关重要的冗余安全保障。安全配置永远是一个动态调整的过程,持续观察日志、分析攻击模式、微调规则,才是长治久安之道。
