前段时间网站被恶意访问(DDos 或 CC 攻击)导致网站宕机,虽然原先使用 sh 脚本写了个规则,按时把恶意访问的 IP 放到 Nginx 的 Deny 列表中,但是这种方法是亡羊补牢之法,需要等待访问过后,过滤 error.log 后才能得到 Deny 名单。所以,当网络上的小流氓频繁使坏的时候,还没等来的及过滤日志,网站就挂了。这就需要加一个主动防御方案了。

主动防御:给网站添加访问限制

强大的 Nginx 提供了如下所示的两个模块,结合着使用正好可以满足限制访问的需要:

主动防御的思路是这样的。每个 IP 每秒钟同时最多只能建立两个连接,超过则返回 503 (Service Temporarily Unavailable) 错误。并且,每个 IP 每秒不超过 1 个请求,漏桶数 (burst) 为 5。这样就可以把那些每秒钟发送超多请求的 IP 给拒之门外了。

另外,为了不影响 SEO,防止限制搜索引擎蜘蛛(如 Googlebot、bingbot、Baiduspider 等)的抓取,以及防止自己指定的 IP 也被限制,还需要设置一个白名单。具体实现代码如下:

http{

    ...

    #白名单,生效必须kill nginx再启动
    #访问限制排除搜索引擎
    map $http_user_agent $perip{
        default $binary_remote_addr;
        ~*(Baiduspider|bingbot|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Yahoo!\ Slurp|qihoobot|YoudaoBot|Sosospider|Sogou\ spider|Sogou\ web\ spider|MSNBot) "";
    }

     #限制连接
    limit_conn_zone $perip zone=conn_perip:10m; #访客IP地址库conn_perip
    limit_req_zone $perip zone=req_perip:10m rate=1r/s; #访客IP地址库req_perip,请求速率1个/秒

    #定义排除IP
    map $remote_addr $limited{
        default 0;
        #需要排出限制的 IP
        127.0.0.1 1;
    }

    ...

    server{

        #...

        #全站限制访问参数
        limit_rate_after 200k; #当传输量大于此值时,超出部分将限速传送
        limit_rate 100k; #超出的限制速度

        #访问限制排除定义的IP
        if ($limited) {
            set $perip '';
        }

        location / {

            #...

            #禁止代理访问
            if ($http_user_agent ~ must-revalidate) {
                return 403;
            }

            #目录限制访问设置
            limit_conn conn_perip 2; #访客IP只能建立2个连接
            limit_req zone=req_perip burst=5 nodelay; #访客IP最高5个/秒突发请求速率

            #...

        }

        location ~ \.php$ {

            #PHP限制访问设置
            limit_conn conn_perip 2;
            limit_req zone=req_perip burst=5 nodelay;

            #...

        }

    #...

    }
}

被动防御:按照规则封锁恶意 IP

除了主动防御,如果有需要,还可以加一个被动防御,也就是本文开头所说的,通过过滤 error.log 获取频繁恶意访问网站的 IP 并加入 Nginx 的 Deny 名单。

实现思路是,当恶意访问的 IP 被主动防御拒绝后会产生 503 错误并记录到 error.log 中,可以编写两个 sh 脚本并加入 Crontab 定时任务中,按时(如 10 分钟)过滤一次,发现出现超如规定次数的 IP(如 20 次)则自动加入 Deny 名单。Deny 名单按时(如 1 天)清空。具体实现代码如下:

文件:block-ip.sh

#!/bin/bash

#从错误日志读取访问超过指定数量IP并将其屏蔽,重启Nginx

ERR_LOG=/usr/local/nginx/logs/error.log
BLOCK_IP_FILE=/usr/local/nginx/conf/blockips.conf
BLOCKED_IP=/usr/local/nginx/conf/blocked-ip.txt
BLOCK_IP=/usr/local/nginx/conf/block-ip.txt
NGINX_CMD=/usr/local/nginx/sbin/nginx

#把屏蔽列表备份到blocked-ip.txt文件
#从错误日志中提取恶意访问IP记录到block-ip.txt文件
#清空错误日志
#重启Nginx

cat $BLOCK_IP_FILE > $BLOCKED_IP &&
/bin/sed -nr 's#.*[^0-9](([0-9]+\.){3}[0-9]+).*#\1#p' $ERR_LOG |/bin/awk '{IP[$1]++}END{for (i in IP) print IP[i],i}'|/bin/awk '{if($1>20)print "deny "$2";"}' > $BLOCK_IP &&
/bin/grep -v -f $BLOCK_IP_FILE $BLOCK_IP >> $BLOCK_IP_FILE &&
#cat /dev/null > $ERR_LOG &&
$($NGINX_CMD -s reload)

文件:clear-ip.sh

#!/bin/bash

#清理分割的错误日志文件

CONF_DIR=/usr/local/nginx/conf
BLOCK_FILE=blockips.conf
BLOCKED_FILE=blocked-ip.txt
NGINX_CMD=/usr/local/nginx/sbin/nginx
LOGS_DIR=/usr/local/nginx/logs
LOG_NAME=error.log

cd $LOGS_DIR &&
/usr/bin/rename $LOG_NAME $(/bin/date +%F-%H -d "last hour").$LOG_NAME $LOG_NAME &&
cat /dev/null > $LOG_NAME &&
cd $CONF_DIR &&
cat /dev/null > $BLOCK_FILE &&
cat /dev/null > $BLOCKED_FILE &&
$($NGINX_CMD -s reload)

然后把上面两个文件添加到 crontab 定时任务中:

命令:crontab -e

#每天清空屏蔽列表
0 0 * * * /bin/bash /usr/local/scripts/clear-ip.sh &>/dev/null
#每十分钟检查超过规定的IP,加入屏蔽列表,重启Nginx
*/10 * * * * /bin/bash /usr/local/scripts/block-ip.sh &>/dev/null

这样,双管齐下,就可以把大部分非正常访问拒之门外了。