1. 说明
网上搜了一下关于Nginx自动封禁IP的教程,基本从一个博客中复制的,而且还有问题,这里重新写一篇关于IP自动封禁的
思路是配置一个conf文件存放封禁的IP,然后编写一个脚本从Nginx访问日志access.log中提取封禁的IP,然后存放在conf中
2. IP封禁原理
nginx默认支持IP封禁的,使用语法为
1 2 3 4 5 6 7 8 9 10 11 12 13
| # 禁止访问 deny 192.168.1.1; #禁止一个网段访问 deny 192.168.2.0/24; #允许访问 allow 192.168.1.4; #允许一个网段访问 allow 192.168.3.0/24;
#允许所有 allow all; # 禁止所有 deny all;
|
如果遇到了冲突,以最前面的为准
配置在http模块或者server中均可,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| http { deny 192.168.2.0/24; server { location / { deny 192.168.1.1; } } }
|
为了使得黑名单IP在一起统一表示,一般我们使用include标签引入一个带有deny ${ip};
列表的文件,然后在文件中添加或删除黑名单IP;
如:
1 2 3 4 5 6 7
| http { include ip_block.conf; server { } }
|
然后ip_block.conf
中按照特定格式,保存IP列表
1 2 3 4 5
| deny 192.168.1.1; deny 192.168.2.0/24; deny 192.168.3.0/24; deny 192.168.4.0/24; deny 192.168.5.0/24;
|
3. 自动封禁IP配置
自动封禁的原理是解析nginx的请求日志,然后判断请求的IP频率,在时间内超过一定次数的就执行封禁,这样可以防止恶意的请求。
在Nginx中,access.log中保存有请求IP的信息等,可以利用此解析出IP地址列表
然后把列表添加至ip_block.conf
中,这样就保证了动态IP的添加
3.1. 配置日志格式
因为要从日志中解析IP,首先要配置日志格式,nginx默认的格式不太好解析时间,需要重新配置时间
在http模块下配置日志格式,主要配置成 $time_iso8601 时间格式2024-08-22T09:30:13+08:00
这里把时间配置到最前面,方便解析。主要把$time_iso8601 - $remote_addr
放在前面,后面的随意配置。
1 2 3 4 5 6
| http { log_format main '$time_iso8601 - $remote_addr - $remote_user "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; }
|
注意: 改了日志格式建议把access.log清理了,要不后续awk切分会不正确
3.2. 解析日志并加入黑名单脚本
这里解析了1分钟内访问超过60次的IP和查php路径的,因为我项目本身没有php,使用了这些请求的多半是非法探测请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| #!/bin/bash
NGINX_DIR='/etc/nginx' LOG_DIR="/var/log/nginx" ACCESS_LOG=${LOG_DIR}/access.log IP_BLOCK=$NGINX_DIR/ip_block.conf IP_TASK_LOG=$LOG_DIR/ip_task.log # 那肯定得一直封着呀 # echo "" > ${NGINX_DIR}/ip_block.conf
#前面最开始编写的统计数据功能 # 2024-08-21T17:15:01+08:00 - 101.204.24.165 - - # 根据`- :T+`拆分, 然后组合成`时间戳 ip`的形式" | 筛选最近60s的数据,并且只保留IP | 排序 | 按IP统计 group_count=$(awk -F '[- :T+]' '{print mktime($1" "$2" "$3" "$4" "$5" "$6) " "$11}' $ACCESS_LOG | awk '{if($1>systime()-60) print $2}' | sort | uniq -cd )
#判断这个变量是否为空 if test -z "$group_count" then #为空则啥也不干 echo "运行IP封禁为空--`date '+%Y%m%d-%H%M%S'`" echo "运行IP封禁为空--`date '+%Y-%m-%d %H:%M:%S'`" >> $IP_TASK_LOG else # 不为空则将IP禁止并记录log # 记录日志 deny_ip_list=$( echo "$group_count" | awk '{if($1>60)print "deny " $2 ";"}') # 筛选大于60次的,并配置好格式 `deny ip;` ip_list=$(echo "$group_count" | awk '{if($1>60)printf $2"-"$1"次 "}') echo "运行IP封禁不为空--`date '+%Y-%m-%d %H:%M:%S'` -- ${ip_list}" echo "运行IP封禁不为空--`date '+%Y-%m-%d %H:%M:%S'` -- ${ip_list}" >> $IP_TASK_LOG # 追加写入封禁数据 echo "${deny_ip_list}" >> $IP_BLOCK fi
echo "准备拦截php路径" # 拦截所有的找password.php的,这就是试探木马 # group_count_by_str=$(cat $ACCESS_LOG | grep .php | awk -F '[- :T+]' '{print mktime($1" "$2" "$3" "$4" "$5" "$6) " "$11}' | awk '{if($1>systime()-120) print $2}' | sort | uniq -cd | sed 's/^[ ]*//g' | sed 's/[ ]*$//g' | grep -v "^$") # 最后的sed是去除首尾空格的 group_count_by_str=$(cat $ACCESS_LOG | grep .php | awk -F '[- :T+]' '{print mktime($1" "$2" "$3" "$4" "$5" "$6) " "$11}' | awk '{if($1>systime()-120) print $2}' | sort | uniq -c) if test -z "$group_count_by_str" then echo "运行php探测 IP封禁为空--`date '+%Y-%m-%d %H:%M:%S'`" echo "运行php探测 IP封禁为空--`date '+%Y-%m-%d %H:%M:%S'`" >> $IP_TASK_LOG else deny_ip_list_by_keyword=$(echo "$group_count_by_str" | awk '{print "deny " $2 ";"}') ip_list_by_keyword=$(echo "$group_count_by_str" | awk '{printf $2"-"$1"次 "}') echo "运行php探测 IP封禁不为空--`date '+%Y-%m-%d %H:%M:%S'` -- ${ip_list_by_keyword}" echo "运行php探测 IP封禁不为空--`date '+%Y-%m-%d %H:%M:%S'` -- ${ip_list_by_keyword}" >> $IP_TASK_LOG # 追加写入封禁数据 echo "${deny_ip_list_by_keyword}" >> $IP_BLOCK fi
# 执行 ip_block.conf去重
#ips=$(cat $IP_BLOCK | sort | uniq -cd | awk '{print $2" "$3}') #echo "ips: $ips" #echo "$ips" > $IP_BLOCK
#重启ngnix nginx -s reload
echo "脚本执行完成"
|
没有ip_block.conf
文件和ip_task.log
则自行创建一个空的文件touch ip_block.conf
和touch /var/log/nginx/ip_task.log
3.2.1. 解析命令 awk说明
原始日志
1 2 3 4
| 2024-08-21T17:15:01+08:00 - 101.204.24.165 - - "GET / HTTP/1.1" 403 555 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" "-" 2024-08-21T17:15:01+08:00 - 101.204.24.165 - - "GET /favicon.ico HTTP/1.1" 403 555 "http://auth.liukewen.cn/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" "-" 2024-08-21T17:15:01+08:00 - 101.204.24.165 - - "GET / HTTP/1.1" 403 555 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" "-" 2024-08-21T17:15:01+08:00 - 101.204.24.165 - - "GET /favicon.ico HTTP/1.1" 403 555 "http://auth.liukewen.cn/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" "-"
|
执行按照时间拆分
awk -F '[- :T+]' '{print mktime($1" "$2" "$3" "$4" "$5" "$6) " "$11}' /var/log/nginx/access.log
1 2 3 4
| 1724231601 220.181.108.177 1724231700 101.204.24.165 1724232075 101.204.24.165 1724232075 101.204.24.165
|
awk -F '[- :T+]' '{print mktime($1" "$2" "$3" "$4" "$5" "$6) " "$11}' /var/log/nginx/access.log | awk '{if($1>systime()-60) print $1" "$2}'
$1>systime()-60 表示匹配近期60s
1 2 3
| 1724231601 220.181.108.177 1724232185 101.204.24.165 1724232185 101.204.24.165
|
排序并分组统计
awk -F '[- :T+]' '{print mktime($1" "$2" "$3" "$4" "$5" "$6) " "$11}' /var/log/nginx/access.log | awk '{if($1>systime()-60) print $1" "$2}' | sort | uniq -cd
1 2
| 1 220.181.108.177 62 101.204.24.165
|
判断大于60次的IP
awk -F '[- :T+]' '{print mktime($1" "$2" "$3" "$4" "$5" "$6) " "$11}' /var/log/nginx/access.log | awk '{if($1>systime()-60) print $1" "$2}' | sort | uniq -cd | awk '{if($1>60)print "deny " $2 ";"}'
这样,就得到了需要的格式。
3.3. corntab定时任务
新建一个corntab任务
crontab -e
1 2
| # 执行脚本,每分钟都执行 * * * * * /etc/nginx/ip_block.sh
|
然后执行重启服务
这样就完成了
引用文章:
awk按照条件字符拆分
linux中awk命令比较时间
awk中的三个时间函数
awk筛选给定时间范围内的日志