CDN场景下配置Vaultwarden启用fail2ban

  Vaulwarden 是一个开源自托管的密码管理工具,这个项目使用 Rust 实现了一套 Bitwarden Server API, 很多小伙伴都用它来管理密钥与凭证。 本文将利用 fail2ban 来实现在 CDN 场景下的防暴力破解。

前言

  作为一个密码凭证管理工具,首要关注的便是安全, Alliot 通过如下架构来部署 Vaultwarden:
Vaultwarden fail2ban 架构图
  通过上图可以看出,这里首先使用 CDN 作为了第一道防线, CDN 除去能够分发静态资源提升访问速度之外,还能比较好的帮助我们隐藏源站,在一定程度上起到了保护源站的作用。 用户的请求通过 CDN 节点回源到 Nginx,最后才会到达 Vaultwarden 服务。
  针对暴力破解,fail2ban 是中小项目应用很广的一个工具,大多数场景会利用 fail2ban 监听登录失败事件/日志,触发 iptables 封锁指定的 IP, Vaultwarden 官方也推荐使用 这种方式 来加固我们的 Vaultwarden。
  然而,在使用 CDN 场景下, 所有用户的请求都是通过 CDN 节点做转发的(WAF 同理),用户并不会直接请求源站,这样在源站的 iptables 封锁用户的 IP 显然无法达到我们的目的, 因此我们需要配置自定义的规则实现从 Nginx 网关层面来阻断恶意的请求, 下文主要针对这部分来做讲解说明。

配置Vaulwarde

  这里对于 Docker 部署 Vaultwarden 的过程就不过多赘述,直接给出我们的部署配置文件:
docker-compose.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: always
volumes:
- ./data:/data
- /var/log/vaultwarden/:/log/
env_file:
- config.env
ports:
- "127.0.0.1:8080:80"

  同级目录下的 config.env:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 是否允许注册  
SIGNUPS_ALLOWED=false

# 是否开启web UI
WEB_VAULT_ENABLED=true

WEBSOCKET_ENABLED=true
LOG_FILE=/log/vaultwarden.log
LOG_LEVEL=warn
EXTENDED_LOGGING=true

# 禁止显示密码提示
SHOW_PASSWORD_HINT=false

# 启用移动端推送
# https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification
# PUSH_ENABLED=true
# PUSH_INSTALLATION_ID=xxx
# PUSH_INSTALLATION_KEY=xxx

  这里我们在 config.env 将 Vaultwarden 的日志等级变更为了 warn, 同时指定了日志文件输出到容器内部的 /log/vaultwarden.log, 然后在 docker-compose 中将其映射到了宿主机的 /var/log/vaultwarden/vaultwarden.log, 一旦用户登录密码错误,就会输出日志到这个日志文件, 我们后面将利用 fail2ban 读取这个日志文件来实现防暴力破解。

配置 fail2ban

  这里我们以 Ubuntu 为例,安装好 fail2ban, 并配置开机启动:

1
2
apt install fail2ban -y
systemctl enable --now fail2ban

  默认情况下,fail2ban 安装完成后会在 /etc/fail2ban 生成配置文件,这里我们按照如下配置,在对应的路径下新建 Vaulwarden 相关的配置:
  新建 /etc/fail2ban/filter.d/vaultwarden.local, 这个文件主要用于定义从 Vaulwarden 日志中筛选出登录失败用户的 IP:

