背景

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 调通后,把 .envADMIN_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.jsonicon_cache/ 只是网页 favicon 缓存,可忽略。

但要"一键复原"光 vw-data/ 不够:docker-compose.yml / Caddyfile / .env 决定怎么把数据跑起来;caddy-data/ 存了 Let’s Encrypt 账户密钥和已签发证书,丢了重签会触发频率限制。所以备份范围放到整个 ~/vaultwarden/ 父目录。

VPS 故障、误删条目、或者哪天 docker volume 玄学损坏,没备份就是全家几百条密码一锅端。三条原则:

  1. 每日异地备份(脱离 VPS)
  2. 加密(云端泄漏不直接暴露 vault)
  3. 季度演练恢复(没真解密验证过的备份等于没备份)

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 服务端不参与,全部在客户端完成。

启用步骤:

  1. 打开 Bitwarden Desktop → Settings,勾选 Use Bitwarden SSH Agent

  2. 客户端会在系统目录里建一个 socket,先确认路径:

    lsof -U | grep bitwarden-ssh-agent
    

    macOS 上典型路径:

    ~/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock
    
  3. 把它 symlink 到一个稳定的位置,方便 shell 引用:

    ln -sf "$HOME/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock" \
           "$HOME/.bitwarden-ssh-agent.sock"
    
  4. ~/.zshrc / ~/.bashrc 里导出:

    export SSH_AUTH_SOCK="$HOME/.bitwarden-ssh-agent.sock"
    
  5. 验证:

    ssh-add -l
    

    能列出 Bitwarden 里标记为 SSH Key 的条目就成功了。

迁移注意点

  • 用桌面客户端导出 / 导入,不要用手机端的两端同步迁移,手机端那个流程会丢失部分自定义字段。
  • 1Password 的 Vault 分组信息会丢失,迁移后需要在 Vaultwarden 里手工建文件夹重新归类。
  • Passkey 无法迁移,Passkey 是绑定在验证器 app 内部的,必须在新平台上对每个站点重新注册。
  • 附件无法直接迁移,但 1Password 导出的 .1pux 文件本质是 zip,把后缀改成 .zip 解压后就能拿到原始附件,再手工挂回 Vaultwarden 对应条目。

– EOF –