【38】SSH 防爆破与 Fail2Ban 加固

前情提要

自从两年前购入一个树莓派 4B 后,它一直作为我的本地服务器勤勤恳恳的 吃灰 运行着。为了方便随时随地访问,我在另一台个人云服务器上搭建了 frp 服务,把内网的 SSH 22 端口转发到了一个公网端口上。

直到今年年初偶然一次检查的时候,才发现这个转发出去的 SSH 端口正在被一大批 IP 24 小时持续不断的进行爆破操作,攻击的速度达到了每秒十几次。可把我吓出一身汗,因为那时候还在使用相对较弱的一组用户名和密码。

事后检查 log 的时候发现,auth.log 里满满当当挤了那个月的 *几千万* 条攻击记录(auth.log 最多记录一个月),也就是说这个攻击已经持续了至少一个月以上。攻击的 IP 来源不少都是外网机器,估摸着都是肉鸡或者代理;攻击记录显示出两大种类型:常见用户名爆破以及 root 密码爆破。好在实际正确的用户名在 log 中只出现了寥寥一两百次,对系统仔细检查后也没有发现异样,因此判断尚未被成功爆破。

要知道,我的树莓派不仅是本地服务器,还是一个 NAS,相当多的个人文件被备份在上面。要是被爆破,作为肉鸡是小事,泄露隐私/丢失数据可是大事。

加固 SSH

面对这样力度的爆破,加固 SSH 是在所难免了。常用的加固方法:

  • 更换端口
  • 使用密钥对登录
  • 禁用密码登录,禁用 root 登录
  • Fail2Ban 等自动封禁

对于更换端口,frp 转发出去的已经是一个奇葩高位端口,仍然被轻易探测到并疯狂爆破(后来新配置的其他高位端口也同样分分钟被探查到);而服务器本身的 22 端口虽然也有被攻击的记录,但是远远少于树莓派受到的攻击量。因此可以说更换端口是一个并没有太大用处的方法。

使用密钥对登录才是真正的杀手锏,毕竟普通密码也许可以试出来,私钥试到下下辈子试到沧海桑田也不一定试得出来。在本地执行以下指令(替换邮箱或说明文字)生成使用 ed25519 算法生成的公私密钥对:

1
ssh-keygen -t ed25519 -C "[email protected]"

(推荐使用 ed25519,相比 RSA 更安全,密钥反而还更短)

生成密钥后,可以用这个指令(按需修改密钥位置)添加/修改/删除密钥的密码:

1
ssh-keygen -p -f ~/.ssh/id_ed25519

最后使用这个指令把公钥复制到需要访问的服务器的 ~/.ssh/authorized_keys 中:

1
ssh-copy-id user@host

当然,也可以手动把公钥复制到对应机器的 ~/.ssh/authorized_keys 里。

完成公钥的设置以后,就可以把服务器的密码登录和 root 登录禁用了。打开 /etc/ssh/sshd_config 文件,编辑以下项:

1
2
3
4
ChallengeResponseAuthentication no
PasswordAuthentication no
PermitRootLogin no
UsePAM no

编辑完成后使用 sudo systemctl reload sshd 使配置生效。

使用 Fail2Ban

更多的问题

完成上一节的设置后,你的 SSH 端口可以说已经是相当坚固了。除非存在配置错误或是 SSH Server 存在严重 0-day 漏洞,否则除了你,神仙也进不了你的服务器。

可是这不妨碍攻击者继续攻击。加固 SSH 之后 ,动不动还是会有人来对树莓派的 SSH 端口进行一波上万次的爆破尝试,可以看得出来基本是在遍历一个常用用户名字典。在十分钟之内进行将近两万次攻击,最多的时候每秒 50 次左右的访问,这对于树莓派来说压力非常大,妥妥的是 DDOS 了。