1
2
3
4
5
6
7
[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =

  同样的,针对 admin 页面,我们也创建一个配置 /etc/fail2ban/filter.d/vaultwarden-admin.local:

1
2
3
4
5
6
7
[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*Invalid admin token\. IP: <ADDR>.*$
ignoreregex =

  我们再来定义一下 action:
  新建 /etc/fail2ban/action.d/vaultwarden.local,这个主要是从 nginx-block-map.conf 这个 action 修改而来, 注意需要将 Nginx conf 路径改成我们自己的:

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
[Definition]
# 配置Nginx的conf路径
srv_cfg_path = /usr/local/nginx/conf/

# cmd-line arguments to supply to test/reload nginx:
#srv_cmd = nginx -c %(srv_cfg_path)s/nginx.conf
srv_cmd = nginx

# first test configuration is correct, hereafter send reload signal:
blck_lst_reload = %(srv_cmd)s -qt; if [ $? -eq 0 ]; then
%(srv_cmd)s -s reload; if [ $? -ne 0 ]; then echo 'reload failed.'; fi;
fi;

# map-file for nginx, can be redefined using `action = nginx-block-map[blck_lst_file="/path/file.map"]`:
blck_lst_file = %(srv_cfg_path)s/vaultwarden_blocked_ips.map

# Action definition:

actionstart_on_demand = false
actionstart = touch '%(blck_lst_file)s'

actionflush = truncate -s 0 '%(blck_lst_file)s'; %(blck_lst_reload)s

actionstop = %(actionflush)s

actioncheck =

_echo_blck_row = printf '\%%s 1;\n' "<fid>"

actionban = %(_echo_blck_row)s >> '%(blck_lst_file)s'; %(blck_lst_reload)s

actionunban = id=$(%(_echo_blck_row)s | sed -e 's/[]\/$*.^|[]/\\&/g'); sed -i "/^$id$/d" %(blck_lst_file)s; %(blck_lst_reload)s

  这个文件主要定义了 fail2ban 在执行 ban 与 unban 操作时的动作,不难看出,主要是将目标 IP 以 Nginx map 格式写入到了 Nginx conf 路径下的 vaultwarden_blocked_ips.map 文件中, 然后执行了 Nginx reload 操作。

  完成后,我们再来配置2个 jail, 简单定义一下规则,包括封禁时间等:
  新建 /etc/fail2ban/jail.d/vaultwarden.local:

1
2
3
4
5
6
7
8
9
[vaultwarden]
enabled = true
filter = vaultwarden
banaction = vaultwarden
logpath = /var/log/vaultwarden/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400

  同样的,针对 admin 页面新建 /etc/fail2ban/jail.d/vaultwarden-admin.local:

1
2
3
4
5
6
7
8
9
[vaultwarden-admin]
enabled = true
filter = vaultwarden-admin
banaction = vaultwarden
logpath = /var/log/vaultwarden/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400

  最后我们需要执行一下 systemctl restart fail2ban 使得前面的配置生效。

配置Nginx

  经过前面的配置,用户在尝试登录失败后,Vaultwarden 会将日志记录到 /var/log/vaultwarden/vaultwarden.log, fail2ban 在匹配到日志后,会将用户的 IP 地址拿到,在尝试登录失败 3 次后,会触发 vaultwarden 的 action, 这个 action 会在 Nginx 的 conf 路径(/usr/local/nginx/conf) 的 vaultwarden_blocked_ips.map 文件中记录用户日志,并 reload Nginx, 这个 vaultwarden_blocked_ips.map 文件格式为:

1
\99.99.99.99 1;

  要想 Nginx 能够根据这个列表来封禁请求,我们还需要配置一下 Vaultwarden 的 Nginx:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

http {
....

# 定义一个fail2ban的日志格式
log_format f2b_log '[$time_local] fail2ban "$blck_lst_ses" - $remote_addr - "$http_referer" - $http_user_agent" "$request"';


# 使用RealIP模块 从CDN的X-Forwarded-For获取用户真实IP 配置为 $remote_addr
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;


###### Vaultwarden ######
upstream vaultwarden-default {
zone vaultwarden-default 64k;
server 127.0.0.1:8080;
keepalive 2;
}

# 兼容websocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}

# 使用用户真实IP作为key
map $remote_addr $blck_lst_ses {
include vaultwarden_blocked_ips.map;
}

server {
listen 443 ssl http2;
server_name www.iots.vip; # 改成你自己的域名
# ... 省略ssl相关配置

# 定义access log
access_log logs/access.log;

location / {
# 定义403页面
error_page 403 = @f2b-banned;
proxy_pass http://vaultwarden-default;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# 配置websocket相关
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_redirect default;

# 使Vaulwarden能够正确获得用户真实IP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 禁止爬虫相关日志
if ($http_user_agent ~* "bot|spider" ) {
access_log off;
}

# 判断是否被BAN 如果是则直接返回403
if ( $blck_lst_ses != "" ) {
return 403;
}
}
location @f2b-banned {
# 定义fail2ban日志
access_log logs/f2b-auth-errors.log f2b_log;

# 直接内联一个简单的403页面,并且显示用户IP
default_type text/html;
return 403 "<br/><center>
<b style=\"color:red; font-size:18pt; border:1pt solid black; padding:2pt;\">
You are banned! </b><div>Your IP address: $remote_addr</div></center>";
}
}
}

测试效果

  这里举例几个常用的 fail2ban 命令:

1
2
3
4
5
6
7
8
# 针对 vaultwarden jail  封禁指定IP
fail2ban-client set vaultwarden banip 192.168.1.1

# 解封
fail2ban-client set vaultwarden unbanip 192.168.1.1

# 查看 vaultwarden jail
fail2ban-client status vaultwarden

  我们先直接通过 Web 界面输入 3 次错误密码登录一下看看效果:
Vaultwarden fail2ban 封禁效果

  然后 fail2ban-client status vaultwarden 查看一下:

1
2
3
4
5
6
7
8
9
Status for the jail: vaultwarden
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- File list: /var/log/vaultwarden/vaultwarden.log
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: x.x.x.x

  可以看到 fail2ban 成功的帮助我们封禁了错误登录尝试 3 次以上的 IP,通过命令解封一下:

1
fail2ban-client set vaultwarden unbanip 192.168.1.1

  解封这个 IP 后,我们又能正常访问 Vaultwarden 了,大工告成,Enjoy it!