原理
- Nginx自带的组件
limit_req_zone
可以对单个IP发起的请求进行限速,采用的策略是漏桶(leaky bucket)算法,划定一块内存空间用来建立桶即可使用,开销比较小.初步确定10Requests/s
,对于超出的请求均返回503. - 对非浏览器端发起的API请求拒绝服务,初步的方法是拒绝无Referer或携带除
*.pasteme.cn
与localhost(调试用)
之外的Referer头的请求,均返回403. - 通过
Fail2Ban
组件监视error.log
,提取触发503限速的IP,通过写入iptables拒绝掉流量.从TCP层面过滤流量.
实现
设置Nginx
limit_req_zone $binary_remote_addr zone=saltedfish:10m rate=5r/s;
# 开启limit_req_zone,以remote_address作为分类标准,分配10M内存空间saltedfish,标准速度为5次请求每秒.
server
{
listen 8080;
server_name _;
index index.html;
root /www/wwwroot;
underscores_in_headers on;
gzip_http_version 1.0;
location / {
try_files $uri $uri/ /index.html;
location ~ .*\.(js|css)?$ {
gzip_static on;
}
}
location /_api/backend/ {
#对/_api/backend/ 进行限流,使用上面划分的zone pasteme,允许有5次请求每秒的突发,因此
#限流的速率要求为共10次请求每秒,nodelay表示超过限流就返回503拒绝请求.
limit_req zone=pasteme burst=5 nodelay;
valid_referers localhost pasteme.cn *.pasteme.cn;
if ($invalid_referer) {
return 403;
}
# 对referer 头部进行校验,只允许 localhost/pasteme.cn/*.pasteme.cn三种域名的referer,若无Referer或Referer中不以http(s)起始也算无效,返回403
#Do Sth.....
}
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
{
return 404;
}
access_log /var/lib/logs/access.log;
error_log /var/lib/logs/access.error.log;
#错误log位置
}
重载Nginx即可生效.
安装Fail2Ban
sudo apt-get install fail2ban
增加日志检测规则
vim /etc/fail2ban/filter.d/nginx-req-limit.conf
内容为
# Fail2Ban configuration file
#
# supports: ngx_http_limit_req_module module
[Definition]
failregex = limiting requests, excess:.* by zone.*client: <HOST>
#使用正则匹配,<HOST>部分即为提取出的封禁IP
# Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored.
# Values: TEXT
#
ignoreregex =
可参见
增加封禁任务
vim /etc/fail2ban/jail.local
官方建议使用*.local
文件来覆写同目录下jail.conf
的默认设置.jail.local
内容如下
[nginx-req-limit]
#任务名称 nginx-req-limit
enabled = true
# 启用
filter = nginx-req-limit
#过滤器 即之前新建的filter.d下的nginx-req-limit.conf
action = iptables-multiport[name=ReqLimit, port="http,https,8080", protocol=tcp]
#封禁动作 使用iptables封禁 iptables规则名称为ReqLimit 端口为http,https和8080,协议为tcp,记得要更改为Nginx的监听端口.
logpath = /var/lib/logs/access.error.log
#要检测日志文件路径
findtime = 600
#检测时间为 600秒为一个时间区间
bantime = 7200
#封禁时间 7200s
maxretry = 10
#时间区间内允许的失败次数,这里设置为600秒内超过限流10次即拉黑.
systemctl restart fail2ban
查看日志规则是否启动
tail -f /var/log/fail2ban.log
2020-12-08 19:48:47,423 fail2ban.jail [6163]: INFO Jail 'nginx-req-limit' uses pyinotify {}
2020-12-08 19:48:47,432 fail2ban.jail [6163]: INFO Initiated 'pyinotify' backend
2020-12-08 19:48:47,438 fail2ban.filter [6163]: INFO Added logfile: '/var/lib/pasteme/pasteme.error.log' (pos = 2070, hash = 838cc5f2d47c4bd59219f24814adb669d56e62db)
2020-12-08 19:48:47,439 fail2ban.filter [6163]: INFO encoding: UTF-8
2020-12-08 19:48:47,440 fail2ban.filter [6163]: INFO maxRetry: 10
2020-12-08 19:48:47,441 fail2ban.filter [6163]: INFO findtime: 600
2020-12-08 19:48:47,441 fail2ban.actions [6163]: INFO banTime: 7200
2020-12-08 19:48:47,445 fail2ban.jail [6163]: INFO Jail 'sshd' started #默认会自动启动对ssh尝试失败的封禁
2020-12-08 19:48:47,456 fail2ban.jail [6163]: INFO Jail 'nginx-req-limit' started #已启动
2020-12-08 19:48:47,649 fail2ban.actions [6163]: NOTICE [nginx-req-limit] Restore Ban 192.168.124.238
或者使用fail2ban-client
fail2ban-client status nginx-req-limit
Status for the jail: nginx-req-limit
|- Filter
| |- Currently failed: 0 #fail指检测到日志中的超限记录条数
| |- Total failed: 0
| `- File list: /var/lib/logs/access.error.log
`- Actions
|- Currently banned: 1 #ban掉的IP
|- Total banned: 1
`- Banned IP list: 192.168.124.238
如何解封
fail2ban-client set <规则名称> unbanip <IP>
fail2ban-client set nginx-req-limit unbanip 192.168.124.238
验证Fail2Ban效果
Fail2Ban
的核心在于改写iptables规则.执行iptables -L
你会看到filter表中的所有规则链,Fail2Ban
默认会新建一条规则链,并在INPUT
的最顶层引用它.而每一个封禁都是一条REJECT记录.
已知问题
Docker容器中开放端口的流量不受过滤.
Docker默认的子网组成方式是使用
Bridge模式
,在iptables的nat表中就转发入Docker容器.而Fail2ban的规则链添加在filter的INPUT链中,通往容器的数据包在NAT的Forward链中已经送往Docker容器,无法执行规则.表的处理优先级:raw>mangle>nat>filter.在表中又有5个规则链:PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING.下图表示了一个数据包进入路由表所经过的规则处理链.
解决方案
设置为将规则添加在FORWARD
链中即可.
vim /etc/fail2ban/action.d/iptables-common.conf
# Option: chain
# Notes specifies the iptables chain to which the Fail2Ban rules should be
# added
# Values: STRING Default: INPUT
chain = FORWARD
#将chain由INPUT改为 FORWARD
systemctl restart fail2ban