Nginx fail2ban:个人站点 DDOS 攻击生存指南

摘要

为了防范潜在的 DDOS 攻击,避免经济损失,我在近期升级了本博客的服务器。更新后,服务器实装了 Nginx 自建的 HTTP 限流模块,配合 fail2ban 封杀恶意 IP,可以有效抵御大量并发请求对服务器的干扰。

Nginx 部分

在 Nginx 上配置 HTTP 限流模块,分为两个步骤:

  1. 创建一个管理区域(以下称 zone)
  2. 将 zone 应用于指定的文件路径(location)

我曾在文章 本博客现已启用全站 HTTPS 加密通讯 中贴出了配置文件 nginx.conf 的代码片段。简明起见,那篇博客省略了配置文件的根节点。实际上,整个文件的结构是这样子的:

# 进程设定 ...  http {     # 逻辑服务器设定     server {         listen 80;         ...         # 文件路径映射         location / {             root /var/www/html;         }         ...     }     server {         listen 443 ssl;         ...     }      # 其它设定 -- TCP SSL 日志 压缩等     ... } 

Nginx 的每个 zone 记录了一套管理规则。这个规则会在开启了该 zone 的 location 上生效。也就是说,应在“其它设定”下面声明 zone,并在“文件路径映射”那里引用它。

来看两个 zone 的声明的例子。

limit_req_zone $binary_remote_addr zone=perip:10m rate=30r/m; 

第一个例子中, limit_req_zone 表明这条语句声明一个限流 zone。 $binary_remote_addr 是 Nginx 内置的变量,用来表示客户端的 IP 地址。它决定了这个 zone 会以客户端 IP 作为控制条件。 perip 是 zone 的名字。冒号后的 10m 表示这个 zone 最大可占用的内存空间。zone 要存储包括 IP 地址在内的客户端状态信息。根据 官方文档 ,状态信息在 32 位机上占用 64 字节,而在 64 位机上占用 128 字节。对我的 64 位服务器而言,10 MB 的内存理论上支持 8 万个并发连接,是绰绰有余的。最后 30r/m 规定,每个 IP 地址的平均请求速度不得大于每分钟 30 次。因为不支持小数的缘故,这里不能写作 0.5r/s

limit_req_zone $server_name zone=perserver:10m rate=100r/s; 

第二个例子使用了内置变量 $server_name ,使得这个 zone 作用于 Nginx 自己。它的意思是,当某个逻辑服务器的平均请求速度大于每秒 100 次时,将不再受理新的请求。

在 location 内引用一个 zone,请参考下面的例子。

limit_req zone=perip burst=5 nodelay; 

这条指令为当前的 location 启用 perip zone。尽管 perip 限制了平均请求速度,Nginx 依旧允许客户端并发创建 5 个连接,以适应现代浏览器的需要。过载后,开启了 nodelay 的 Nginx 将立即向客户端返回错误码。如果没有 nodelay 选项,Nginx 会故意延迟响应客户端的请求,以便将其响应请求的平均速度拉低到限定值以下。因为在这种情况下,服务器仍然需要消耗内存记录尚未响应的请求,所以对付 DDOS 攻击一定要开启 nodelay 选项。

在声明 zone 的位置,同时可以自定义返回的状态码。默认返回的状态码是 503 服务暂时不可用。我建议通过 limit_req_status 444; 将它替换成 444 无回应断开连接。444 是一个由 Nginx 引入的非标准 HTTP 状态码,它从字面上将拒绝服务的责任从服务器端 (5xx) 转移到客户端 (4xx),更加真实地反映了客观情况。

如果 Nginx 服务器流量超限,我们可以在 error 日志中找到这样的记录:

... limiting requests, excess:..., client:..., server:..., request:..., host:... 

Nginx 的限流仅仅可以停止响应服务,但客户端依然能不断地发送 TCP 请求建立新的连接。这时需要 fail2ban 上场彻底阻断恶意客户端的魔爪。fail2ban 的工作,就是从监控日志开始的。

fail2ban 部分

fail2ban 可在各 Linux 发行版的包管理器中安装。它的过滤规则位于 /etc/fail2ban/filter.d/ 路径下。新安装的 fail2ban 已经设置好了多项规则,我们只要照着模板完成 nginx-http-limit-req.conf 文件即可(文件名任取)。

# nginx-http-limit-req.conf  [Definition]  failregex = limiting requests, excess:.*client: <HOST>  ignoreregex = 

failregex 描述了 fail2ban 期望匹配的特征。 .* 用于匹配中间的任意多个字符。重点是 client: <HOST> ,fail2ban 依此来抓取客户端的 IP 地址。

之后,需要注册并启用这条规则。因为原始的配置文件 /etc/fail2ban/jail.conf 会随着版本更新而被覆盖,我们要创建一个副本 /etc/fail2ban/jail.local ,并在其中填写配置。在文件的最后插入下面这段内容:

[nginx-http-limit-req]  enabled = true port = http,https logpath = %(nginx_error_log)s findtime = 600 maxretry = 5 bantime = 7200 

注意,标签要与过滤规则的文件名相同。在默认状态,所有规则都是禁用的,我们需要单独启用想要加载的规则。fail2ban 运行后,会自动监测 %(nginx_error_log)s 文件,如果在 600 秒的时间内( findtime )连续 5 次( maxretry )发现客户端的请求速度超过限额,则在将来的 7200 秒内( bantime )禁止该 IP 到服务器 http 和 https 端口的连接。 findtimemaxretry 都使用了默认值,因此那两行也可以不写。

一切就绪后,运行 sudo service fail2ban restart 重载 fail2ban。日志 /var/log/fail2ban.log 中记录了攻击者的 IP 地址。可惜它们通常是肉鸡,不值得打回去。总而言之,咱们服务器管理员要牢记的一点是:网络世界并不太平。

为了防范潜在的 DDOS 攻击,避免经济损失,我在近期升级了本博客的服务器。更新后,服务器实装了 Nginx 自建的 HTTP 限流模块,配合 fail2ban 封杀恶意 IP,可以有效抵御大量并发请求对服务器的干扰。

Nginx 部分

在 Nginx 上配置 HTTP 限流模块,分为两个步骤:

  1. 创建一个管理区域(以下称 zone)
  2. 将 zone 应用于指定的文件路径(location)

我曾在文章 本博客现已启用全站 HTTPS 加密通讯 中贴出了配置文件 nginx.conf 的代码片段。简明起见,那篇博客省略了配置文件的根节点。实际上,整个文件的结构是这样子的:

# 进程设定 ...  http {     # 逻辑服务器设定     server {         listen 80;         ...         # 文件路径映射         location / {             root /var/www/html;         }         ...     }     server {         listen 443 ssl;         ...     }      # 其它设定 -- TCP SSL 日志 压缩等     ... } 

Nginx 的每个 zone 记录了一套管理规则。这个规则会在开启了该 zone 的 location 上生效。也就是说,应在“其它设定”下面声明 zone,并在“文件路径映射”那里引用它。

来看两个 zone 的声明的例子。

limit_req_zone $binary_remote_addr zone=perip:10m rate=30r/m; 

第一个例子中, limit_req_zone 表明这条语句声明一个限流 zone。 $binary_remote_addr 是 Nginx 内置的变量,用来表示客户端的 IP 地址。它决定了这个 zone 会以客户端 IP 作为控制条件。 perip 是 zone 的名字。冒号后的 10m 表示这个 zone 最大可占用的内存空间。zone 要存储包括 IP 地址在内的客户端状态信息。根据 官方文档 ,状态信息在 32 位机上占用 64 字节,而在 64 位机上占用 128 字节。对我的 64 位服务器而言,10 MB 的内存理论上支持 8 万个并发连接,是绰绰有余的。最后 30r/m 规定,每个 IP 地址的平均请求速度不得大于每分钟 30 次。因为不支持小数的缘故,这里不能写作 0.5r/s

