Vaultwarden VPS 备份与恢复:SQLite 正确方法
使用 sqlite3 .backup 复制运行中的 Vaultwarden 数据库,并保留 attachments、config.json 和 rsa_key 文件。本文还会验证恢复结果,避免备份到真正需要时才发现无效。
Vaultwarden 备份必须包含哪些内容
Vaultwarden 备份是整个数据目录的副本,其中的数据库必须使用正确方式复制。请运行 sqlite3 db.sqlite3 ".backup out.sqlite3",不要运行 cp,因为直接复制正在写入的数据库,可能得到无法打开的文件。还要保留与数据库位于同一目录的其他文件,这部分最容易被忽略。
在 Docker 安装中,数据目录就是挂载到 /data 的目录。它可能是主机上的路径,也可能是命名卷;bind mount 与命名卷的区别决定了 Vaultwarden 数据实际存储在磁盘上的位置。该目录包含以下内容。
db.sqlite3:所有账户、保险库项目、文件夹和组织。丢失此文件就会丢失整个保险库。db.sqlite3-wal和db.sqlite3-shm:预写式日志(WAL)及其共享内存索引。SQLite 将最近的写入合并到主文件前,这些写入会保存在这里。attachments/:用户附加到保险库项目的文件。文件经过加密,并按项目分别存放在目录中。sends/:Bitwarden Send 链接对应的文件。config.json:您在管理页面中保存的所有设置。rsa_key.pem,以及旧版安装中的rsa_key.der和rsa_key.pub.der:用于签署登录令牌的密钥。icon_cache/:下载的网站图标。此目录可以跳过,因为 Vaultwarden 会在需要时重新获取这些图标。
Vaultwarden 数据库安全吗?文件中实际保存了什么
两个命令即可回答这个问题,您现在就可以运行这两个命令。
sudo apt update && sudo apt install -y sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select email from users;"
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select name from ciphers limit 1;"第一个命令会以明文输出用户的电子邮件地址。第二个命令会输出一个条目名称,结果类似这样:
2.k9Qw1nQ0y7Yy2Xw==|E1r0J3l5s7d9f1g3h5j7k9==|Lm4nOp6qRs8tUv0wXy2zAb4cDe6fGh8i=条目名称、用户名、密码和备注会在发送前由客户端加密,因此服务器保存的是无法读取的密文。2. 前缀表示 Bitwarden 的加密类型,后面依次是初始化向量(IV)、密文和 MAC(消息认证码);这些内容均采用 base64 编码,并以 | 分隔。用于解密的密钥由账户主密码派生,而主密码不会以可用形式发送到服务器。正如Vaultwarden 与自托管 Bitwarden 的比较中所述,无论运行 Vaultwarden 还是官方服务器,这部分机制都相同。
数据库的其余部分未加密。电子邮件地址、账户名称、密码提示和双因素恢复代码以明文存储,旁边还包括创建时间、条目所属组织等元数据。因此,备份文件本身就是机密数据。任何持有该文件的人都能知道您的用户是谁,并可以利用其硬件允许的速度离线攻击其中的加密数据块。正是这一点决定了后文的存储规则:副本离开服务器前必须先加密。
为什么 Vaultwarden 运行时复制 db.sqlite3 不能作为备份
Vaultwarden 默认让 SQLite 使用 WAL 模式(ENABLE_DB_WAL=true)。写入内容首先进入 db.sqlite3-wal,只有执行检查点后才会合并到 db.sqlite3。单独复制 db.sqlite3 时,得到的只是上次检查点时的数据库状态。因此,十分钟前保存的密码可能不会出现在归档中,而且不会有任何提示。
使用 cp 同时复制这 3 个文件也不能解决问题。复制操作发生在略有差异的时间点,因此保存的 WAL 可能记录了页面版本,而保存的主数据库文件与这些版本不再匹配。随后 SQLite 会尝试用其中一个文件恢复另一个文件,结果可能不正确。直到很久以后,您才会发现问题:
Error: database disk image is malformed.backup 使用 SQLite Online Backup API,因此不会有这个问题。SQLite 将该 API 作为复制正在使用中的数据库的方法。它会在读锁下读取页面;如果写入进程在读取期间修改文件,它会重新开始。因此,最终写入磁盘的是同一时刻的一致状态。
使用 sqlite3 .backup 创建数据库副本
sudo apt update && sudo apt install -y sqlite3
sudo install -d -m 700 /var/backups/vaultwarden
OUT=/var/backups/vaultwarden/db-$(date '+%Y%m%d-%H%M').sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '$OUT'"
sudo sqlite3 "$OUT" "PRAGMA integrity_check;"最后一条命令会单独输出一行 ok。如果输出其他内容,说明副本不可用。不要保留该副本,也不要删除之前的副本。整个过程都在运行中的服务器上执行,因此不会让任何用户退出,也不会重启容器。
sqlite3 工具不在 Vaultwarden 容器中。该镜像基于 debian:trixie-slim 构建,并包含 ca-certificates、curl、libmariadb3、libpq5 和 openssl,因此 docker exec vaultwarden sqlite3 ... 会失败,并显示:
exec: "sqlite3": executable file not found in $PATH应在主机上针对已挂载的路径运行该命令,这正是上面命令的做法。如果数据位于命名卷中,docker volume inspect <name> 会输出 /var/lib/docker/volumes/ 下的主机路径。
从 1.32.1 版本开始,Vaultwarden 也提供了自己的备份命令。在服务器上运行:
docker exec -it vaultwarden /vaultwarden backup该命令运行 VACUUM INTO,并将 db_YYYYMMDD_HHMMSS.sqlite3 写入数据目录。由此需要注意两点。副本会与原始文件位于同一磁盘上,因此这只是暂存步骤,还不能算作备份。该命令仅支持 SQLite:如果使用 MariaDB 或 PostgreSQL,则会因 The database type is not SQLite. Backups only works for SQLite databases 而停止。
容易被忽略的文件
attachments/以不透明名称保存密文。每个附件对应的数据库行都包含加密后的文件名,以及客户端解密该文件所需的密钥材料。没有数据库,附件只是无法读取的噪声;没有附件,数据库中的项目会导致用户下载失败。请在同一次运行中备份两者。
config.json保存您在管理页面中保存的所有设置,其值优先于匹配的环境变量。这有利有弊:恢复旧的 config.json 会静默覆盖 compose 文件中的设置,而该文件本身也属于敏感信息,因为其中可能包含 SMTP 密码和管理令牌。请将该令牌存储为 Argon2id PHC(password hashing competition)字符串,而不是明文。docker run --rm -it vaultwarden/server /vaultwarden hash 可为您生成一个。
rsa_key.pem用于签署 JSON web token(JWT),以保持客户端登录状态。如果 Vaultwarden 启动时找不到该文件,就会生成新的密钥,因此所有使用旧密钥签署的令牌都将无法通过验证,所有客户端也会被登出。Vault 内容不会因此丢失,因为它们使用从主密码派生的密钥加密。恢复该密钥文件即可避免大规模登出。
sends/保存 Send 链接所对应的文件。缺少这些文件只会导致相关下载失败,不会影响其他功能。
将整个流程放入一个脚本
#!/bin/bash
set -euo pipefail
DATA=/opt/vaultwarden/data
DEST=/var/backups/vaultwarden
STAMP=$(date '+%Y%m%d-%H%M%S')
STAGE=$(mktemp -d /tmp/vw-stage.XXXXXX)
install -d -m 700 "$DEST"
sqlite3 "$DATA/db.sqlite3" ".backup '$STAGE/db.sqlite3'"
test "$(sqlite3 "$STAGE/db.sqlite3" 'PRAGMA integrity_check;')" = "ok"
cp -a "$DATA"/rsa_key* "$STAGE/"
for extra in config.json attachments sends; do
if [ -e "$DATA/$extra" ]; then cp -a "$DATA/$extra" "$STAGE/"; fi
done
tar -C "$STAGE" -czf "$DEST/vw-$STAMP.tar.gz" .
chmod 600 "$DEST/vw-$STAMP.tar.gz"
rm -rf "$STAGE"
tar -tzf "$DEST/vw-$STAMP.tar.gz"将脚本保存为 /usr/local/sbin/vw-backup.sh,为其添加执行权限 chmod 700,然后以 root 身份运行。test 行负责实际检查:即使 PRAGMA integrity_check 报告数据损坏,sqlite3 仍会返回 0,因此将输出与 ok 比较,才能让损坏的副本导致脚本失败。随后,set -euo pipefail 会停止整个流程,而不是让 tar 围绕损坏的数据库生成一个看似正常的归档。
最后的 tar -tzf 会列出实际捕获的内容。首次运行时请阅读该输出。需要确认存在 ./db.sqlite3、./rsa_key.pem、./config.json 和 ./attachments/,并且不存在 ./db.sqlite3-wal。如果需要 journalctl 输出以及能够报告失败状态的单元,请使用 systemd 服务和计时器 每晚运行,而不要使用 cron。
验证备份:将其恢复到临时目录
未经测试的备份只是猜测。将备份恢复到临时目录只需一分钟,也不会触及正在运行的环境。
sudo install -d -m 700 /tmp/vw-check
sudo tar -C /tmp/vw-check -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
ls -l /tmp/vw-check
sudo sqlite3 /tmp/vw-check/db.sqlite3 "PRAGMA integrity_check;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from users;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from ciphers;"
sudo du -sh /tmp/vw-check/attachments有四项结果需要关注。integrity_check 会打印 ok。用户数量应与您已知的账户数量一致。密码条目数量应接近 sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;" 显示的当前数量;正在使用的密码库中该数量绝不会为 0。附件目录的大小应大致符合预期;如果没有人上传附件,可以跳过此项。然后运行 sudo rm -rf /tmp/vw-check,因为该目录现在包含了所有数据的第二份副本。
恢复手动复制的数据目录时,必须遵守一条规则:启动服务器前删除 db.sqlite3-wal 和 db.sqlite3-shm。否则,SQLite 会尝试使用属于另一份副本的日志来恢复已恢复的数据库,从而损坏原本完整的数据库。上述脚本生成的归档中不会包含这些文件,因为 .backup 写入的是一个完整的数据库。
恢复到服务器
以下命令在您自己的服务器上执行,并且容器必须已停止。数据目录发生变化时,Vaultwarden 不得写入该目录。
cd /opt/vaultwarden
docker compose stop vaultwarden
sudo mv data data.old.$(date '+%Y%m%d-%H%M%S')
sudo install -d -m 700 data
sudo tar -C data -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
sudo chown -R root:root data
docker compose start vaultwarden
docker compose logs --tail 20 vaultwardenchown 必须指定容器运行所使用的用户。官方镜像以 root 身份运行,因此除非您在 compose 文件中设置了 user:,否则应使用 root:root。如果服务器无法写入数据目录,登录页面会导致每个请求失败,日志中也会记录这一点。
正常启动结束时,应出现 Rocket 行:
[INFO] Rocket has launched from http://0.0.0.0:80然后通过浏览器登录,打开一个项目,并下载一个附件。如果可以登录但附件下载失败,说明归档包含数据库,但不包含 attachments/。在所有检查完成前,请保留 data.old.*,确认无误后再将其删除。回滚时执行相同的 3 个步骤,但交换两个目录的方向。
如果您的路径与本文不同,请参阅 VPS 上的 Vaultwarden 安装指南,其中包含这些命令所假定的 compose 文件。
备份不应存放的位置
- 不要与数据目录位于同一磁盘上。一个卷故障会同时丢失两份副本,错误路径上的一个
rm -rf也会造成同样的结果。 - 不要存放在同一台服务器上,即使使用第二个卷也不行。攻击者一旦获得 root 权限,就能在同一个会话中访问备份。
- 不要在未加密的对象存储中保存,因为归档包含电子邮件地址、密码提示、恢复代码和保管库密文,攻击者可以在离线状态下进行攻击。
- 不要只依赖服务商的快照。快照恢复速度快,确实值得保留,但它们与服务器位于同一账户中,因此账户出现问题时,快照也会一并受影响。
异地副本适合使用 restic,因为 restic 会先在服务器上加密存储库,然后才上传任何内容。在服务器上:
sudo apt install -y restic
export RESTIC_REPOSITORY=s3:https://s3.example.com/vaultwarden-backups
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /var/backups/vaultwarden --tag vaultwarden
restic snapshots --tag vaultwarden
restic forget --tag vaultwarden --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune让 restic 指向归档目录,而不是实时数据目录。这样上传的就是你已经检查过的一致副本。将存储库密码保存在受保护服务器之外的位置:密码丢失后,快照将无法读取,这是设计如此。存储系统支持时,应为服务器配置只能写入、不能删除的凭据。这样即使服务器遭到入侵,攻击者也无法删除服务器自身的历史备份。在 VPS 上设置 restic 备份完整介绍存储库和计划任务;如果你尚未决定,restic 与 BorgBackup 的比较介绍了如何选择。
按计划测试恢复
每月选择一天。使用 restic restore latest --tag vaultwarden --target /tmp/vw-check 将最新快照拉取到临时目录,运行相同的 PRAGMA integrity_check,执行相同的行数统计,然后记录日期和统计结果。六个月内从未执行过恢复的备份,其状态是未知的。等到服务中断时才了解它的状态,是最糟糕的时机。
每年执行一次完整测试。使用恢复的数据目录,在备用端口上启动第二个 Vaultwarden 容器,并使用真实账户登录。这可以端到端验证主密码流程,而行数统计无法做到这一点。在相同计划下运行 restic check --read-data-subset=10%,可以确认已存储的数据确实可读,而不只是能被列出。
FAQ
Vaultwarden 运行时可以使用 cp 复制 db.sqlite3 吗?
不可以。Vaultwarden 以 WAL 模式运行 SQLite,因此最近的写入会保留在 db.sqlite3-wal 中,尚未写入 db.sqlite3。仅复制主数据库文件的 cp 会静默丢失这些写入,分别复制这两个文件还可能产生不匹配的文件对,之后表现为 Error: database disk image is malformed。请改用 sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'"。它使用 SQLite 的 Online Backup API,在服务器继续提供服务的同时生成一个一致的文件。
备份时必须停止 Vaultwarden 容器吗?
不必,这正是 .backup 的作用。服务器运行时复制数据库是安全的。用户上传附件或 Send 文件时,文件会立即写入。因此,如果文件在复制数据库和执行 tar 之间添加,可能不会进入当晚的归档,最坏情况下只会丢失一个附件。如果您不介意停机几秒,可在脚本前执行 docker compose stop,并在脚本后执行 docker compose start,这样连这种情况也能避免。
如果恢复时没有 rsa_key 文件,会发生什么?
Vaultwarden 会在启动时生成新密钥。该密钥用于为维持会话有效的 JSON Web Token (JWT) 签名,因此所有现有令牌都会停止通过验证,所有客户端都会被注销,并且必须重新登录。Vault 内容不受影响,因为这些内容使用从每个用户主密码派生的密钥加密,而不是使用 RSA 密钥加密。将 rsa_key.pem 与数据目录中的其他内容一起恢复,用户不会察觉这次恢复。
备份归档可以直接上传到对象存储吗?
不可以。项目名称、密码和备注是密文,但电子邮件地址、账户名称、密码提示和双因素恢复代码以明文存储在数据库中,离线攻击者还可以按照自己的速度反复尝试破解密文。归档离开服务器前必须加密。restic 仓库会代您完成这项工作,而 gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz 会生成一个可交给任意存储服务的单个加密文件。
如何备份使用 PostgreSQL 或 MariaDB 的 Vaultwarden?
SQLite 的步骤不适用,内置命令会因 The database type is not SQLite. Backups only works for SQLite databases 而拒绝执行。使用数据库原生工具导出数据库,例如 pg_dump 或 mysqldump,其他规则保持不变。导出文件应与 attachments/、sends/、config.json 和 rsa_key 文件放在同一个归档中。这些内容必须在同一次运行中获取、加密,并存储在生成该备份的服务器之外的位置。