一波不爽之后,决定上最后一层杀手锏,自动封禁。自动封禁工具的原理非常简单,它会自动监视目标服务的 log(对于 SSH 来说就是 /var/log/auth.log),如果一个 IP 在一定时间内失败超过特定次数,就对其进行封禁操作(比如在 iptables / ufw 中封禁对应 IP)。可以用于 SSH 的自动封禁工具五花八门,Fail2BanDenyHostsSSHGuard 等都可以实现对应的功能。我这里选用了较为通用和强大,并且仍有开发组活跃维护的 Fail2Ban。

安装

Fail2Ban 在 Ubuntu / Debian 上的配置相当容易。

首先安装。使用 apt 安装的 Fail2Ban 版本较老,可以选择手动安装/升级最新版本。官方提供了文档:How to install or upgrade fail2ban manually,基本操作如下:

  1. 下载安装包(这里略去文档中提供的验证环节)

    1
    
    wget https://github.com/fail2ban/fail2ban/releases/download/0.11.2/fail2ban_0.11.2-1.upstream1_all.deb
  2. 停止 Fail2Ban 服务(如有)

    1
    
    sudo fail2ban-client stop
  3. 安装刚下载的安装包

    1
    
    sudo dpkg -i fail2ban_0.11.2-1.upstream1_all.deb

这样,就可以用上最新版本的 Fail2Ban 了。

配置

在 Ubuntu / Debian 上,sshd 的 jail 已经默认启用了(/etc/fail2ban/jail.d/defaults-debian.conf)。在 /etc/fail2ban/ 下新建 jail.local 来对默认配置(jail.conf)进行覆盖修改:(不应该直接去修改原本的配置文件)

1
2
3
4
5
6
[sshd]
enabled = true
mode = aggressive
maxretry = 10
findtime = 1h
bantime = 12h

修改完毕后执行 sudo fail2ban-client reload 重载配置。

为了能够抓取到私钥验证失败的 log,这里选用了 aggressive 模式,具体可以看 /etc/fail2ban/filter.d/sshd.conf 中的正则表达式定义与 110 行左右的注释。注意这只在较新的版本中才支持,详见这条 issue#2115 的讨论。

然后为这个 jail 配置了最大失败次数为 10,统计失败的时间窗口大小为 1 小时,封禁时间为 12 小时。注意,一次失败的请求可能会产生不止一条 log,每一条被正则识别到的失败 log 都会被 Fail2Ban 认为是一次失败。为了防止一不小心把自己关在外面,不能设置太小的 maxretry

Extra Juice

分析

上一节使用的 jail.local 配置文件在我搭建 frp 转发服务的云服务器上完美的守护着 SSH 端口。然而对于本地的树莓派来说,这仍然不太够。

首先,frpc 在访问本地 SSH 22 端口的时候,auth.log 中记录的 IP 是 127.0.0.1,而非远程攻击者的 IP。显然由于现在 frp 的限制,远程连接的 IP 元信息并不能很好的传递到本地来。而 Fail2Ban 默认忽略本地 IP,需要修改对应配置。

另外,就算本地 iptables 将 127.0.0.1 封禁,攻击者的访问请求仍然会被转发到本地,然后 frp 再把拒绝信息转发回去,白白消耗服务器流量和带宽。如果能在封禁 IP 的同时,直接暂时停用本地的 frpc 服务,那就最好了。这样一来攻击的请求将直接在云服务器端被拒绝掉,根本不会到达本地。

实现

在 Fail2Ban 中实现前者非常容易,配置中加入 ignoreself = false 即可。

对于后者,需要自己实现一个 action 来执行操作。在 /etc/fail2ban/action.d/ 下新建文件 systemd.local(或是其他你喜欢的名字),在其中定义一个能够自动关闭服务的 action:

 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
# Fail2Ban configuration file
#
# Author: SteveHawk
# Modified for auto stopping frpc (or other kinds of) systemd services.
#

[Definition]

# Option:  actionstart
# Notes.:  command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
# Values:  CMD
#
actionstart = if systemctl -q is-active <service>; then touch <tmp_file>; fi

# Option:  actionstop
# Notes.:  command executed at the stop of jail (or at the end of Fail2Ban)
# Values:  CMD
#
actionstop = if [ -f '<tmp_file>' ]; then systemctl start <service> && rm <tmp_file>; fi

