配置Fail2ban使用FreeBSD的pf或Cloudflare

起因

使用Fail2ban已经有一段时间了,效果还是不错,但是在FreeBSD上和/或在Cloudflare后面时会有一些问题。

Cloudflare的问题是很显然,因为请求过来的是CDN服务器的地址,而不是实际客户端的地址,虽然可以通过real_ip来取得实际客户端的地址,但无法ban掉它……因为它不并直接连到我们的服务器上,需要通过Cloudflare的API让CDN去ban它。

FreeBSD则是pf的问题。因为在FreeBSD上配置了Fail2ban以后发现似乎没有生效,看了日志才发现原来action调用pfctl失败了,所以研究了一下问题在哪里。

但是对pf还是不够熟,毕竟现在iptables用得多,所以有些命令不是太了解,先研究了一下:

pf的日常用法

最基本的就是启用pf,跟一般服务差不多,都是在/etc/rc.conf里加:

pf_enable="YES"
pflog_enable="YES"
gateway_enable="YES"

不过我实际上只加了pf_enable。

启动停止可以通过服务命令,也可以用pfctl:

pfctl -e  # enabled
pfctl -d  # disabled
pfctl -vnf /etc/pf.conf  # 检查规则但不实际加载
pfctl -Fa -f /etc/pf.conf  # 删除所有规则并重新加载(与iptables的重启效果不同,此操作SSH将中断)
pfctl -sr / -sn / -ss / -sA / -sT / -sS  # 显示规则表,NAT表,状态表,锚表,地址表,源表
pfctl -t name -Tk / -Tf / -Ta / -Td / -Ts  # 地址表操作,name表名,删除表,刷新表,表中增加、删除、显示地址

Fail2ban日志里显示的用法要复杂一些,用到了Anchor,然后通过在这个anchor下添加table来实现精细过滤。稍微研究了一下觉得没有必要搞这么精细,只要粗放地把IP封掉就可以了,所以自己作了一下改造。

pf配置

将自带的/usr/local/etc/fail2ban/action.d/pf.conf复制一个pf-all.conf,作了一些修改,去掉注释后的内容如下:

[Definition]
actionstart =
actionstart_on_demand = false
actionstop = <pfctl> -sr 2>/dev/null | grep -v <tablename> | <pfctl> -f-
             %(actionflush)s
actionflush = <pfctl> -t <tablename> -T flush
actioncheck = <pfctl> -sr | grep -q <tablename>
actionban = <pfctl> -t <tablename> -T add <ip>
actionunban = <pfctl> -t <tablename> -T delete <ip>
pfctl = pfctl
[Init]
tablename = f2b
block = block in quick
protocol = tcp
actiontype = <allports>
allports = any
multiport = any port $port

主要修改了几个地方:

  • actionstart去掉了
  • actionstop去掉了删除表的操作
  • 所有的<tablename>-<name>都改为统一的<tablename>
  • pfctl去掉了anchor参数
  • actiontype改为allports

原来是在启动时会对不同的过滤器创建不同的表,然后把要ban的IP放到对应的表里,停止的时候把相应的表删除。

现在改为只有一个f2b表,手工添加到/etc/pf.conf里:

table <f2b> persist
block in quick from <f2b> to any

然后重新加载pf,记得用pfctl -sr检查一下。

动作时就简单地把IP添加进去,让pf把这个IP的所有请求全部ban掉。

实践后效果很好。

Cloudflare API配置

使用CDN需要处理的问题有两个:一个是真实IP获取,一个是调API封IP。

真实IP获取可以参见《Nginx增加基于地理位置的日志和限制》,使用Nginx的realip模块。

因为这台机器使用的是Apache,虽然也有相应的mod_cloudflare之类,但还是为了省事简单地使用自定义日志格式:

LogFormat "%{CF-Connecting-IP}i %l %u %t \"%r\" %>s %B \"%{Referer}i\" \"%{User-Agent}i\" %I %O \"%{X-Forwarded-For}i\" %{Host}i %D" cfproxy

注意,走CloudFlare的网站日志必须单独记录,不要跟不走CloudFlare的网站混合,因为需要用不同的Jail来监控。

然后就是API的处理。

Fail2ban自带了一个action用于CloudFlare的API,只要登录CloudFlare的管理页面,即可取得一个全局的Token,加上你的登录用户邮箱,配置到etc/fail2ban/action.d/cloudflare.conf里的cfusercftoken即可。

使用方法就是创建单独的Jail监控相应的日志文件,然后在这个Jail里使用cloudflare这个action。

不过个人觉得用全局token有点不太安全,所以我是单独创建了一个zone级别的token(可以选择授权给多个或全部的zones),然后用zone级别的API来调用,所以需要修改action配置。

复制cloudflare.confcloudflare-zone.conf,然后修改内容如下(已去除注释):

[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
            -d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
            <_cf_api_url>
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
                   "<_cf_api_url>?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1&notes=Fail2Ban%%20<name>" \
                   | { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
              curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"
_cf_api_url = https://api.cloudflare.com/client/v4/zones/<zone_id>/firewall/access_rules/rules
_cf_api_prms = -H 'Authorization: Bearer <cftoken>' -H 'Content-Type: application/json'
[Init]
cftoken = >>>your_token<<<

其实就修改了几个地方:

  • 去掉了cfuser,只需要cftoken即可
  • 修改cftoken的请求头_cf_api_prms,因为全局token和zone级token用法不同
  • 修改了_cf_api_url,使用zone级别的API路径

Jail里调用action时需要带上zone_id参数,zone_id显示在CloudFlare的zone管理页面右侧下部位置:

action = cloudflare-zone[zone_id=>>>your_zone_id<<<]

另外,复制jail时要注意同时修改jail名称(不只是文件名称)。

推送到[go4pro.org]