说明
自部署的 vaultwarden 接入 Cloudflare 后,各类恶意访问与攻击行为随之增多,常见目录扫描、接口探测、账号密码暴力破解等行为层出不穷。即便依靠 Cloudflare 防护策略与 Nginx 访问规则,能够拦截绝大多数基础恶意流量,却难以精准甄别并拦截针对性的登录爆破行为。
在此场景下,借助 Fail2Ban 实时监控运行日志、识别异常访问并自动封禁攻击源,便可高效补齐防护短板,大幅提升服务整体安全性。
需要注意的是在 Cloudflare 代理的情况下,vaultwarden 虽可通过请求头获取访客真实 IP,但若仅依靠 Fail2Ban 在服务器本地封禁 IP,无法起到实际防护效果,因为所有访问流量均经由 Cloudflare 节点转发至服务器,本地封禁仅能拦截节点 IP,无法封禁攻击者真实地址。
因此使用 Fail2Ban 识别真实攻击 IP + 触发 Action 调用 Cloudflare API 的组合方案,将封禁动作同步至 Cloudflare 自定义规则,实现 CDN 层面的拦截。
NOTE
- Fail2Ban 内置两款 Cloudflare 封禁动作,该动作是每封禁一个 IP 生成一条 IP 封禁规则,不美观也不利于强迫症,因此可以使用自定义规则将所有的封禁记录存于一条自定义规则中。
- 该方案存在一定局限:Cloudflare 自定义规则表达式字符上限约 4000 字符,最多仅能容纳170余条封禁 IP,若长期封禁大量 IP 容易导致超出字符以至于无法完成新的封禁动作,因此建议合理设置封禁时长,自动释放过期封禁条目。
- 若有长期封禁的需要,后期可能加入封禁 IP 段的实现,以减少多 IP 的字符占用。
一、vaultwarden 配置
1.1 创建专用用户以及数据目录
这里以使用 Docker Compose 全新部署 vaultwarden 为例
# 请确保具有 root 权限执行以下命令# 创建 vaultwarden 用户(禁止登录、无家目录)useradd -r -s /sbin/nologin -M vaultwarden
# 创建 vaultwarden 数据目录mkdir /opt/vaultwarden && cd /opt/vaultwarden
# 设置目录权限(仅运行用户可访问)chown -R vaultwarden:vaultwarden /opt/vaultwardenchmod -R 700 /opt/vaultwarden
# 创建 docker-compose.yml 文件touch docker-compose.yml
# 编辑 docker-compose.yml 示例内容见下方
# 部署 Docker 容器docker compose up -d1.2 docker-compose.yml 示例内容
使用 vaultwarden 用户运行容器,本地监听 10000 端口公网无法直接访问
通过 ROCKET_PROXY_HEADER 解析 Cloudflare 传递的真实客户端 IP
开启日志持久化存储,为后续 Fail2Ban 防爆破监控提供日志来源
services: server: image: vaultwarden/server container_name: vaultwarden restart: always user: vaultwarden:vaultwarden environment: - DOMAIN=https://vaultwarden.example.com # 填写托管在 CloudFlare 的域名,注意协议是https - SIGNUPS_ALLOWED=true # 是否允许注册 如果仅是自己使用先设置为 true,注册完成后在 admin 管理页面中修改为 false - ORG_CREATION_USERS=false # 是否允许创建组织 如果仅是自己使用设置为 false - ADMIN_TOKEN= # 可使用 openssl rand -base64 32 生成,也可根据官方指引生成 argon2 - WEBSOCKET_ENABLED=true # 启用 websocket 支持 - ROCKET_PROXY_HEADER=CF-Connecting-IP # 解析请求头以获取真实用户ip - TZ=Asia/Shanghai # 设置时区 - LOG_FILE=/data/vaultwarden.log # 设置日志文件位置 ports: - "127.0.0.1:10000:80" # 设置暴露端口 volumes: - ./data:/data/ # 设置数据保存目录部署完成后可通过以下命令查看日志是否生成
cat /opt/vaultwarden/data/vaultwarden.log二、CloudFlare 配置
2.1 创建自定义规则
进入 Cloudflare 后台 → 安全性 → 安全规则 → 点击创建规则 → 自定义规则
输入规则名称,例如Fail2ban vaultwarden
添加至少一个需要 ban 的 IP,如果暂时没有可输入1.2.3.4,点击保存
2.2 获取自定义规则信息
点击刚刚创建的规则 → 点击通过 API 调用保存
记录下方路径中的关键信息zone_id、ruleset_id、rule_id
/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id}2.3 创建 API 令牌
进入管理账户 → 账户 API 令牌 → 创建令牌
设置令牌名称,例如Fail2ban vaultwarden
授予 Rules & Configuration - Account Rulesets 的读写权限
建议设置客户端 IP,提高安全性
点击审核令牌获取cfat_开头的 API 令牌
三、Fail2Ban 配置
3.1 修改匹配规则
Fail2Ban 自带的匹配规则因 Vaultwarden 日志格式更新已失效,需要替换
规则文件路径为:/etc/fail2ban/filter.d/vaultwarden.conf
将failregex的内容替换为:
^\s*(?:\[\]\s*)?\[vaultwarden::api::(?:identity|admin|core::two_factor::authenticator)?\]\[ERROR\] (?:Invalid admin token|Invalid TOTP code|Username or password is incorrect)[\.!](?:\s+(?!IP:)\S+)* IP: <ADDR>(?:\. Username: <F-USER>\S+</F-USER>)?3.2 验证匹配规则的有效性
# 通用系统fail2ban-regex /opt/vaultwarden/data/vaultwarden.log /etc/fail2ban/filter.d/vaultwarden.conf
# 宝塔面板安装的 Fail2Ban/www/server/panel/pyenv/bin/fail2ban-regex /opt/vaultwarden/data/vaultwarden.log /etc/fail2ban/filter.d/vaultwarden.conf出现如下结果表示规则匹配正常,表示890行日志命中拦截规则75行
Lines: 890 lines, 0 ignored, 75 matched, 815 missed3.3 配置 Jail 规则
jail.local是 Fail2Ban 的核心防护规则配置文件,新增如下内容:
#vaultwarden-START[vaultwarden]enabled = true # 启用 Vaultwarden 防爆破规则port = 80,443 # 指定监控与封禁的端口filter = vaultwarden # 日志过滤规则,须与/etc/fail2ban/filter.d/下的匹配规则文件名称一致banaction = %(banaction_allports)s # 封禁所有端口(全局屏蔽)logpath = /opt/vaultwarden/data/vaultwarden.log # Vaultwarden 日志路径backend = pyinotify # 监听文件maxretry = 3 # 最大失败次数findtime = 60 # 统计时长 60sbantime = 60 # 封禁时长 60saction = cloudflare-vaultwarden # 触发 Action 动作调用 Cloudflare API 进行封禁和解封#vaultwarden-END3.4 配置 Action 执行脚本
# 创建脚本文件夹mkdir /etc/fail2ban/scripts
# 创建脚本文件touch /etc/fail2ban/scripts/cloudflare-vaultwarden.sh
# 脚本内容见下方
# 赋予执行权限chmod -R 700 /etc/fail2ban/scripts/cloudflare-vaultwarden.sh脚本内容如下:
#!/bin/bash
# 定义变量,接收传入的参数status="$1"IP="$2"CLOUDFLARE_API_TOKEN="$3"ZONE_ID="$4"RULESET_ID="$5"RULE_ID="$6"
# 定义 Cloudflare API 的 URL。Get an account or zone entry point rulesetcf_api_get_point_ruleset_url="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_request_firewall_custom/entrypoint"
# 获取指定 ZONE_ID 的 http_request_firewall_custom 规则的所有规则集ruleset_data=$(curl -s -X GET "${cf_api_get_point_ruleset_url}" -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
# 根据 RULE_ID 获取对应规则集中现在有的表达式expression=$(echo "$ruleset_data" | jq -r --arg RULE_ID "$RULE_ID" '.result.rules[] | select(.id == $RULE_ID) | .expression')action=$(echo "$ruleset_data" | jq -r --arg RULE_ID "$RULE_ID" '.result.rules[] | select(.id == $RULE_ID) | .action')description=$(echo "$ruleset_data" | jq -r --arg RULE_ID "$RULE_ID" '.result.rules[] | select(.id == $RULE_ID) | .description')
# 如果没有找到对应的规则,直接退出if [ -z "$expression" ]; then exit 0fi
# 根据 status 参数执行相应的操作if [ "$status" = "ban" ]; then # 检查现有表达式中是否已经包含要添加的IP if echo "$expression" | grep -q "(ip.src eq ${IP})"; then exit 0 fi
# 如果不包含要添加的IP则重新构造表达式,添加需要封禁的IP new_expression="${expression} or (ip.src eq ${IP})"
elif [ "$status" = "unban" ]; then # 检查现有表达式中是否包含需要取消封禁的IP if ! echo "$expression" | grep -q "(ip.src eq ${IP})"; then exit 0 fi
# 如果包含需要取消封禁的IP则重新构造表达式,删除需要取消封禁的IP new_expression=$(echo "$expression" | sed "s/ or (ip.src eq ${IP})//g; s/(ip.src eq ${IP}) or //g; s/(ip.src eq ${IP})//g") # 禁止删除最后一个 IP,避免规则失效 if [ -z "$new_expression" ]; then exit 0 fifi
# 调用 API 更新规则curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/${RULESET_ID}/rules/${RULE_ID}" \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ -d "{\"description\":\"${description}\",\"action\":\"${action}\",\"expression\":\"${new_expression}\"}"3.5 配置 Action 动作
在/etc/fail2ban/action.d中创建 cloudflare-vaultwarden.conf
将获取到的信息填写到 [init] 中留空的位置
[Definition]actionstart =actionstop =actioncheck =actionban = /etc/fail2ban/scripts/cloudflare-vaultwarden.sh ban <ip> <cloudflare_api_token> <zone_id> <ruleset_id> <rule_id>actionunban = /etc/fail2ban/scripts/cloudflare-vaultwarden.sh unban <ip> <cloudflare_api_token> <zone_id> <ruleset_id> <rule_id>
[Init]
# Cloudflare API 令牌cloudflare_api_token =
# 域名区域 ID (zone_id)zone_id =
# 自定义规则集 ID(ruleset_id)ruleset_id =
# 规则 ID (rule_id)rule_id =
# 防火墙规则的目标。默认是 "ip"(IPv4)cftarget = ip
[Init?family=inet6]# 启用 IPv6cftarget = ip63.6 重载配置并测试
# 重新加载配置fail2ban-client reload
# 手动测试封禁# 可在自定义规则中查看是否添加成功fail2ban-client set vaultwarden banip 2.3.4.5
# 检查封禁状态fail2ban-client status vaultwarden
# 手动测试解除封禁# 可在自定义规则中查看是否解除成功root@VM-0-13-ubuntu:~# fail2ban-client set vaultwarden unbanip 2.3.4.5非商业用途可以使用,但必须注明出处;
若有改编需采用相同许可协议发布。