4763 字
24 分钟
Vaultwarden 安全部署实例——Cloudflare 边缘拦截 + Fail2Ban 动态封禁 + R2 定期备份

一、前言#

当网站接入到 Cloudflare 后,攻击者可通过 泛播IP 反查获得大量关联域名,扩大了域名在公网的暴露面。

这也导致各类恶意访问与攻击行为随之增多,常见目录扫描、接口探测、账号密码暴力破解等行为层出不穷。

Cloudflare 依靠其通用防护策略能够拦截绝大多数基础恶意流量,但面对伪装成正常请求的针对性登录爆破,仍缺乏精准甄别与拦截能力。即便引入 自定义规则,也难以实时适配攻击者不断变换的 IP 池与请求特征,防护效果存在滞后性。

而在服务端搭配 Nginx 访问规则虽可作为补充拦截手段,但其复杂的语法配置对不熟悉 Nginx 的用户而言门槛过高,难以在实际运维中有效落地。

针对上面提到的痛点,我们可以使用 Fail2Ban 工具进行入栈拦截,通过实时监控运行日志、识别异常访问并自动封禁攻击源的入栈请求。

同时搭配 Cloudflare IP访问规则在 CDN 层面同步拦截,在流量抵达源站前完成阻断。避免恶意请求穿透至服务器,从而有效减少服务器的性能损耗。

二、vaultwarden 安全部署#

本文通过 Docker Compose 使用专用用户部署 Vaultwarden 为例,避免漏洞提权提高安全性。

2.1 创建专用用户以及数据目录#

以下命令均需要在 root 权限下执行:

Terminal window
# 创建 vaultwarden 用户(禁止登录、无home目录)
useradd -r -s /sbin/nologin -M vaultwarden
# 创建 vaultwarden 数据目录
mkdir /opt/vaultwarden && cd /opt/vaultwarden
# 设置目录权限(仅 vaultwarden 用户可访问)
chown -R vaultwarden:vaultwarden /opt/vaultwarden
chmod -R 700 /opt/vaultwarden

2.2 创建 docker 容器#

创建 docker-compose.yml 配置文件:

Terminal window
cat << 'EOF' > /opt/vaultwarden/docker-compose.yml
services:
server:
image: vaultwarden/server
container_name: vaultwarden
restart: always
user: vaultwarden:vaultwarden # 使用 vaultwarden 用户运行容器
environment:
- DOMAIN=https://vaultwarden.example.com # vaultwarden 访问域名
- SIGNUPS_ALLOWED=true # 是否允许注册:首次部署时必须设为 true 才能创建管理员账户,否则无法创建自己的账户。如果不开放使用可在注册完成后通过管理界面或修改 config.json 进行关闭。
- ORG_CREATION_USERS=false # 是否允许创建组织:个人使用建议设置为 false
- ADMIN_TOKEN= # 使用 openssl rand -base64 32 生成,或根据官方指引生成 argon2
- WEBSOCKET_ENABLED=true # 启用 websocket 支持,关闭后部分插件以及 App 将无法使用
- ROCKET_PROXY_HEADER=CF-Connecting-IP # 设置解析 Cloudflare 请求头获取真实客户端 IP
- TZ=Asia/Shanghai # 设置时区
- LOG_FILE=/data/vaultwarden.log # 设置日志文件存储位置
ports:
- "127.0.0.1:10000:80" # 本地监听 10000 端口公网无法直接访问
volumes:
- ./data:/data/ # 设置数据保存目录
EOF

修改 docker 配置后,创建容器并查看日志是否生成:

Terminal window
docker compose up -d
cat /opt/vaultwarden/data/vaultwarden.log

三、Cloudflare 配置#