limit_req_zone $server_name zone=perserver:10m rate=100r/s; 

第二个例子使用了内置变量 $server_name ,使得这个 zone 作用于 Nginx 自己。它的意思是,当某个逻辑服务器的平均请求速度大于每秒 100 次时,将不再受理新的请求。

在 location 内引用一个 zone,请参考下面的例子。

limit_req zone=perip burst=5 nodelay; 

这条指令为当前的 location 启用 perip zone。尽管 perip 限制了平均请求速度,Nginx 依旧允许客户端并发创建 5 个连接,以适应现代浏览器的需要。过载后,开启了 nodelay 的 Nginx 将立即向客户端返回错误码。如果没有 nodelay 选项,Nginx 会故意延迟响应客户端的请求,以便将其响应请求的平均速度拉低到限定值以下。因为在这种情况下,服务器仍然需要消耗内存记录尚未响应的请求,所以对付 DDOS 攻击一定要开启 nodelay 选项。

在声明 zone 的位置,同时可以自定义返回的状态码。默认返回的状态码是 503 服务暂时不可用。我建议通过 limit_req_status 444; 将它替换成 444 无回应断开连接。444 是一个由 Nginx 引入的非标准 HTTP 状态码,它从字面上将拒绝服务的责任从服务器端 (5xx) 转移到客户端 (4xx),更加真实地反映了客观情况。

如果 Nginx 服务器流量超限,我们可以在 error 日志中找到这样的记录:

... limiting requests, excess:..., client:..., server:..., request:..., host:... 

Nginx 的限流仅仅可以停止响应服务,但客户端依然能不断地发送 TCP 请求建立新的连接。这时需要 fail2ban 上场彻底阻断恶意客户端的魔爪。fail2ban 的工作,就是从监控日志开始的。

fail2ban 部分

fail2ban 可在各 Linux 发行版的包管理器中安装。它的过滤规则位于 /etc/fail2ban/filter.d/ 路径下。新安装的 fail2ban 已经设置好了多项规则,我们只要照着模板完成 nginx-http-limit-req.conf 文件即可(文件名任取)。

# nginx-http-limit-req.conf  [Definition]  failregex = limiting requests, excess:.*client: <HOST>  ignoreregex = 

failregex 描述了 fail2ban 期望匹配的特征。 .* 用于匹配中间的任意多个字符。重点是 client: <HOST> ,fail2ban 依此来抓取客户端的 IP 地址。

之后,需要注册并启用这条规则。因为原始的配置文件 /etc/fail2ban/jail.conf 会随着版本更新而被覆盖,我们要创建一个副本 /etc/fail2ban/jail.local ,并在其中填写配置。在文件的最后插入下面这段内容:

[nginx-http-limit-req]  enabled = true port = http,https logpath = %(nginx_error_log)s findtime = 600 maxretry = 5 bantime = 7200 

注意,标签要与过滤规则的文件名相同。在默认状态,所有规则都是禁用的,我们需要单独启用想要加载的规则。fail2ban 运行后,会自动监测 %(nginx_error_log)s 文件,如果在 600 秒的时间内( findtime )连续 5 次( maxretry )发现客户端的请求速度超过限额,则在将来的 7200 秒内( bantime )禁止该 IP 到服务器 http 和 https 端口的连接。 findtimemaxretry 都使用了默认值,因此那两行也可以不写。

一切就绪后,运行 sudo service fail2ban restart 重载 fail2ban。日志 /var/log/fail2ban.log 中记录了攻击者的 IP 地址。可惜它们通常是肉鸡,不值得打回去。总而言之,咱们服务器管理员要牢记的一点是:网络世界并不太平。

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: