背景
1Password 不再有跨区订阅优惠,Family Annual ¥498/年 还是有些肉疼。同事们都推荐 Vaultwarden,正好有空闲的 VPS,干脆自建。
Vaultwarden 是 Bitwarden 服务端的 Rust 重写实现,协议完全兼容官方客户端(桌面 / 移动端 / 浏览器扩展),但资源占用极低,1C/1G 的小机器跑得很轻松。
前置准备
- 一台 Debian 12 VPS(1C/1G 起步即可,家用绰绰有余)
- 一个域名,例如
vault.example.com,在 DNS 服务商把 A 记录指向 VPS 公网 IP - 一个 SMTP 邮箱(用于邀请家人、找回密码),推荐 Resend
环境初始化
apt update && apt upgrade -y && apt install -y curl ca-certificates
timedatectl set-timezone Asia/Shanghai
adduser evan
usermod -aG sudo evan
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
usermod -aG docker evan
部署 Vaultwarden + Caddy
切到普通用户,建立工作目录:
sudo -iu evan
mkdir -p ~/vaultwarden/{vw-data,caddy-data,caddy-config}
cd ~/vaultwarden
写 docker-compose.yml:
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://${VAULT_HOST}"
ADMIN_TOKEN: ${ADMIN_TOKEN}
SIGNUPS_ALLOWED: "false"
SMTP_HOST: "smtp.resend.com"
SMTP_FROM: ${SMTP_FROM}
SMTP_PORT: "465"
SMTP_SECURITY: "force_tls"
SMTP_USERNAME: "resend"
SMTP_PASSWORD: ${SMTP_PASSWORD}
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
volumes:
- ./vw-data:/data
networks: [internal]
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
environment:
VAULT_HOST: ${VAULT_HOST}
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks: [internal]
networks:
internal:
driver: bridge
写 .env,凭据全部走环境变量,别直接写进 compose。VAULT_HOST 是单一来源,Vaultwarden 的 DOMAIN 由 compose 拼上 https:// 前缀,Caddyfile 通过 {$VAULT_HOST} 读到纯 hostname:
vim .env
VAULT_HOST="vault.example.com"
SMTP_FROM="vault@notify.example.com"
SMTP_PASSWORD="re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$<salt>$<hash>'
ADMIN_TOKEN 是后台管理页 /admin 的访问密钥,跟普通用户登录是两套体系。在 SIGNUPS_ALLOWED: false 的私有部署下,邀请家人、改运行时配置、管理 Organization 都要靠它。两点务必照做:
1. 填 argon2id hash,别填明文。 明文会被 docker inspect / debug 模式下的 docker logs / 某些重启场景下的 ps -ef 捕获;哈希后即使 .env 泄漏,攻击者也无法直接登录。生成命令:
docker run --rm -it vaultwarden/server /vaultwarden hash
交互式输入两次 token,输出 $argon2id$... 整段粘进 .env。
同一个 token 每次跑出的 hash 都不一样,这是 argon2id 故意的——salt 是现场随机生成并嵌在 hash 串里的。任挑一个写进
.env都行,不必纠结哪个"对"。验证时 Vaultwarden 会从存储 hash 里提取 salt 复现比对。
注意:登录 /admin 时输入的是原文 token,不是 hash,原文请自己用密码管理器存好。
2. 配置稳定后关掉后台。 家人都加完、SMTP 调通后,把 .env 里 ADMIN_TOKEN= 留空(或注释整行),重启容器后 /admin 直接 404,攻击面归零。后续要管理时再临时打开重启即可。
更多细节参考 Vaultwarden Wiki: Enabling admin page。
写 Caddyfile,域名从 compose 注入的环境变量读取:
{$VAULT_HOST} {
encode zstd gzip
reverse_proxy vaultwarden:80
}
启动:
docker compose up -d
docker compose logs -f
容器跑起来后,访问 https://vault.example.com/admin,用 ADMIN_TOKEN 原文登录后台,就可以邀请家人了。
数据备份方案
Vaultwarden 所有状态都在 vw-data/ 下:SQLite 数据库 db.sqlite3、附件 attachments/、Sends sends/、RSA 私钥 rsa_key.*、运行时配置 config.json。icon_cache/ 只是网页 favicon 缓存,可忽略。
但要"一键复原"光 vw-data/ 不够:docker-compose.yml / Caddyfile / .env 决定怎么把数据跑起来;caddy-data/ 存了 Let’s Encrypt 账户密钥和已签发证书,丢了重签会触发频率限制。所以备份范围放到整个 ~/vaultwarden/ 父目录。
VPS 故障、误删条目、或者哪天 docker volume 玄学损坏,没备份就是全家几百条密码一锅端。三条原则:
- 每日异地备份(脱离 VPS)
- 加密(云端泄漏不直接暴露 vault)
- 季度演练恢复(没真解密验证过的备份等于没备份)
restic + Backblaze B2
restic 自带去重、增量、AES-256 加密,单二进制,跟 B2 / R2 / S3 都能直连。下面以 B2 为例(10GB 内免费,对密码库绰绰有余)。
宿主机一次性初始化:
sudo apt install -y restic
cat > /root/.restic-env <<'EOF'
B2_ACCOUNT_ID=<key-id>
B2_ACCOUNT_KEY=<application-key>
RESTIC_REPOSITORY=b2:my-vaultwarden-backup:/
RESTIC_PASSWORD_FILE=/root/.restic-password
EOF
chmod 600 /root/.restic-env
echo '<a-strong-passphrase>' > /root/.restic-password
chmod 600 /root/.restic-password
set -a; source /root/.restic-env; set +a
restic init
set -a让随后赋值的变量自动 export,source才能把.restic-env喂给 restic 子进程;set +a收尾。set内置命令的符号约定跟普通 CLI 反过来 ——-启用、+关闭(-e/-u/-o pipefail这些脚本安全开关同理)。新开 shell 手动跑 restic 时要重新 source 一次。RESTIC_PASSWORD是恢复备份的唯一凭据,丢了就什么都救不回来。它又不能只存在 Vaultwarden 里(鸡生蛋问题),可以打印一份纸质放抽屉。
备份脚本
/root/vaultwarden-backup.sh:
#!/usr/bin/env bash
set -euo pipefail
set -a; source /root/.restic-env; set +a
# 任何退出路径都保证容器恢复运行
trap 'docker start vaultwarden >/dev/null 2>&1 || true' EXIT
# 短暂停机(~5s)拿到一致性快照
docker stop vaultwarden >/dev/null
# 备份整个部署目录:vw-data + Caddyfile + docker-compose.yml + .env + caddy-data
# 用相对路径,snapshot 里只记 vaultwarden/,恢复时落点干净
cd /home/evan && restic backup vaultwarden \
--tag vaultwarden \
--exclude='vaultwarden/vw-data/icon_cache'
# 滚动保留:日 7 / 周 4 / 月 12
restic forget --prune \
--keep-daily 7 --keep-weekly 4 --keep-monthly 12
chmod +x /root/vaultwarden-backup.sh
不在线备份(不用
sqlite3 .backup)的原因:vaultwarden 官方镜像不一定带 sqlite3 CLI,且 WAL 模式下直接 rsync 容易拿到不一致快照。stop 5 秒最稳。家用场景凌晨 3 点没人在用,这点停机完全无感。 用相对路径是因为 restic 按 snapshot 路径串做增量匹配,绝对 / 相对混用会让下次 backup 当全新数据重传。新仓库一开始就定下来。
每日跑
cron 一行:
sudo crontab -e
30 3 * * * /root/vaultwarden-backup.sh >> /var/log/vaultwarden-backup.log 2>&1
演练恢复
每季度做一次,且必须真解密一条记录验证 —— 文件存在不等于能解密:
# 拉一份最新快照到临时目录
restic snapshots
restic restore latest --target /tmp/vw-restore
# /tmp/vw-restore/vaultwarden/ 是完整部署,cd 进去 docker compose up -d 就能起
# 用 master password 登录 → 打开任意条目 → 字段能看到明文 = 成功
SQLite 文件损坏或 RSA 密钥不匹配时,文件都在但密码全部解不开。演练才是备份的最后一公里。
SSH Agent 设置
Bitwarden 桌面端从 2024 年起内置了 SSH Agent,可以替代 1Password 的 SSH 体验。Vaultwarden 服务端不参与,全部在客户端完成。
启用步骤:
打开 Bitwarden Desktop → Settings,勾选 Use Bitwarden SSH Agent
客户端会在系统目录里建一个 socket,先确认路径:
lsof -U | grep bitwarden-ssh-agentmacOS 上典型路径:
~/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock把它 symlink 到一个稳定的位置,方便 shell 引用:
ln -sf "$HOME/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock" \ "$HOME/.bitwarden-ssh-agent.sock"在
~/.zshrc/~/.bashrc里导出:export SSH_AUTH_SOCK="$HOME/.bitwarden-ssh-agent.sock"验证:
ssh-add -l能列出 Bitwarden 里标记为 SSH Key 的条目就成功了。
迁移注意点
- 用桌面客户端导出 / 导入,不要用手机端的两端同步迁移,手机端那个流程会丢失部分自定义字段。
- 1Password 的 Vault 分组信息会丢失,迁移后需要在 Vaultwarden 里手工建文件夹重新归类。
- Passkey 无法迁移,Passkey 是绑定在验证器 app 内部的,必须在新平台上对每个站点重新注册。
- 附件无法直接迁移,但 1Password 导出的
.1pux文件本质是 zip,把后缀改成.zip解压后就能拿到原始附件,再手工挂回 Vaultwarden 对应条目。
– EOF –