3.1 创建 Cloudflare API Token#

  1. 进入 Cloudflare 控制台点击 管理账户 → 账户 API 令牌 → 创建令牌
  2. 设置令牌名称:例如 Vaultwarden Fail2ban API key
  3. 设置权限策略:
    • 强烈建议指定 API Token 生效的域名,不选则针对所有域名生效
    • 选择 App SecurityFirewall Services 的读写权限
  4. 设置令牌过期时间:可根据实际需要设置令牌有效期
  5. 设置客户端 IP 地址过滤:建议设置 服务器IP 提高安全性
  6. 点击 审核令牌 创建令牌 获取 cfat_ 开头的 API Token

3.2 获取 Zone ID#

  1. 进入 Cloudflare 控制台点击 域名 → 概览 → 选择域名
  2. 点击 概述 并下拉至最下方
  3. 显示的 区域 ID 即为 Zone ID

3.3 使用 API 测试权限配置正确#

注意:尖括号 <> 为占位标识,填写时请务必删除,不要直接复制。

尝试封禁 IP 192.168.0.1#

Terminal window
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/<填写 Zone ID 即 区域 ID>/firewall/access_rules/rules" \
-H "Authorization: Bearer <填写 Cloudflare API Token>" \
-H "Content-Type: application/json" \
--data '{"mode":"block","configuration":{"target":"ip","value":"192.168.0.1"},"notes":"Fail2Ban Test>"}'

尝试解封 IP 192.168.0.1#

Terminal window
id=$(curl -s -G -X GET "https://api.cloudflare.com/client/v4/zones/<填写 Zone ID 即 区域 ID>/firewall/access_rules/rules" \
--data-urlencode "mode=block" \
--data-urlencode "configuration.target=ip" \
--data-urlencode "configuration.value=192.168.0.1" \
-H "Authorization: Bearer <填写 Cloudflare API Token>" \
-H "Content-Type: application/json" \
| awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'id'\042/){print $(i+1)}}}' \
| tr -d ' "' \
| head -n 1)
if [ -z "$id" ]; then
echo "未找到 IP 192.168.0.1 的封禁规则"
exit 0
fi
curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/<填写 Zone ID 即 区域 ID>/firewall/access_rules/rules/$id" \
-H "Authorization: Bearer <填写 Cloudflare API Token>" \
-H "Content-Type: application/json" \
--data '{"cascade": "none"}'

四、Fail2Ban 配置#

4.1 创建匹配规则#

Fail2Ban 通过匹配规则的正则表达式来匹配日志文件中的记录,所以匹配规则的完整性是 Fail2Ban 拦截的基础。
通过软件源安装的 Fail2Ban 一般附带 vaultwarden 的匹配规则,但是面板等方式安装的可能没有附带。
如果存在没有附带的情况,可以使用以下命令获取或创建文件:

  • 在线获取文件:
Terminal window
curl -o /etc/fail2ban/filter.d/vaultwarden.conf https://raw.githubusercontent.com/fail2ban/fail2ban/refs/heads/master/config/filter.d/vaultwarden.conf
  • 命令行创建文件:
Terminal window
cat << 'EOF' > /etc/fail2ban/filter.d/vaultwarden.conf
# Fail2Ban filter for unsuccessful Vaultwarden authentication attempts
# Logged in /var/log/vaultwarden.log
# Author: LearningSpot
[Definition]
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>)?
ignoreregex =
EOF
TIP

Vaultwarden 在仓库 Wiki 中也提供了匹配规则,但校验范围较小,无法匹配管理员令牌错误、TOTP 错误等情况。因此建议使用该匹配规则的用户切换为覆盖范围更广的 Fail2ban 官方规则。

4.2 验证匹配规则#

匹配规则创建完成后,建议使用以下命令测试,确保匹配规则可以生效:

Terminal window
fail2ban-regex /opt/vaultwarden/data/vaultwarden.log /etc/fail2ban/filter.d/vaultwarden.conf

如果是宝塔面板安装的 Fail2Ban 可以使用以下命令测试:

Terminal window
/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 missed

4.3 配置 Action 执行脚本#

Fail2Ban 一般附带 cloudflare.conf cloudflare-token.conf 两个 Action 配置,两者的核心区别如下:

  • cloudflare.conf 基于 Cloudflare Global Key 进行操作,实行 Cloudflare 全局拦截规则。由于 Global Key 权限过大,存在严重安全隐患,Cloudflare 已不再推荐使用。
  • cloudflare-token.conf 基于 Cloudflare API Token 和 Zone ID 操作,仅在指定的 Zone 内进行拦截。遵循最小权限原则,安全性更高,是当前推荐使用的配置方式。、

本文以使用 cloudflare-token.conf 为例,只需取消以下两行的注释,并填入从 Cloudflare 获得的信息即可:

cfzone = <填写 Zone ID 即 区域 ID>
cftoken = <填写 Cloudflare API Token>

注意:尖括号 <> 为占位标识,填写时请务必删除,不要直接复制。

如果存在没有附带 cloudflare-token.conf 的情况,可以使用以下命令获取或创建文件:

  • 在线获取文件:
Terminal window
curl -o /etc/fail2ban/action.d/cloudflare-token.conf https://raw.githubusercontent.com/fail2ban/fail2ban/refs/heads/master/config/action.d/cloudflare-token.conf
  • 命令行创建文件:
Terminal window
cat << 'EOF' > /etc/fail2ban/action.d/vaultwarden.local
#
# Author: Logic-32
#
# IMPORTANT
#
# Please set jail.local's permission to 640 because it contains your CF API token.
#
# This action depends on curl.
#
# To get your Cloudflare API token: https://developers.cloudflare.com/api/tokens/create/
#
# Cloudflare Firewall API: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/endpoints/
[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 =
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop =
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
actionban = curl -s -X POST "<_cf_api_url>" \
<_cf_api_prms> \
--data '{"mode":"<cfmode>","configuration":{"target":"<cftarget>","value":"<ip>"},"notes":"<notes>"}'
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionunban = id=$(curl -s -G -X GET "<_cf_api_url>" \
--data-urlencode "mode=<cfmode>" --data-urlencode "notes=<notes>" --data-urlencode "configuration.target=<cftarget>" --data-urlencode "configuration.value=<ip>" \
<_cf_api_prms> \
| awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'id'\042/){print $(i+1)}}}' \
| tr -d ' "' \
| head -n 1)
if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found using target <cftarget>"; exit 0; fi; \
curl -s -X DELETE "<_cf_api_url>/$id" \
<_cf_api_prms> \
--data '{"cascade": "none"}'
_cf_api_url = https://api.cloudflare.com/client/v4/zones/<cfzone>/firewall/access_rules/rules
_cf_api_prms = -H "Authorization: Bearer <cftoken>" -H "Content-Type: application/json"
[Init]
# Declare your Cloudflare Authorization Bearer Token in the [DEFAULT] section of your jail.local file.
# The Cloudflare <ZONE_ID> of the domain you want to manage.
#
# cfzone =
# Your personal Cloudflare token. Ideally restricted to just have "Zone.Firewall Services" permissions.
#
# cftoken =
# Target of the firewall rule. Default is "ip" (v4).
#
cftarget = ip
# The firewall mode Cloudflare should use. Default is "block" (deny access).
# Consider also "js_challenge" or other "allowed_modes" if you want.
#
cfmode = block
# The message to include in the firewall IP banning rule.
#
notes = Fail2Ban <name>
[Init?family=inet6]
cftarget = ip6
EOF

4.4 创建拦截规则#

创建匹配规则后,还需配置对应的拦截规则。
当某个 IP 在指定时间内多次触发匹配规则时,Fail2Ban 将自动执行拦截规则对其进行封禁。

通过以下命令创建拦截规则:

Terminal window
cat << 'EOF' > /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true # 启用 Vaultwarden 防爆破规则
filter = vaultwarden # 日志过滤规则,须与/etc/fail2ban/filter.d/下的匹配规则文件名称一致
logpath = /opt/vaultwarden/data/vaultwarden.log # Vaultwarden 日志路径
backend = pyinotify # 监听文件
maxretry = 5 # 最大失败次数
findtime = 600 # 统计时长 600s
bantime = 3660 # 封禁时长 3660s
action = cloudflare-token # 触发 Action 动作调用 Cloudflare API 进行封禁和解封
EOF
NOTE

由于已通过 Cloudflare 前置拦截,下述拦截规则未作配置,因其仅对请求到服务器的请求生效。

port = 80,443 # 指定监控与封禁的端口
banaction = %(banaction_allports)s # 封禁所有端口(全局屏蔽)

4.5 拦截测试#

Terminal window
# 重启 fail2ban
systemctl restart fail2ban-client
# 手动测试封禁
fail2ban-client set vaultwarden banip 1.2.3.4
# 检查本地封禁状态,同时可以访问 Cloudflare 查看封禁效果
# 如果本地生效,但 Cloudflare 没有更新规则,请检测文件权限以及 API Token 权限以及 IP 授权范围
fail2ban-client status vaultwarden
# 手动测试解除封禁
fail2ban-client set vaultwarden unbanip 1.2.3.4

五、使用 Cloudflare 自定义规则#

使用 cloudflare-token.conf 进行封禁时,每封禁一个恶意访问的 IP 都将会生成一条 IP 封禁规则,对于追求配置简洁的用户来说难以接受。

因此我们可以通过 Cloudflare WAF 自定义规则 将所有封禁 IP 合并到一条规则中。

但该方法存在局限性:免费账户的自定义规则表达式上限为 4000个字符

当 IP 地址每个段均为三位数(如 123.123.123.123)时最多可以存储 250 个 IP。

一旦字符数超限,那么新 IP 将无法被加入规则,进而导致封禁失效。

同时介于该规则通过文本内容进行匹配,因此不建议在本方案的自定义规则上进行额外的手动配置。

NOTE

本方案与 cloudflare-token.conf 可以同时设置,但是这是不必要的。

5.1 创建 Cloudflare 自定义规则#

  1. 进入 Cloudflare 控制台点击 安全性 → 安全规则 → 创建规则 → 自定义规则
  2. 输入规则名称,例如Vaultwarden Fail2ban Rules
  3. 字段选择 IP 源地址
  4. 运算符选择 包含以下各项
  5. 值:填写一个需要封禁IP,如果暂时没有可输入192.168.0.1
  6. 采取措施选择阻止
  7. 执行顺序选择第一个
  8. 状态选择活动
  9. 记录 通过 API 调用保存 中的 zone_idruleset_idrule_id
    路径对应信息如下:/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id}
  10. 点击保存

5.2 创建 Cloudflare API Token#

NOTE

该方案所需的 Cloudflare API Token 权限与上文所需权限不一致!!!

  1. 进入 Cloudflare 控制台点击 管理账户 → 账户 API 令牌 → 创建令牌
  2. 设置令牌名称:例如 Vaultwarden Fail2ban Rules API key
  3. 设置权限策略:
    • 强烈建议指定 API Token 生效的域名,不选则针对所有域名生效
    • 选择 Rules & ConfigurationAccount Rulesets 的读写权限
  4. 设置令牌过期时间:可根据实际需要设置令牌有效期
  5. 设置客户端 IP 地址过滤:建议设置服务器IP提高安全性
  6. 点击 审核令牌 创建令牌 获取 cfat_ 开头的 API Token

5.3 配置 Action 执行脚本#

TIP

该 Action 脚本依赖 jq 工具解析 JSON 数据,所以需要确保运行环境已安装该依赖。

Terminal window
apt-get install -y jq

使用以下命令获取或创建执行脚本:

  • 在线获取执行脚本:
Terminal window
curl -o /etc/fail2ban/action.d/vaultwarden-cf-rules.local https://www.self4m.com/file/vaultwarden-cloudflare-fail2ban-r2-backup/vaultwarden-cf-rules.local
chmod 640 /etc/fail2ban/action.d/vaultwarden-cf-rules.local
  • 文件创建执行脚本:
Terminal window
cat << "EOF" > /etc/fail2ban/action.d/vaultwarden-cf-rules.local
#
# Author: self4m
#
# Date: 2026-07-31
#
# version: 1.1
#
# 提示:
#
# 请设置 vaultwarden-cf-rules.local 的权限为 640,因为它包含你的 CF API 令牌
# chmod 640 /etc/fail2ban/action.d/vaultwarden-cf-rules.local
[Definition]
# 封禁操作:获取规则 → 提取IP列表 → 添加IP → 更新规则
actionban = \
data=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/<cfzone>/rulesets/<cfruleset_id>" \
-H "Authorization: Bearer <cftoken>") && \
expression=$(echo "$data" | jq -r --arg RULE_ID "<cfrule_id>" \
'.result.rules[] | select(.id == $RULE_ID) | .expression') && \
if [ -n "$expression" ]; then \
action=$(echo "$data" | jq -r --arg RULE_ID "<cfrule_id>" \
'.result.rules[] | select(.id == $RULE_ID) | .action') && \
description=$(echo "$data" | jq -r --arg RULE_ID "<cfrule_id>" \
'.result.rules[] | select(.id == $RULE_ID) | .description | tojson') && \
original_ip_segment=$(echo "$expression" | grep -oP 'ip\.src in \{[^}]+\}') && \
ip_list=$(echo "$expression" | grep -oP '(?<=ip\.src in \{)[^}]+(?=\})') && \
if ! echo " $ip_list " | grep -qF " <ip> "; then \
ip_list="${ip_list} <ip>" && \
new_ip_segment="ip.src in {${ip_list}}" && \
new_expression=$(echo "$expression" | sed "s|${original_ip_segment}|${new_ip_segment}|") && \
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/<cfzone>/rulesets/<cfruleset_id>/rules/<cfrule_id>" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer <cftoken>" \
-d "{\"description\":${description},\"action\":\"${action}\",\"expression\":\"${new_expression}\"}"
fi
fi
# 解封操作:获取规则 → 提取IP列表 → 删除IP → 更新规则
actionunban = \
data=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/<cfzone>/rulesets/<cfruleset_id>" \
-H "Authorization: Bearer <cftoken>") && \
expression=$(echo "$data" | jq -r --arg RULE_ID "<cfrule_id>" \
'.result.rules[] | select(.id == $RULE_ID) | .expression') && \
if [ -n "$expression" ]; then \
action=$(echo "$data" | jq -r --arg RULE_ID "<cfrule_id>" \
'.result.rules[] | select(.id == $RULE_ID) | .action') && \
description=$(echo "$data" | jq -r --arg RULE_ID "<cfrule_id>" \
'.result.rules[] | select(.id == $RULE_ID) | .description | tojson') && \
original_ip_segment=$(echo "$expression" | grep -oP 'ip\.src in \{[^}]+\}') && \
ip_list=$(echo "$expression" | grep -oP '(?<=ip\.src in \{)[^}]+(?=\})') && \
if echo " ${ip_list} " | grep -qwF "<ip>"; then \
ip_list=$(echo "$ip_list" | tr ' ' '\n' | grep -vxF "<ip>" | tr '\n' ' ' | sed 's/^ *//;s/ *$//') && \
if [ -z "$ip_list" ]; then \
exit 0
fi
new_ip_segment="ip.src in {${ip_list}}" && \
new_expression=$(echo "$expression" | sed "s|${original_ip_segment}|${new_ip_segment}|") && \
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/<cfzone>/rulesets/<cfruleset_id>/rules/<cfrule_id>" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer <cftoken>" \
-d "{\"description\":${description},\"action\":\"${action}\",\"expression\":\"${new_expression}\"}"
fi
fi
[Init]
cfrule_id = <填写规则 ID>
cfruleset_id = <填写规则集 ID>
cfzone = <填写 Zone ID 即 区域 ID>
cftoken = <填写 Cloudflare API Token>
EOF
chmod 640 /etc/fail2ban/action.d/vaultwarden-cf-rules.local

注意:尖括号 <> 为占位标识,填写时请务必删除,不要直接复制。

5.4 修改拦截规则#

由于配置了自定义执行脚本,所以需要在拦截规则中更新 action 的值,以匹配脚本的实际路径:

action = vaultwarden-cf-rules

5.5 拦截测试#

Terminal window
# 重启 fail2ban
systemctl restart fail2ban-client
# 手动测试封禁
fail2ban-client set vaultwarden banip 1.2.3.4
# 检查本地封禁状态,同时可以访问 Cloudflare 查看封禁效果
# 如果本地生效,但 Cloudflare 没有更新规则,请检测文件权限以及 API Token 权限以及 IP 授权范围
fail2ban-client status vaultwarden
# 手动测试解除封禁
fail2ban-client set vaultwarden unbanip 1.2.3.4

六、R2 定期备份#

R2 是 Cloudflare 的兼容 S3 协议的对象存储,除用于静态资源分发外,也可以作为数据备份的存储。

6.1 创建 Cloudflare R2 存储桶#

  1. 进入 Cloudflare 控制台点击 管理账户 → R2 对象存储 → 概述 → 创建存储桶
  2. 设置存储桶名称:例如 vaultwarden-backup
  3. 位置选择:自动
  4. 默认存储类选择:标准
  5. 点击创建
IMPORTANT

存储桶创建后是默认禁用公开访问的,为了数据的安全,请勿手动开启访问权限。

6.2 创建 Cloudflare API Token#

NOTE

该方案所需的 Cloudflare API Token 权限与上文所需权限不一致!!!

存储桶创建以后需要创建 API Token 用于脚本操作备份,根据最小权限原则建议创建一个独立的 API Token。
如果想要复用 API Token 额外添加下方所提及的权限即可。

  1. 进入 Cloudflare 控制台点击 存储和数据库 → 账户 API 令牌 → 创建令牌
  2. 设置令牌名称:例如 Vaultwarden Backup API key
  3. 设置权限策略:
    • 选择类型R2 存储桶 并选择 vaultwarden-backup,不选则针对所有存储桶生效
    • 选择 Developer PlatformWorkers R2 Storage Bucket Item 的读写权限
  4. 设置令牌过期时间:可根据实际需要设置令牌有效期
  5. 设置客户端 IP 地址过滤:建议设置服务器IP提高安全性
  6. 点击 审核令牌 创建令牌 获取 访问密钥 ID 秘密访问密钥 S3 API

6.3 安装 AWS CLI#

使用以下命令安装 AWS CLI:

Terminal window
apt-get install -y awscli

6.4 创建备份脚本文件#

因为 vaultwarden 使用本地数据库,为了避免直接备份导致数据差异,本脚本在备份过程中将会停止 vaultwarden 容器。容器停止后会立即打包所有数据文件,打包完成后会立即重启 vaultwarden 容器。
如果备份数据到 R2 失败,会将备份文件临时存储在 /tmp/vaultwarden_backup 目录下。

通过以下两种方式之一创建备份脚本:

  • 在线获取脚本文件:
Terminal window
curl -o /opt/vaultwarden/vaultwarden_backup.sh https://www.self4m.com/file/vaultwarden-cloudflare-fail2ban-r2-backup/vaultwarden_backup.sh
  • 命令行创建脚本文件:
cat << "EOF" > /opt/vaultwarden/vaultwarden_backup.sh
#!/bin/bash
set -e
export AWS_ACCESS_KEY_ID="访问密钥 ID"
export AWS_SECRET_ACCESS_KEY="秘密访问密钥"
export ACCOUNT_ID="Cloudflare 账户 ID"
CONTAINER_NAME="vaultwarden" # 如果你的 vaultwarden 容器名称不同,需要修改为实际容器名称
BACKUP_DIR="/tmp/vaultwarden_backup"
BUCKET_NAME="vaultwarden-backup"
DATE=$(date +%Y%m%d_%H%M%S)
FILE_NAME="vaultwarden_${DATE}.tar.gz"
mkdir -p "$BACKUP_DIR"
echo "停止容器 $CONTAINER_NAME..."
docker stop "$CONTAINER_NAME"
echo "备份数据到 $BACKUP_DIR/$FILE_NAME ..."
tar -czf "${BACKUP_DIR}/${FILE_NAME}" -C /opt vaultwarden # 如果你的 vaultwarden 数据文件路径不同,需要修改为实际数据文件路径
echo "启动容器 $CONTAINER_NAME ..."
docker start "$CONTAINER_NAME"
echo "上传到 R2 ..."
if aws s3 cp "${BACKUP_DIR}/${FILE_NAME}" \
"s3://${BUCKET_NAME}/${FILE_NAME}" \
--endpoint-url "https://${ACCOUNT_ID}.r2.cloudflarestorage.com"; then
rm -f "${BACKUP_DIR}/${FILE_NAME}"
echo "备份完成: $FILE_NAME"
else
echo "上传失败,保留本地备份: ${BACKUP_DIR}/${FILE_NAME}"
fi
find "$BACKUP_DIR" -type f -name "vaultwarden_*.tar.gz" -mtime +7 -exec rm -f {} \;
EOF

脚本文件创建后,在脚本中填写如下参数信息:

  • AWS_ACCESS_KEY_ID:填写 访问密钥 ID
  • AWS_SECRET_ACCESS_KEY:填写 秘密访问密钥
  • ACCOUNT_ID:填写 S3 API 中的账户ID,https://<账户ID>.r2.cloudflarestorage.com/vaultwarden-backup

填写完成后设置 vaultwarden_backup.sh 的权限为 640,因为它包含你的 Access Key

Terminal window
chmod 640 /opt/vaultwarden/vaultwarden_backup.sh

6.5 设置 crontab 定时任务#

使用 crontab -e 添加定时任务,例如每天凌晨 1 点执行备份:

Terminal window
0 1 * * * /opt/vaultwarden/vaultwarden_backup.sh >> /var/log/r2-backup.log 2>&1

6.6 手动测试备份#

Terminal window
bash /opt/vaultwarden/vaultwarden_backup.sh

如果无法完成备份,请检查文件权限以及 API Token 权限以及 IP 授权范围

七、总结#

通过以上的配置,我们已经完成了使用 Cloudflare + Fail2ban 对 Vaultwarden 进行安全加固,同时借助 R2 对象存储实现了数据的定期备份,为数据安全提供了基础保障。

但需要明确的是,无论防护措施做得多完善,数据安全的最终责任仍然掌握在自己手中。Vaultwarden 存储的是个人最敏感的密码信息,一旦数据丢失或服务遭入侵,恢复成本极高,甚至无法挽回。

因此,建议定期检查备份文件的完整性,验证恢复流程是否可用,同时妥善保管 API 密钥和各类凭证,避免将全部信任寄托在单一防护措施上。安全从来不是一劳永逸的结果,而是一个需要持续关注与不断维护的过程。

Vaultwarden 安全部署实例——Cloudflare 边缘拦截 + Fail2Ban 动态封禁 + R2 定期备份
https://www.self4m.com/posts/vaultwarden-cloudflare-fail2ban-r2-backup/
✍️作者
Self4m
📅发布于
2026-05-21
©️许可协议
CC BY-NC-SA 4.0

商业用途必须事先获得作者授权;
非商业用途可以使用,但必须注明出处;
若有改编需采用相同许可协议发布。