DocsByRcDocsByRc
Vaultwarden 自建密码库
Vaultwarden · 密码管理

Vaultwarden 自建密码库

Vaultwarden 稳定安全部署:PVE LXC / 公网 VPS 双环境,Cloudflare Tunnel / Caddy 双接入,手机推送、Fail2ban 防爆破、GPG 加密备份。

适用PVE LXC / VPS + Docker
难度中等
阅读—

本教程覆盖两种部署,均为容器化,配置文件完全通用,只是宿主环境不同:

场景宿主特点
本地 PVE 家用推荐Debian 12 LXC 容器内跑 Docker数据完全在自己手里,快照/备份方便,开销极低
公网 VPSDebian 12 直接跑 Docker7×24 在线,无需内网穿透

最终访问链路(主线方案 Cloudflare Tunnel):

浏览器 → Cloudflare 边缘(HTTPS)→ 加密隧道 → cloudflared 容器 → vaultwarden 容器。无论家里还是 VPS,都不需要开放任何入站端口,真实 IP 全程隐藏。

前置条件

  • 一台装好 Debian 12 的 PVE LXC 或公网 VPS(有 root 或 sudo 权限)
  • 一个 Cloudflare 账号,域名 你的域名.com 已托管其上(NS 已指向 Cloudflare)
  • 一个能收邮件的邮箱(注册 Cloudflare、bitwarden.com/host 时用)
  • 基础的 Linux 命令行操作能力

本教程默认启用的安全基线

  • 首次注册后关闭公开注册,杜绝陌生人注册
  • Admin 后台用 Argon2 加密令牌保护,不存明文
  • 账户强制开启 2FA 双因素认证
  • 零开放端口(Cloudflare Tunnel),隐藏源站 IP
  • Fail2ban 登录防爆破
  • GPG 加密 + 异地上云的自动备份
约定:命令默认以 root 执行(非 root 请在前面加 sudo)。代码里带黄色底色的值(如 vault.你的域名.com)是需要你按自己情况替换的占位。
主密码 = 唯一钥匙,且无法找回。Vaultwarden 采用零知识端到端加密,服务器只存密文。主密码一旦丢失,整个密码库将永久无法解密,没有任何后门或客服能帮你找回。请使用足够强的主密码,并离线(纸质/离线设备)另存一份。

按你的场景选择对应标签页完成宿主准备,最后两者共用同一套 Docker 安装步骤。

以下命令在 PVE 宿主机 shell 执行。

① 下载 Debian 12 模板(若已有可跳过)

BASH · PVE 宿主机
pveam update
pveam available | grep debian-12
# 版本号以上面输出为准,下面 12.7-1 只是示例
pveam download local debian-12-standard_12.7-1_amd64.tar.zst

② 创建 LXC 容器

BASH · PVE 宿主机
# CTID 用 110 举例;存储 local-lvm、网桥 vmbr0 按你的环境改
pct create 110 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname vaultwarden \
  --cores 2 --memory 1024 --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 1 \
  --features nesting=1,keyctl=1 \
  --onboot 1 --password
--features nesting=1,keyctl=1 是 Docker 能在非特权 LXC 里正常运行的关键,缺了 Docker 会起不来。若容器已存在,用 pct set 110 --features nesting=1,keyctl=1 补上后重启容器。

③ 启动并进入容器

BASH · PVE 宿主机
pct start 110
pct enter 110
# 进入后先设时区、更新系统
apt update && apt -y upgrade
apt install -y curl ca-certificates gnupg

之后所有步骤都在容器内执行,继续往下看"共用:安装 Docker"。

以 root SSH 登录你的 Debian 12 VPS。

① 更新系统与基础工具

BASH · VPS
apt update && apt -y upgrade
apt install -y curl sudo ca-certificates gnupg

② 防火墙(ufw)

主线走 Cloudflare Tunnel 时无需放行 80/443,只留 SSH,攻击面最小。

BASH · VPS
apt install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
# 仅当第 3 节改用 Caddy 反代(而非 CF Tunnel)时,才放行 Web 端口:
# ufw allow 80/tcp
# ufw allow 443/tcp
ufw --force enable

③ SSH 加固

BASH · VPS
# 务必先确认已能用密钥免密登录,再执行下面两行!
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl restart ssh
关闭密码登录前,务必确认公钥已加入 ~/.ssh/authorized_keys 且能免密登录,否则会把自己彻底锁在服务器外面。

共用:安装 Docker

LXC 内 / VPS 上均执行以下官方源安装步骤(Debian 12):

BASH
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
BASH · 验证
docker run --rm hello-world
docker compose version
看到 hello-world 的欢迎语,以及 Docker Compose version v2.x 版本号,即环境就绪。

① 建立目录

BASH
mkdir -p /opt/vaultwarden/vw-data
cd /opt/vaultwarden

② 生成 Argon2 加密的 Admin 令牌

BASH
# 按提示输入两次令牌口令,复制输出的整串 $argon2id$... 
docker run --rm -it vaultwarden/server /vaultwarden hash
这串口令是进 /admin 后台用的,请单独记好,不要与账户主密码相同。

③ 写入环境变量文件 vaultwarden.env

BASH · 一条命令创建(先替换高亮占位)
cat > /opt/vaultwarden/vaultwarden.env <<'EOF'
DOMAIN=https://vault.你的域名.com
# 首次注册完成后改成 false(见第 4 节)
SIGNUPS_ALLOWED=true
SIGNUPS_VERIFY=false
INVITATIONS_ALLOWED=true
# 下面粘贴第 ② 步 hash 输出的 $argon2id$ 整串
ADMIN_TOKEN=$argon2id$v=19$m=...整串...
ENABLE_WEBSOCKET=true
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=warn
EXTENDED_LOGGING=true
# CF Tunnel/橙云=CF-Connecting-IP;Caddy 灰云直连=X-Real-IP
IP_HEADER=CF-Connecting-IP
ROCKET_WORKERS=10
# 推送先关,第 5 节再开启
PUSH_ENABLED=false
EOF
用 <<'EOF'(带引号的定界符)保证 $ 原样写入,argon2 令牌里的 $ 不会被 shell 解析。执行前务必把两处高亮占位改成你的实际值。
ENV · /opt/vaultwarden/vaultwarden.env
DOMAIN=https://vault.你的域名.com
# 首次注册完成后改成 false(见第 4 节)
SIGNUPS_ALLOWED=true
SIGNUPS_VERIFY=false
INVITATIONS_ALLOWED=true
# 下面粘贴第 ② 步 hash 输出的 $argon2id$ 整串
ADMIN_TOKEN=$argon2id$v=19$m=...整串...
ENABLE_WEBSOCKET=true
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=warn
EXTENDED_LOGGING=true
# CF Tunnel/橙云=CF-Connecting-IP;Caddy 灰云直连=X-Real-IP
IP_HEADER=CF-Connecting-IP
ROCKET_WORKERS=10
# 推送先关,第 5 节再开启
PUSH_ENABLED=false
注释单独占一行(以 # 开头);不要写成 值 # 注释 的行内形式,老版本 env_file 会把 # 后内容并进值里,导致布尔项解析出错。
关于 $ 转义:用 env_file 方式(本教程)时,值原样传入容器,$ 不会被解析,argon2 整串直接粘贴即可。如果你改用 compose 的 environment: 内联写法,则每个 $ 必须写成 $$,否则令牌失效。

④ 写入 docker-compose.yml(基础版)

反向代理 / 隧道服务第 3 节再往这个文件里加,先只跑 Vaultwarden 本体。

BASH · 一条命令创建
cat > /opt/vaultwarden/docker-compose.yml <<'EOF'
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file: ./vaultwarden.env
    volumes:
      - ./vw-data:/data
    networks: [vw-net]

networks:
  vw-net:
    name: vw-net
EOF
YAML · /opt/vaultwarden/docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file: ./vaultwarden.env
    volumes:
      - ./vw-data:/data
    networks: [vw-net]

networks:
  vw-net:
    name: vw-net
:latest 方便,但可能自动拉到不兼容的新版本。生产环境建议锁定具体版本号(见第 8 节),更新前先读 release notes。

⑤ 启动并自检

BASH
docker compose up -d
docker compose ps
# 看日志,出现 "Rocket has launched from http://0.0.0.0:80" 即正常
docker compose logs -f vaultwarden
容器已在内网 vw-net 上运行,但尚未接入公网。下一节把 vault.你的域名.com 接进来并上 HTTPS。

主线用 Cloudflare Tunnel(两种部署都适用、零开放端口、隐藏源站 IP);若你不想依赖 CF 或家里已做端口转发,用 Caddy 反代。

方案需开入站端口隐藏源站 IP证书适用
Cloudflare Tunnel 推荐否是CF 自动PVE 家用 + VPS 都行
Caddy 反代 备选需 80/443默认否Let's Encrypt 自动VPS,或家里已端口转发

① 在 Zero Trust 控制台建隧道

打开 one.dash.cloudflare.com → Networks → Tunnels → Create a tunnel → 选 Cloudflared → 命名 vaultwarden → Save。保存后复制它给出的 隧道令牌(eyJ... 一长串)。

② token 写入 .env,compose 加入 cloudflared

先把隧道令牌写进 .env:

BASH · 一条命令创建
cat > /opt/vaultwarden/.env <<'EOF'
TUNNEL_TOKEN=eyJ...你的隧道令牌...
EOF
ENV · /opt/vaultwarden/.env
TUNNEL_TOKEN=eyJ...你的隧道令牌...
BASH · 收紧权限
chmod 600 /opt/vaultwarden/.env

再用下面的完整版覆盖 docker-compose.yml(在基础版上加了 cloudflared 服务):

BASH · 覆盖写入
cat > /opt/vaultwarden/docker-compose.yml <<'EOF'
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file: ./vaultwarden.env
    volumes:
      - ./vw-data:/data
    networks: [vw-net]

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    env_file: ./.env
    networks: [vw-net]
    depends_on: [vaultwarden]

networks:
  vw-net:
    name: vw-net
EOF
cloudflared 服务用 env_file: ./.env 直接把 .env 里的 TUNNEL_TOKEN 注入容器,不需要 compose 变量插值。
YAML · /opt/vaultwarden/docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file: ./vaultwarden.env
    volumes:
      - ./vw-data:/data
    networks: [vw-net]

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    env_file: ./.env
    networks: [vw-net]
    depends_on: [vaultwarden]

networks:
  vw-net:
    name: vw-net

③ 配置 Public Hostname

回到该隧道 → Public Hostname → Add a public hostname:

Subdomainvault
Domain你的域名.com
Service TypeHTTP
URLvaultwarden:80
URL 填 vaultwarden:80,是因为 cloudflared 与 vaultwarden 在同一 docker 网络,直接用容器名互访,无需映射任何端口到宿主机。DNS 的 CNAME 记录会自动创建。

④ 启动并验证

BASH
cd /opt/vaultwarden
docker compose up -d
# 看到 Registered tunnel connection 即隧道已连上
docker compose logs cloudflared
浏览器打开 https://vault.你的域名.com,应看到 Vaultwarden 登录页,证书由 Cloudflare 自动签发。IP_HEADER=CF-Connecting-IP 已在 env 里设好,第 6 节 Fail2ban 才能拿到真实访客 IP。

适用于 VPS,或家里已把 80/443 端口转发到本机的场景。

① DNS 记录

在 Cloudflare 给 vault.你的域名.com 加一条 A 记录指向本机公网 IP。首次签证书请先设为 DNS only(灰云),让 Caddy 能通过 Let's Encrypt 验证。

② 放行端口

VPS 上执行(见第 1 节):ufw allow 80/tcp && ufw allow 443/tcp。

③ 写 Caddyfile

BASH · 一条命令创建
cat > /opt/vaultwarden/Caddyfile <<'EOF'
vault.你的域名.com {
    encode zstd gzip
    reverse_proxy vaultwarden:80 {
        header_up X-Real-IP {remote_host}
    }
}
EOF
CADDY · /opt/vaultwarden/Caddyfile
vault.你的域名.com {
    encode zstd gzip
    reverse_proxy vaultwarden:80 {
        header_up X-Real-IP {remote_host}
    }
}

④ 用完整版覆盖 docker-compose.yml

在基础版上加了 caddy 服务与具名卷,直接覆盖原文件:

BASH · 覆盖写入
cat > /opt/vaultwarden/docker-compose.yml <<'EOF'
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file: ./vaultwarden.env
    volumes:
      - ./vw-data:/data
    networks: [vw-net]

  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    networks: [vw-net]
    depends_on: [vaultwarden]

networks:
  vw-net:
    name: vw-net

volumes:
  caddy-data:
  caddy-config:
EOF
YAML · /opt/vaultwarden/docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file: ./vaultwarden.env
    volumes:
      - ./vw-data:/data
    networks: [vw-net]

  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    networks: [vw-net]
    depends_on: [vaultwarden]

networks:
  vw-net:
    name: vw-net

volumes:
  caddy-data:
  caddy-config:

⑤ 改 IP 头并启动

把 vaultwarden.env 里的 IP_HEADER 改成 X-Real-IP(Caddy 灰云直连场景),然后:

BASH
cd /opt/vaultwarden
docker compose up -d
# 看到 certificate obtained 即签证成功
docker compose logs caddy
想隐藏 IP + 上 WAF:Caddy 签好证书后,在 CF 把该记录切橙云,SSL/TLS 模式设 Full (strict)。注意橙云后 Caddy 看到的是 CF 边缘 IP,要拿真实访客 IP 需让 Caddy 信任 CF 网段并读取 CF-Connecting-IP(进阶,可选)。

① 注册你的账户

打开 https://vault.你的域名.com → Create account → 填邮箱 + 强主密码 → 完成注册并登录。

主密码请用四词以上的高强度口令,并离线另存一份。它是解密整库的唯一钥匙,丢失无法找回。

② 立即关闭公开注册

你的账户建好后,马上堵死注册入口。编辑 vaultwarden.env:

ENV · 修改
SIGNUPS_ALLOWED=false
BASH
cd /opt/vaultwarden && docker compose up -d
此后陌生人无法注册,新增成员只能由你在 Admin 后台邀请。

③ 开启 2FA(两步验证)

登录 → 右上头像 → Account settings → Security → Two-step login → 启用 Authenticator app,用手机验证器 App 扫码绑定,并保存恢复码。

④ 进入 Admin 后台

打开 https://vault.你的域名.com/admin → 输入第 2 节生成令牌时设的口令。可在此邀请家人(Users)、看诊断(Diagnostics)、核对 Domain。

Admin 后台里改的设置会写进 vw-data/config.json 并覆盖 env 里的同名项。想用 env 统一管理,就别在后台改同一个开关。

⑤ 安装并连接客户端

浏览器扩展 / 手机 App / 桌面端,在登录界面选 自托管(Self-hosted),服务器地址填 https://vault.你的域名.com,再用刚注册的账户登录。

目标:电脑端改动后,手机 App 秒级收到同步。

① 申请安装 ID / Key

打开 bitwarden.com/host → 选国际版(默认 .com) → 填服务器管理邮箱 → 提交,得到 Installation ID 与 Installation Key。

② 填入 vaultwarden.env

ENV · 修改这几行
PUSH_ENABLED=true
PUSH_INSTALLATION_ID=你的 Installation ID
PUSH_INSTALLATION_KEY=你的 Installation Key
PUSH_RELAY_URI=https://push.bitwarden.com
PUSH_IDENTITY_URI=https://identity.bitwarden.com

③ 重建生效

BASH
cd /opt/vaultwarden && docker compose up -d

④ 手机端

手机 App(已按第 4 节把服务器指向 vault.你的域名.com)重新登录一次,之后同步即为实时。

推送只对手机 App 生效(浏览器扩展/桌面端靠自身 WebSocket 实时同步)。区域必须是 .com,与 bitwarden.com/host 注册区一致,否则推送握手失败。

目标:同一 IP 反复登录失败(含 /admin)自动封禁一段时间。

前提:第 2 节已开 LOG_FILE 与 EXTENDED_LOGGING,且 IP_HEADER 与接入方式匹配(CF Tunnel → CF-Connecting-IP;Caddy 灰云 → X-Real-IP),否则会把代理 IP 当成攻击者误封。

① 安装

BASH
apt install -y fail2ban

② 过滤规则(通用)

BASH · 一条命令创建
cat > /etc/fail2ban/filter.d/vaultwarden.conf <<'EOF'
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <HOST>\..*$
            ^.*Invalid admin token\. IP: <HOST>.*$
ignoreregex =
EOF
INI · /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <HOST>\..*$
            ^.*Invalid admin token\. IP: <HOST>.*$
ignoreregex =

③ 按接入方式选择封禁动作

走 CF Tunnel/橙云时,请求都经 Cloudflare 到达源站,本地封 IP 无效,必须在 CF 边缘封。

建 API Token:dash.cloudflare.com → My Profile → API Tokens → Create Token → Custom token,权限选 Zone · Firewall Services · Edit,限定到 你的域名.com 所在的 Zone,复制 token。

封禁接口用的是 Zone 级端点 /zones/{zone_id}/firewall/access_rules/rules,Token 只需 Zone 权限即可。{zone_id} 在 Cloudflare 域名总览页右侧栏复制(32 位十六进制串),替换掉配置里的 {zone_id} 占位。
BASH · 创建封禁动作(先替换 token)
cat > /etc/fail2ban/action.d/cloudflare-token.conf <<'EOF'
[Definition]
actionban = curl -s -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules" -H "Authorization: Bearer <cftoken>" -H "Content-Type: application/json" --data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"fail2ban-vaultwarden"}'
actionunban = id=$(curl -s "https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules?configuration.value=<ip>&mode=block" -H "Authorization: Bearer <cftoken>" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4); [ -n "$id" ] && curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules/$id" -H "Authorization: Bearer <cftoken>"
[Init]
cftoken = YOUR_CF_API_TOKEN
EOF
INI · /etc/fail2ban/action.d/cloudflare-token.conf
[Definition]
actionban = curl -s -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules" -H "Authorization: Bearer <cftoken>" -H "Content-Type: application/json" --data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"fail2ban-vaultwarden"}'
actionunban = id=$(curl -s "https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules?configuration.value=<ip>&mode=block" -H "Authorization: Bearer <cftoken>" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4); [ -n "$id" ] && curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules/$id" -H "Authorization: Bearer <cftoken>"
[Init]
cftoken = YOUR_CF_API_TOKEN
YOUR_CF_API_TOKEN 必须替换为第 6 节开头建好的 API Token,不替换则封禁动作静默失败(curl 返回 401 但 fail2ban 不报错),被 ban 的 IP 实际没封住。
BASH · 创建 jail
cat > /etc/fail2ban/jail.d/vaultwarden.local <<'EOF'
[vaultwarden]
enabled   = true
filter    = vaultwarden
logpath   = /opt/vaultwarden/vw-data/vaultwarden.log
maxretry  = 5
findtime  = 600
bantime   = 86400
banaction = cloudflare-token
EOF
INI · /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled   = true
filter    = vaultwarden
logpath   = /opt/vaultwarden/vw-data/vaultwarden.log
maxretry  = 5
findtime  = 600
bantime   = 86400
banaction = cloudflare-token
更省事的边缘防护:CF 控制台 Security → WAF → Rate limiting rules 对路径 /identity/connect/token 限速,可不依赖 fail2ban。两者也能叠加。
Vaultwarden/Caddy 通过 docker 发布端口,普通 iptables INPUT 链拦不住转发流量,必须封在 DOCKER-USER 链。
BASH · 创建封禁动作
cat > /etc/fail2ban/action.d/docker-user.conf <<'EOF'
[Definition]
actionstart =
actionstop =
actioncheck =
actionban   = iptables -I DOCKER-USER -s <ip> -j DROP
actionunban = iptables -D DOCKER-USER -s <ip> -j DROP
EOF
INI · /etc/fail2ban/action.d/docker-user.conf
[Definition]
actionstart =
actionstop =
actioncheck =
actionban   = iptables -I DOCKER-USER -s <ip> -j DROP
actionunban = iptables -D DOCKER-USER -s <ip> -j DROP
BASH · 创建 jail
cat > /etc/fail2ban/jail.d/vaultwarden.local <<'EOF'
[vaultwarden]
enabled   = true
filter    = vaultwarden
logpath   = /opt/vaultwarden/vw-data/vaultwarden.log
maxretry  = 5
findtime  = 600
bantime   = 3600
banaction = docker-user
EOF
INI · /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled   = true
filter    = vaultwarden
logpath   = /opt/vaultwarden/vw-data/vaultwarden.log
maxretry  = 5
findtime  = 600
bantime   = 3600
banaction = docker-user

④ 启动并查看

BASH
systemctl enable --now fail2ban
systemctl restart fail2ban
# 查看本 jail 状态与已封 IP
fail2ban-client status vaultwarden
故意错登几次(超过 maxretry)即可看到对应 IP 被封。若 jail 启动报错找不到日志,先确认 Vaultwarden 已产生 vw-data/vaultwarden.log。

目标:定时把数据库+附件+配置打包、GPG 加密、本地轮转,并自动推一份到异地云。

① 装依赖 + 设加密口令

BASH
apt install -y sqlite3 gnupg
# rclone(异地上云用)
curl https://rclone.org/install.sh | bash
# 备份加密口令 —— 请离线另存,丢了备份永远打不开
printf '你的高强度备份口令' > /root/.vw_backup_pass
chmod 600 /root/.vw_backup_pass
/root/.vw_backup_pass 里的口令是解开所有备份的唯一钥匙,务必离线另存一份。它和账户主密码是两回事,别混用。

② 配置 rclone 异地目标(选你的云)

先在 Cloudflare R2 建桶 vaultwarden,并创建 R2 API Token(拿到 Access Key ID + Secret + 账户 ID)。

BASH
rclone config create vwbackup s3 \
  provider Cloudflare \
  access_key_id 你的KEY \
  secret_access_key 你的SECRET \
  endpoint https://账户ID.r2.cloudflarestorage.com \
  acl private
rclone mkdir vwbackup:vaultwarden
rclone lsd vwbackup:            # 能列出桶即配置成功

OneDrive / Google Drive 需浏览器 OAuth 授权,无头服务器走远程授权:

BASH · 交互式
rclone config
# n 新建 → 名字填 vwbackup → 类型选 onedrive 或 drive
# client_id/secret 直接回车留空
# 问 "Use auto config?" 选 n(无头服务器关键)
# 它会提示你在有浏览器的电脑上执行:
#     rclone authorize "onedrive"   (或 "drive")
# 登录授权后把返回的一长串 token 粘回服务器即可
rclone mkdir vwbackup:vaultwarden
Google Drive 建议自建 client_id 以提升 API 配额稳定性(可选);OneDrive 直接留空即可。

通用 S3(AWS / MinIO / 阿里云 OSS / 腾讯 COS 等 S3 兼容):

BASH · S3 兼容
rclone config create vwbackup s3 \
  provider AWS \
  access_key_id 你的KEY \
  secret_access_key 你的SECRET \
  region 区域 \
  endpoint 自建填地址;AWS 留空
rclone mkdir vwbackup:vaultwarden

或推到另一台服务器(SFTP):

BASH · SFTP
rclone config create vwbackup sftp \
  host 目标IP \
  user 用户名 \
  key_file /root/.ssh/id_ed25519
rclone mkdir vwbackup:vaultwarden

③ 备份脚本 /opt/vaultwarden/backup.sh

BASH · 一条命令创建
cat > /opt/vaultwarden/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# ── 配置(按需改)──
DATA_DIR="/opt/vaultwarden/vw-data"
BACKUP_DIR="/opt/vaultwarden/backups"
PASSFILE="/root/.vw_backup_pass"
RCLONE_REMOTE="vwbackup:vaultwarden"    # 第 ② 步配好的远程
KEEP=14                                 # 本地/云端保留份数(天)

STAMP=$(date +%Y%m%d-%H%M%S)
WORK=$(mktemp -d)
mkdir -p "$BACKUP_DIR"

# 1) SQLite 一致性快照(禁止直接 cp)
sqlite3 "$DATA_DIR/db.sqlite3" ".backup \'$WORK/db.sqlite3\'"

# 2) 收集其余数据
cp -a "$DATA_DIR/config.json" "$WORK/" 2>/dev/null || true
cp -a "$DATA_DIR"/rsa_key*    "$WORK/" 2>/dev/null || true
cp -a "$DATA_DIR"/*.pem       "$WORK/" 2>/dev/null || true
[ -d "$DATA_DIR/attachments" ] && cp -a "$DATA_DIR/attachments" "$WORK/"
[ -d "$DATA_DIR/sends" ]       && cp -a "$DATA_DIR/sends" "$WORK/"

# 3) 打包并直接 GPG AES-256 加密(不落地明文)
OUT="$BACKUP_DIR/vw-$STAMP.tar.gz.gpg"
tar -czf - -C "$WORK" . | gpg --batch --yes --symmetric --cipher-algo AES256 \
  --passphrase-file "$PASSFILE" -o "$OUT"
rm -rf "$WORK"

# 4) 推送异地
rclone copy "$OUT" "$RCLONE_REMOTE/" --no-traverse

# 5) 本地 + 云端轮转
ls -1t "$BACKUP_DIR"/vw-*.tar.gz.gpg | tail -n +$((KEEP+1)) | xargs -r rm -f
rclone delete "$RCLONE_REMOTE/" --min-age ${KEEP}d 2>/dev/null || true

echo "[OK] $STAMP -> $OUT (+ $RCLONE_REMOTE)"
EOF
用带引号的 <<'EOF',脚本里的 $WORK、$(date ...) 等会原样写进文件,在运行时才求值,而不是创建时。
BASH · /opt/vaultwarden/backup.sh
#!/usr/bin/env bash
set -euo pipefail

# ── 配置(按需改)──
DATA_DIR="/opt/vaultwarden/vw-data"
BACKUP_DIR="/opt/vaultwarden/backups"
PASSFILE="/root/.vw_backup_pass"
RCLONE_REMOTE="vwbackup:vaultwarden"    # 第 ② 步配好的远程
KEEP=14                                 # 本地/云端保留份数(天)

STAMP=$(date +%Y%m%d-%H%M%S)
WORK=$(mktemp -d)
mkdir -p "$BACKUP_DIR"

# 1) SQLite 一致性快照(禁止直接 cp)
sqlite3 "$DATA_DIR/db.sqlite3" ".backup '$WORK/db.sqlite3'"

# 2) 收集其余数据
cp -a "$DATA_DIR/config.json" "$WORK/" 2>/dev/null || true
cp -a "$DATA_DIR"/rsa_key*    "$WORK/" 2>/dev/null || true
cp -a "$DATA_DIR"/*.pem       "$WORK/" 2>/dev/null || true
[ -d "$DATA_DIR/attachments" ] && cp -a "$DATA_DIR/attachments" "$WORK/"
[ -d "$DATA_DIR/sends" ]       && cp -a "$DATA_DIR/sends" "$WORK/"

# 3) 打包并直接 GPG AES-256 加密(不落地明文)
OUT="$BACKUP_DIR/vw-$STAMP.tar.gz.gpg"
tar -czf - -C "$WORK" . | gpg --batch --yes --symmetric --cipher-algo AES256 \
  --passphrase-file "$PASSFILE" -o "$OUT"
rm -rf "$WORK"

# 4) 推送异地
rclone copy "$OUT" "$RCLONE_REMOTE/" --no-traverse

# 5) 本地 + 云端轮转
ls -1t "$BACKUP_DIR"/vw-*.tar.gz.gpg | tail -n +$((KEEP+1)) | xargs -r rm -f
rclone delete "$RCLONE_REMOTE/" --min-age ${KEEP}d 2>/dev/null || true

echo "[OK] $STAMP -> $OUT (+ $RCLONE_REMOTE)"
BASH
chmod +x /opt/vaultwarden/backup.sh
/opt/vaultwarden/backup.sh          # 先手动跑一次验证

④ 定时任务(每天 04:30)

BASH · crontab -e 追加一行
30 4 * * * /opt/vaultwarden/backup.sh >> /var/log/vw-backup.log 2>&1

⑤ 恢复演练

BASH
cd /opt/vaultwarden
docker compose down
mkdir -p /tmp/vwrestore
gpg --batch --yes --passphrase-file /root/.vw_backup_pass \
  -d backups/vw-时间戳.tar.gz.gpg | tar -xzf - -C /tmp/vwrestore
mv vw-data vw-data.broken.$(date +%s)
mkdir vw-data && cp -a /tmp/vwrestore/. vw-data/
docker compose up -d
恢复时 DOMAIN 必须与备份时一致。建议每隔一段时间做一次恢复演练——没验证过能恢复的备份不算备份。

⑥ 整机重建(服务器崩了 / 换新机)

下面三样必须离线另存(纸质 / 离线设备),崩机后全靠它们恢复,缺一不可:① 备份加密口令(/root/.vw_backup_pass 里那串);② R2 的 Access Key ID / Secret / 账户ID;③ 你的 DOMAIN(vault.你的域名.com)。只丢加密口令→备份永远打不开;只丢 R2 key→取不回备份。

第 1 步 新机按第 1 节装好 Debian 12 + Docker,重建目录:

BASH · 新机
mkdir -p /opt/vaultwarden/vw-data
cd /opt/vaultwarden

第 2 步 按第 2 节重写 vaultwarden.env(DOMAIN 必须与原来一致;ADMIN_TOKEN 可用第 2 节命令重新生成),按第 3 节重写 docker-compose.yml 与 .env(TUNNEL_TOKEN)。

第 3 步 装 rclone,用离线存的 key 重配 R2:

BASH · 新机
apt install -y rclone gnupg
rclone config create vwbackup s3 \
  provider Cloudflare \
  access_key_id 你的KEY \
  secret_access_key 你的SECRET \
  endpoint https://账户ID.r2.cloudflarestorage.com \
  acl private
rclone config update vwbackup no_check_bucket true

第 4 步 恢复加密口令文件:

BASH · 新机
printf '你离线存的备份口令' > /root/.vw_backup_pass
chmod 600 /root/.vw_backup_pass

第 5 步 从 R2 拉回最新备份并解密解压进 vw-data:

BASH · 新机
rclone lsf vwbackup:vaultwarden          # 看云端有哪些,挑最新一份
rclone copy vwbackup:vaultwarden/vw-时间戳.tar.gz.gpg /opt/vaultwarden/backups/
mkdir -p /tmp/vwrestore
gpg --batch --yes --passphrase-file /root/.vw_backup_pass \
  -d /opt/vaultwarden/backups/vw-时间戳.tar.gz.gpg | tar -xzf - -C /tmp/vwrestore
cp -a /tmp/vwrestore/. /opt/vaultwarden/vw-data/

第 6 步 起服务:

BASH · 新机
docker compose up -d
docker compose logs -f cloudflared      # 出现 Registered tunnel connection 即隧道已连

第 7 步 Cloudflare 侧,按令牌是否还在分两种:

  • 原隧道令牌还在(已写回 .env)→ cloudflared 直接连上,公开主机名无需改动。
  • 令牌也丢了 → 按第 3 节重建隧道,新 token 写进 .env,在已发布应用程序路由重配 vault → 你的域名.com → HTTP → vaultwarden:80。
浏览器打开 https://vault.你的域名.com,用原账户主密码登录,数据都在即整机恢复成功。主密码在客户端本地解密,不依赖服务器,换机不影响。

上线前逐条核对。前面各节已做的在此复查,另有几项收尾加固。

  • SIGNUPS_ALLOWED=false —— 注册已关闭(第 4 节)
  • ADMIN_TOKEN 为 Argon2 哈希,非明文(第 2 节)
  • 你的账户已开启 2FA(第 4 节)
  • 公网零开放端口(CF Tunnel)或仅放行 80/443(Caddy);SSH 仅密钥登录
  • Fail2ban 正在运行且规则命中(第 6 节)
  • 备份 cron 正常,加密口令已离线另存(第 7 节)
  • 镜像 tag 已从 :latest 固定到具体版本
  • 已建立定期更新习惯

① 收紧额外环境变量

vaultwarden.env · 追加/确认
SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=false      # 不用邀请功能就关掉
SHOW_PASSWORD_HINT=false       # 不在登录页泄露密码提示
PASSWORD_ITERATIONS=600000     # 新账户客户端 KDF 迭代次数

改完照例重建容器生效:

BASH
cd /opt/vaultwarden && docker compose up -d

② 固定镜像版本

把 docker-compose.yml 里的 vaultwarden/server:latest 改成具体版本(去 Releases 查最新稳定号),例如 vaultwarden/server:1.32.0,避免自动拉到不兼容的新镜像。

③ 定期更新

BASH
cd /opt/vaultwarden
docker compose pull        # 拉新镜像(先把 tag 调到目标版本)
docker compose up -d       # 平滑重建
docker image prune -f      # 清理旧镜像
更新前先手动跑一次 backup.sh。升级 cloudflared / caddy 同理:改 tag → pull → up -d。

④ 防火墙复查

BASH
ufw status verbose
# CF Tunnel 方案:应只见 OpenSSH,对公网无 80/443
# Caddy 方案:仅 OpenSSH + 80/443,其余一律 deny

先看这四条日志

BASH · 在 /opt/vaultwarden 下
docker compose ps                    # 容器是否都 Up
docker compose logs -f vaultwarden   # 主程序日志
docker compose logs cloudflared      # 隧道日志(CF 方案)
fail2ban-client status vaultwarden   # 封禁与命中情况
现象常见原因 → 处理
访问报 502 / Bad Gateway cloudflared 没连上,或 Public Hostname 指向写错。查 docker compose logs cloudflared;确认 Service 填的是 http://vaultwarden:80。
页面一直转圈 / 登录不进 DOMAIN 必须是 https:// 且与实际访问域名完全一致;CF 上 pass 记录须橙云代理。看 docker compose logs vaultwarden。
手机收不到推送 推送区域与注册的 relay 不一致(本教程统一 .com);确认 PUSH_ENABLED=true 且 ID/KEY 正确;手机端退出重新登录一次。
Fail2ban 不封 / 误封 IP_HEADER 与接入方式不匹配(CF→CF-Connecting-IP,Caddy 灰云→X-Real-IP);LOG_FILE/EXTENDED_LOGGING 没开。tail -f vw-data/vaultwarden.log 看是否有带真实 IP 的失败行。
忘记 ADMIN_TOKEN 重新用第 2 节的 hash 命令生成一串,覆盖 env 里的 ADMIN_TOKEN,再 docker compose up -d。
Caddy 签不到证书(HTTPS 失败) 该子域名 A 记录必须灰云(DNS-only)且 80/443 对公网可达,Caddy 才能过 Let's Encrypt 验证。看 docker compose logs caddy。
容器起不来 / 反复重启 docker compose logs 看报错;检查 env 值里的 $ 是否按第 2 节处理;PVE-LXC 缺 nesting=1,keyctl=1 会导致 Docker 异常。
换机 / 迁移数据 / 服务器崩了 旧机还在:停容器 → 整个 vw-data 目录拷到新机 → 保持 DOMAIN 一致 → docker compose up -d。旧机已废 / 整机崩溃:按 第 7 节 ⑥ 整机重建 从 R2 加密备份恢复。
到这里,一套零开放端口、自动 HTTPS、手机实时推送、防爆破、每日异地加密备份的 Vaultwarden 就跑起来了。官方文档随时可查:Vaultwarden Wiki。