# Option:  actioncheck
# Notes.:  command executed once before each actionban command
# Values:  CMD
#
actioncheck = systemctl list-units --all --type=service | grep -q <service>

# Option:  actionban
# Notes.:  command executed when banning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    See jail.conf(5) man page
# Values:  CMD
#
actionban = systemctl stop <service>

# Option:  actionunban
# Notes.:  command executed when unbanning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    See jail.conf(5) man page
# Values:  CMD
#
actionunban = if [ -f '<tmp_file>' ]; then systemctl start <service>; fi

[Init]

service = frpc
tmp_file = /tmp/<service>-active.fail2ban

其中 [Init] 部分定义了可以被覆盖的变量,这里声明了服务名称和对应的用于保存状态的临时文件路径,这样可以很方便的在调用 action 的 jail 配置文件中指定 frpc 之外的 systemd 服务。

[Definition] 部分则是重头戏,共定义了 5 组指令:

  • actionstart 第一次执行封禁的时候执行的操作(需设置 actionstart_on_demand=true,否则会在服务启用时就执行)。我们自然不希望 Fail2Ban 去把一个本来被禁用的服务启用起来,只希望把启用状态的服务在封禁的时候禁用,解禁的时候启用。因此这里如果读取到服务为启用状态,就在指定路径生成一个临时文件,记录该服务最开始为启用状态。
  • actionstop jail 或 服务停止时执行的指令。这里检查是否存在临时文件,存在的话启用服务,并删除该文件。
  • actioncheck 在执行每次封禁/解禁操作前执行的检查操作,需要返回一个布尔值,为真时才会执行封禁/解禁对应的指令。其实可以留空,不过这里定义的命令会去检查该服务是否存在。
  • actionban 封禁命令。简单粗暴,直接停止服务。
  • actionunban 解禁命令。如果存在临时文件,则重新启用服务。

最终,将 jail.local 编辑为:

1
2
3
4
5
6
7
8
9
[sshd]
enabled = true
ignoreself = false
action = %(known/action)s
         systemd[service=frpc, actionstart_on_demand=true]
mode = aggressive
maxretry = 10
findtime = 15m
bantime = 30m

配置完成后执行 sudo fail2ban-client reload 重载配置。

其中 action 后面跟着的 %(known/action)s 会被自动替换为之前定义的 action,这样写相当于直接在原本的 action 上 append 了一个新的 action,非常方便。而 systemd action 中的 service 也可以填写任意别的服务名,轻松完成一样的功能。

不过这里我选择将 findtime 调整为较短的 15 分钟,bantime 调整为较短的半小时,因为仍然需要保证转发服务大部分时间的可用性,所以只封禁短时间内的大量失败,并且很快就重新上线服务。

缺陷

然而需要指出的是,这样子的配置有两点缺陷:

  • 这个 action 只能记录 actionstart 时的服务状态,后面如果用户有手动更改状态的话,Fail2Ban 不会知道。会有两种情况:原本启用的服务,在 Fail2Ban 执行封禁解封以后如果用户手动禁用服务,下一次解封的时候 Fail2Ban 还是会自动重新启用;原本禁用的服务,在 Fail2Ban 执行封禁解封以后如果用户手动启用服务,下一次解封的时候 Fail2Ban 不会自动启用。

  • 这样的配置非常容易被拒绝访问攻击。幸好我对于可用性的要求并不高,因此不会是个大问题。

尾声

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
2021-05-10 02:33:59,794 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:33:59
2021-05-10 02:33:59,942 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:33:59
2021-05-10 02:33:59,949 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:33:59
2021-05-10 02:34:00,006 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,008 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,032 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,040 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,055 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,065 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,093 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,101 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,108 fail2ban.filter         [539]: INFO    [sshd] Found 127.0.0.1 - 2021-05-10 02:34:00
2021-05-10 02:34:00,109 fail2ban.actions        [539]: NOTICE  [sshd] Ban 127.0.0.1

成功的拦下了攻击!目标完美达成。


本文阅读量
本站访客量