0x01 前言
我博客的静态资源托管在腾讯CDN,得益于前后端分离的架构,CDN服务器可以直接与后端服务器通讯以获取最新的静态文件。
但不能排除后端服务器被爬虫或访客有意识地访问,因为正常情况下是不会有人主动访问后端服务器的,所以可以大胆地假设:所有非CDN服务器的访问都是非法访问。
同时因为托管静态文件的后端服务器为腾讯云的VPS,所以公网流量会产生高昂的费用。
为此,我决定使用fail2ban对非腾讯CDN服务器的IP进行封禁。
如果你没有接触过fail2ban,我建议先阅读以下文章:
0x02 准备
腾讯CDN服务器的IP肯定是无时无刻都在变化的,但有一个比较明显的特征:
上图中所有的IP几乎都是腾讯云CDN的IP,因为cn2这台服务器仅允许CDN服务器访问。然后通过浏览器访问其中一个IP后,会发现:
响应内容是这样的,而响应头是这样的:
虽然特征比较明显,但直接取值,肯定是不行的,可信度会大大地降低。
但腾讯云提供了CDN IP的查询页面:
如果每次都手动检测,也是比较头疼的一件事,毕竟IP众多,另外手动添加黑名单也不靠谱,效率太低。
其实以上说的都是废话,这种事情肯定需要调用API,在这里我依然选择python SDK,因为我只会python。
接口的相关说明请浏览以下页面:
在这里有一点需要注意的:我写这篇文章和撰写脚本的时候,腾讯云有两个版本的SDK:
- 新版:腾讯云开发者工具套件(SDK)3.0
- 旧版:QcloudApi
新版仅完成了常用的一小部分服务的API调用功能,所以在这里我依旧使用QcloudApi这个旧版的SDK。
另外,新旧版SDK都是支持python3的,所以我会用python3编写脚本。
在继续往下之前,我们需要先安装相关的python模块:
[root@cn2 ~]# pip3 install qcloudapi-sdk-python
完成后即可继续往下:
0x03 脚本
首先需要定义各种预设的变量:
# 模块 module = 'cdn' # 接口 action = 'QueryCdnIp' # 账户信息 secret_id = '' secret_key = '' # 定义config字典 config = { 'Region': 'gz', 'secretId': secret_id, 'secretKey': secret_key, 'method': 'get' } # 定义固定参数,传入IP地址 params = { 'ips': client_ip }
因为是自定义脚本,所以需要符合fail2ban的相关动作需求:
- banip
- unbanip
- start
- stop
基本的动作有以上四个。因为需要操作iptables,所以这四个动作有包含相应的iptables命令。
而banip比较特殊,包含校验访客IP是否为腾讯云CDN的相关脚本。
0x03.1 banip
以下为banip动作的相关脚本:
service = QcloudApi(module, config) qcloud_output = service.call(action, params).decode() qcloud_output = json.loads(qcloud_output) # 校验API调用是否正常 if qcloud_output['code'] == 0: pass else: error_msg = 'pub_error_code: ' + str(qcloud_output['code']) + ' error_msg: ' + qcloud_output['message'] print(error_msg) sys.exit(0) # 校验API调用是否正常 if qcloud_output['codeDesc'] == 'Success': pass else: error_msg = 'codeDesc: ' + str(qcloud_output['code']) + ' error_msg: ' + qcloud_output['message'] print(error_msg) sys.exit(0) # 校验是否为腾讯CDN的IP if qcloud_output['data']['list'][0]['platform'] == 'no': f2b_cmd_1 = '/usr/sbin/iptables -I f2b-' + jail_name + ' 1 -s ' + client_ip + ' -j DROP' else: f2b_cmd_1 = '/usr/bin/fail2ban-client set ' + jail_name + ' unbanip ' + client_ip
首先调用腾讯云的API,腾讯云返还的内容如下:
{ "codeDesc": "Success", "message": "", "data": { "last_update_time": 1526393225, "list": [ { "prov_name": "上海", "platform": "yes", "status": "on", "isp_name": "联通", "pid": 200111, "ip": "140.207.120.100" } ] }, "code": 0 }
如果用户参数异常,则code会返还非0数字,所以在调用API后需要校验。
然后需要校验codeDesc的value是否为Success,如果不是,则表示业务侧出现了错误,这个有可能是没有传递正确的IP或传递了空IP。
最后检查platform这个key是否为yes,如果不是,则调用iptables封禁IP。
其实还需要校验status这个key的,如果为off,则表示CDN节点处于停用状态,这时候也应该进行封禁。但目前没有加上这个key的校验,计划在下个版本中进行修正。
最后有两个命令:
f2b_cmd_1 = '/usr/sbin/iptables -I f2b-' + jail_name + ' 1 -s ' + client_ip + ' -j DROP' f2b_cmd_1 = '/usr/bin/fail2ban-client set ' + jail_name + ' unbanip ' + client_ip
第一行是调用iptables封禁IP的,第二行是将该IP的封禁记录从fail2ban数据库中删除,这个我们放到后面讲。先来解释第一行的iptables命令:
- /usr/sbin/iptables:可执行二进制文件的绝对路径
- -I:插入到链的第一行
- f2b-‘ + jail_name + ‘:以“f2b-”为前缀,加上jail名字组成链的名称
- 1:指定该规则的序号,默认就是1
- -s:来源IP
- -j:采取的动作
整合到一起的意思是:在’f2b-‘ + jail_name这个链的第一行插入拒绝来源IP为client_ip的规则。
例如:
/usr/sbin/iptables -I f2b-log-ngx-20x 1 -s 1.1.1.1 -j DROP
0x03.2 unbanip
f2b_cmd_1 = '/usr/sbin/iptables -D f2b-' + jail_name + ' -s ' + client_ip + ' -j DROP'
unbanip的内容比较简单,就是一行iptables调用命令。
0x03.3 start
启动与停止的命令正好相反,启动动作包含以下内容:
f2b_cmd_1 = '/usr/sbin/iptables -N f2b-' + jail_name f2b_cmd_2 = '/usr/sbin/iptables -A f2b-' + jail_name + ' -j RETURN' f2b_cmd_3 = '/usr/sbin/iptables -I INPUT -j f2b-' + jail_name
以上命令的含义如下:
- f2b_cmd_1:建立名称为f2b- + jail_name的自定义链
- f2b_cmd_2:在f2b- + jail_name链中添加RETURN动作
- f2b_cmd_3:在INPUT链中添加名称为f2b- + jail_name的默认动作
第一行比较容易理解,就是建立一个名称独一无二的链,用于存放该jail被ban的IP;
第二行得意思是,如果在自定义的链中没有命中规则,则返回珠链,也就是INPUT。因为iptables匹配的顺序是子链—>父链—>缺省,所以通过RETURN就可以将数据包丢给父链,在这里是INPUT。
第三行的意思是接收到子链过来的数据包后,需要进行的动作,如果匹配上链名称,则根据定义的动作进行操作,如果没有指定动作,则按默认的动作进行操作。
执行完上面的三条规则后,iptables会有以下内容:
Chain INPUT (policy ACCEPT 515K packets, 31M bytes) pkts bytes target prot opt in out source destination 21M 26G f2b-qcloud-cdn-ip-check all -- * * 0.0.0.0/0 0.0.0.0/0 Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 19M packets, 26G bytes) pkts bytes target prot opt in out source destination Chain f2b-qcloud-cdn-ip-check (1 references) pkts bytes target prot opt in out source destination 21M 26G RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
在这里就不深入探讨iptables相关的内容了,如果有时间,我会另外撰写文章。
0x03.4 stop
以上是启动的命令,如果重启或停止fail2ban,是需要将被ban的IP一个个拉出来,然后删除自定义的链。所以命令如下:
f2b_cmd_1 = '/usr/sbin/iptables -D INPUT -j f2b-' + jail_name f2b_cmd_2 = '/usr/sbin/iptables -F f2b-' + jail_name f2b_cmd_3 = '/usr/sbin/iptables -X f2b-' + jail_name
停止和启动的命令是相反的,首先需要删除INPUT链中相关的规则;然后清空自定义链中的所有规则;最后删除自定义链。
0x03.5 调用
为了方便调用,我将命令组合成列表,然后使用循环进行调用:
f2b_cmd_list = [f2b_cmd_1, f2b_cmd_2, f2b_cmd_3] for i in f2b_cmd_list: if i is None: break else: cmd_output = subprocess.call(i, shell=True) print(cmd_output)
如果命令为空,则停止循环,这样可以保证各个命令得以运行。
0x04 fail2ban
准备好脚本后,我们将它放置在一个地方:
/usr/local/shell/qcloud_cdn_ip_check.py
然后建立jail:
[qcloud-cdn-ip-check] enabled = true port = http,https filter = qcloud-cdn-ip-check logpath = /var/log/nginx/access.log action = qcloud-cdn-ip-check[name=%(__name__)s] %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"] maxretry = 2 findtime = 1800 bantime = 86400 ignoreip = 127.0.0.1
再建立一个filter:
[root@cn2 ~]# cat /etc/fail2ban/filter.d/qcloud-cdn-ip-check.conf [Definition] failregex = <HOST> - - \[.*\] \".*\" cdn\.enginx\.cn .* \d*\s\" ignoreregex =
以上filter仅筛选日志中cdn.enginx.cn这个域名的访客IP。
最后建立action:
[root@cn2 ~]# cat /etc/fail2ban/action.d/qcloud-cdn-ip-check.conf [INCLUDES] [Definition] actionstart = /usr/bin/python3 /usr/local/shell/qcloud_cdn_ip_check.py start None <name> actionstop = /usr/bin/python3 /usr/local/shell/qcloud_cdn_ip_check.py stop None <name> actioncheck = /usr/sbin/iptables -n -L INPUT | grep -q 'f2b-<name>[ \t]' actionban = /usr/bin/python3 /usr/local/shell/qcloud_cdn_ip_check.py banip <ip> <name> actionunban = /usr/bin/python3 /usr/local/shell/qcloud_cdn_ip_check.py unbanip <ip> <name> [Init]
可以看到脚本后面跟着三个参数,方便识别各种动作、传递IP与传递jail名称。
一切准备就绪后,即可启动fail2ban,然后等待恶意访客上门。
0x05 结语
经过一周的使用,效果良好。但该API接口限制每分钟请求不超过一千次,在受到攻击时可能会超过限制,在后期计划建立一个本地json的缓存。
最后,完整的脚本可以在我的私有gitlab中找到: