Vaulwarden 是一个开源自托管的密码管理工具,这个项目使用 Rust 实现了一套 Bitwarden Server API, 很多小伙伴都用它来管理密钥与凭证。 本文将利用 fail2ban 来实现在 CDN 场景下的防暴力破解。
前言 作为一个密码凭证管理工具,首要关注的便是安全, Alliot 通过如下架构来部署 Vaultwarden: 通过上图可以看出,这里首先使用 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 文件格式为:
要想 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 次错误密码登录一下看看效果:
然后 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!