Vaultwarden 自建密码库
Vaultwarden 稳定安全部署:PVE LXC / 公网 VPS 双环境,Cloudflare Tunnel / Caddy 双接入,手机推送、Fail2ban 防爆破、GPG 加密备份。
本教程覆盖两种部署,均为容器化,配置文件完全通用,只是宿主环境不同:
| 场景 | 宿主 | 特点 |
|---|---|---|
| 本地 PVE 家用推荐 | Debian 12 LXC 容器内跑 Docker | 数据完全在自己手里,快照/备份方便,开销极低 |
| 公网 VPS | Debian 12 直接跑 Docker | 7×24 在线,无需内网穿透 |
最终访问链路(主线方案 Cloudflare Tunnel):
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 加密 + 异地上云的自动备份
sudo)。代码里带黄色底色的值(如 vault.你的域名.com)是需要你按自己情况替换的占位。按你的场景选择对应标签页完成宿主准备,最后两者共用同一套 Docker 安装步骤。
以下命令在 PVE 宿主机 shell 执行。
① 下载 Debian 12 模板(若已有可跳过)
pveam update
pveam available | grep debian-12
# 版本号以上面输出为准,下面 12.7-1 只是示例
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
② 创建 LXC 容器
# 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 补上后重启容器。③ 启动并进入容器
pct start 110 pct enter 110 # 进入后先设时区、更新系统 apt update && apt -y upgrade apt install -y curl ca-certificates gnupg
之后所有步骤都在容器内执行,继续往下看"共用:安装 Docker"。
以 root SSH 登录你的 Debian 12 VPS。
① 更新系统与基础工具
apt update && apt -y upgrade apt install -y curl sudo ca-certificates gnupg
② 防火墙(ufw)
主线走 Cloudflare Tunnel 时无需放行 80/443,只留 SSH,攻击面最小。
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 加固
# 务必先确认已能用密钥免密登录,再执行下面两行!
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):
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
docker run --rm hello-world docker compose version
Docker Compose version v2.x 版本号,即环境就绪。① 建立目录
mkdir -p /opt/vaultwarden/vw-data cd /opt/vaultwarden
② 生成 Argon2 加密的 Admin 令牌
# 按提示输入两次令牌口令,复制输出的整串 $argon2id$...
docker run --rm -it vaultwarden/server /vaultwarden hash
/admin 后台用的,请单独记好,不要与账户主密码相同。③ 写入环境变量文件 vaultwarden.env
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 解析。执行前务必把两处高亮占位改成你的实际值。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 本体。
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
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。⑤ 启动并自检
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:
cat > /opt/vaultwarden/.env <<'EOF' TUNNEL_TOKEN=eyJ...你的隧道令牌... EOF
TUNNEL_TOKEN=eyJ...你的隧道令牌...
chmod 600 /opt/vaultwarden/.env
再用下面的完整版覆盖 docker-compose.yml(在基础版上加了 cloudflared 服务):
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
env_file: ./.env 直接把 .env 里的 TUNNEL_TOKEN 注入容器,不需要 compose 变量插值。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:
| Subdomain | vault |
| Domain | 你的域名.com |
| Service Type | HTTP |
| URL | vaultwarden:80 |
vaultwarden:80,是因为 cloudflared 与 vaultwarden 在同一 docker 网络,直接用容器名互访,无需映射任何端口到宿主机。DNS 的 CNAME 记录会自动创建。④ 启动并验证
cd /opt/vaultwarden
docker compose up -d
# 看到 Registered tunnel connection 即隧道已连上
docker compose logs cloudflared
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
cat > /opt/vaultwarden/Caddyfile <<'EOF' vault.你的域名.com { encode zstd gzip reverse_proxy vaultwarden:80 { header_up X-Real-IP {remote_host} } } EOF
vault.你的域名.com {
encode zstd gzip
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
④ 用完整版覆盖 docker-compose.yml
在基础版上加了 caddy 服务与具名卷,直接覆盖原文件:
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
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 灰云直连场景),然后:
cd /opt/vaultwarden
docker compose up -d
# 看到 certificate obtained 即签证成功
docker compose logs caddy
CF-Connecting-IP(进阶,可选)。① 注册你的账户
打开 https://vault.你的域名.com → Create account → 填邮箱 + 强主密码 → 完成注册并登录。
② 立即关闭公开注册
你的账户建好后,马上堵死注册入口。编辑 vaultwarden.env:
SIGNUPS_ALLOWED=false
cd /opt/vaultwarden && docker compose up -d
③ 开启 2FA(两步验证)
登录 → 右上头像 → Account settings → Security → Two-step login → 启用 Authenticator app,用手机验证器 App 扫码绑定,并保存恢复码。
④ 进入 Admin 后台
打开 https://vault.你的域名.com/admin → 输入第 2 节生成令牌时设的口令。可在此邀请家人(Users)、看诊断(Diagnostics)、核对 Domain。
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
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
③ 重建生效
cd /opt/vaultwarden && docker compose up -d
④ 手机端
手机 App(已按第 4 节把服务器指向 vault.你的域名.com)重新登录一次,之后同步即为实时。
bitwarden.com/host 注册区一致,否则推送握手失败。目标:同一 IP 反复登录失败(含 /admin)自动封禁一段时间。
LOG_FILE 与 EXTENDED_LOGGING,且 IP_HEADER 与接入方式匹配(CF Tunnel → CF-Connecting-IP;Caddy 灰云 → X-Real-IP),否则会把代理 IP 当成攻击者误封。① 安装
apt install -y fail2ban
② 过滤规则(通用)
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
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <HOST>\..*$
^.*Invalid admin token\. IP: <HOST>.*$
ignoreregex =
③ 按接入方式选择封禁动作
建 API Token:dash.cloudflare.com → My Profile → API Tokens → Create Token → Custom token,权限选 Zone · Firewall Services · Edit,限定到 你的域名.com 所在的 Zone,复制 token。
/zones/{zone_id}/firewall/access_rules/rules,Token 只需 Zone 权限即可。{zone_id} 在 Cloudflare 域名总览页右侧栏复制(32 位十六进制串),替换掉配置里的 {zone_id} 占位。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
[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 实际没封住。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
[vaultwarden] enabled = true filter = vaultwarden logpath = /opt/vaultwarden/vw-data/vaultwarden.log maxretry = 5 findtime = 600 bantime = 86400 banaction = cloudflare-token
/identity/connect/token 限速,可不依赖 fail2ban。两者也能叠加。DOCKER-USER 链。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
[Definition] actionstart = actionstop = actioncheck = actionban = iptables -I DOCKER-USER -s <ip> -j DROP actionunban = iptables -D DOCKER-USER -s <ip> -j DROP
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
[vaultwarden] enabled = true filter = vaultwarden logpath = /opt/vaultwarden/vw-data/vaultwarden.log maxretry = 5 findtime = 600 bantime = 3600 banaction = docker-user
④ 启动并查看
systemctl enable --now fail2ban
systemctl restart fail2ban
# 查看本 jail 状态与已封 IP
fail2ban-client status vaultwarden
vw-data/vaultwarden.log。目标:定时把数据库+附件+配置打包、GPG 加密、本地轮转,并自动推一份到异地云。
① 装依赖 + 设加密口令
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)。
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 授权,无头服务器走远程授权:
rclone config # n 新建 → 名字填 vwbackup → 类型选 onedrive 或 drive # client_id/secret 直接回车留空 # 问 "Use auto config?" 选 n(无头服务器关键) # 它会提示你在有浏览器的电脑上执行: # rclone authorize "onedrive" (或 "drive") # 登录授权后把返回的一长串 token 粘回服务器即可 rclone mkdir vwbackup:vaultwarden
通用 S3(AWS / MinIO / 阿里云 OSS / 腾讯 COS 等 S3 兼容):
rclone config create vwbackup s3 \ provider AWS \ access_key_id 你的KEY \ secret_access_key 你的SECRET \ region 区域 \ endpoint 自建填地址;AWS 留空 rclone mkdir vwbackup:vaultwarden
或推到另一台服务器(SFTP):
rclone config create vwbackup sftp \ host 目标IP \ user 用户名 \ key_file /root/.ssh/id_ed25519 rclone mkdir vwbackup:vaultwarden
③ 备份脚本 /opt/vaultwarden/backup.sh
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 ...) 等会原样写进文件,在运行时才求值,而不是创建时。#!/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)"
chmod +x /opt/vaultwarden/backup.sh
/opt/vaultwarden/backup.sh # 先手动跑一次验证
④ 定时任务(每天 04:30)
30 4 * * * /opt/vaultwarden/backup.sh >> /var/log/vw-backup.log 2>&1
⑤ 恢复演练
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,重建目录:
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:
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 步 恢复加密口令文件:
printf '你离线存的备份口令' > /root/.vw_backup_pass
chmod 600 /root/.vw_backup_pass
第 5 步 从 R2 拉回最新备份并解密解压进 vw-data:
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 步 起服务:
docker compose up -d
docker compose logs -f cloudflared # 出现 Registered tunnel connection 即隧道已连
第 7 步 Cloudflare 侧,按令牌是否还在分两种:
- 原隧道令牌还在(已写回
.env)→ cloudflared 直接连上,公开主机名无需改动。 - 令牌也丢了 → 按第 3 节重建隧道,新 token 写进
.env,在已发布应用程序路由重配vault→你的域名.com→HTTP→vaultwarden:80。
上线前逐条核对。前面各节已做的在此复查,另有几项收尾加固。
SIGNUPS_ALLOWED=false—— 注册已关闭(第 4 节)ADMIN_TOKEN为 Argon2 哈希,非明文(第 2 节)- 你的账户已开启 2FA(第 4 节)
- 公网零开放端口(CF Tunnel)或仅放行 80/443(Caddy);SSH 仅密钥登录
- Fail2ban 正在运行且规则命中(第 6 节)
- 备份 cron 正常,加密口令已离线另存(第 7 节)
- 镜像 tag 已从
:latest固定到具体版本 - 已建立定期更新习惯
① 收紧额外环境变量
SIGNUPS_ALLOWED=false INVITATIONS_ALLOWED=false # 不用邀请功能就关掉 SHOW_PASSWORD_HINT=false # 不在登录页泄露密码提示 PASSWORD_ITERATIONS=600000 # 新账户客户端 KDF 迭代次数
改完照例重建容器生效:
cd /opt/vaultwarden && docker compose up -d
② 固定镜像版本
把 docker-compose.yml 里的 vaultwarden/server:latest 改成具体版本(去 Releases 查最新稳定号),例如 vaultwarden/server:1.32.0,避免自动拉到不兼容的新镜像。
③ 定期更新
cd /opt/vaultwarden docker compose pull # 拉新镜像(先把 tag 调到目标版本) docker compose up -d # 平滑重建 docker image prune -f # 清理旧镜像
backup.sh。升级 cloudflared / caddy 同理:改 tag → pull → up -d。④ 防火墙复查
ufw status verbose # CF Tunnel 方案:应只见 OpenSSH,对公网无 80/443 # Caddy 方案:仅 OpenSSH + 80/443,其余一律 deny
先看这四条日志
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 加密备份恢复。 |