Nginx主动防御配置实战:构建Cloudflare后的第二道安全防线

📅 2026/7/19 5:45:41 👁️ 阅读次数 📝 编程学习
Nginx主动防御配置实战:构建Cloudflare后的第二道安全防线

1. 项目概述:为什么你的网站需要“双保险”?

如果你正在使用 Cloudflare 作为 CDN 和防护盾,并且后端服务器运行着 Nginx,那么恭喜你,你已经站在了一个非常稳固的起点上。但很多朋友可能就止步于此了:在 Nginx 里配置一下set_real_ip_fromreal_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 头,如AcceptAccept-LanguageAccept-Encoding等。而许多自动化工具发起的请求可能会缺失这些头,或者其值非常规。
  • 针对特定漏洞的扫描路径:攻击者会批量请求诸如/wp-admin/,/phpmyadmin/,/config.json,.env等常见的管理后台、配置文件路径,试探是否存在未授权访问或已知漏洞。

2.2 Nginx 防御策略分层

基于以上特征,我们可以在 Nginx 构建一个分层的防御体系:

  1. 连接层限制:控制单个 IP 的连接数和请求速率,这是应对 CC 洪水最直接的手段。
  2. 请求特征过滤:根据 User-Agent、请求头等特征,直接拦截掉明显的恶意请求。
  3. 路径与资源保护:屏蔽对敏感路径的访问,并对消耗资源的特定接口(如登录、搜索)实施更严格的频率限制。
  4. 验证与质询:对于可疑但不确认的请求,可以返回一个简单的验证(如 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.confconf.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_zonelimit_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应用在serverlocation块。可以为不同的路径设置不同的zonerate

注意事项:速率限制的数值需要根据实际业务流量进行压测和调整。设置过严会误伤正常用户(尤其在秒杀、抢购场景),设置过松则起不到防护作用。建议先在日志中观察正常用户的请求频率分布。

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 等复杂逻辑,尽量使用mapserver块匹配、location嵌套等方式替代。
  • 所有基于正则表达式的匹配(~,~*)都有性能开销,规则越多越复杂,性能影响越大。应将最可能匹配的、最严格的规则放在前面。

6. 常见问题与排查技巧实录

6.1 限流规则不生效?

  • 检查真实 IP 获取:确保$binary_remote_addr已经是用户真实 IP。查看日志确认。
  • 检查作用域limit_req_zonelimit_conn_zone必须定义在http块内。limit_reqlimit_conn应用在serverlocation块。
  • 检查配置语法:使用nginx -t测试配置文件语法。
  • 检查共享内存区大小:如果 IP 数量非常多,10m 可能不够,观察 Nginx 错误日志是否有limiting connections相关的zone空间不足的警告。

6.2 误伤了正常用户怎么办?

  • 调整阈值:这是最直接的方法。观察正常业务高峰期的请求频率,将rateburst值设置得比峰值稍高一些。
  • 使用白名单:为已知的、可信的 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 averagenginx进程的 CPU 和内存使用情况,以及访问日志中$limit_req_status的变化。

6.5 动态黑名单的维护手动维护黑名单不现实。一个简单的自动化思路是:

  1. 定期(如每分钟)扫描 Nginx 日志,找出在短时间内(如10秒)返回 429/503 状态码超过 N 次(如50次)的 IP。
  2. 将这些 IP 写入一个文件(如/etc/nginx/blockips.conf),格式为deny IP;
  3. 在 Nginx 配置的http块中,通过include /etc/nginx/blockips.conf;引入。
  4. 执行nginx -s reload使配置生效(注意:频繁 reload 有性能损耗,生产环境慎用)。
  5. 可以设置一个过期时间,比如将封禁 IP 写入文件时记录时间戳,另一个定时任务清理超过一定时间(如24小时)的封禁记录。

这套组合拳打下来,你的网站从 Nginx 层面就已经具备了相当可观的主动防御能力。它不能替代专业的 WAF 或云防护服务,但作为一道成本极低、可控性极高的内部防线,能在 Cloudflare 之外为你提供至关重要的冗余安全保障。安全配置永远是一个动态调整的过程,持续观察日志、分析攻击模式、微调规则,才是长治久安之道。