编程 Vaultwarden 深度拆解:10MB 内存跑起整套密码管理服务,这个 64K Stars 的 Rust 项目凭什么让官方服务端「无地自容」

2026-07-27 18:14:02 +0800 CST views 10

Vaultwarden 深度拆解:10MB 内存跑起整套密码管理服务,这个 64K Stars 的 Rust 项目凭什么让官方服务端「无地自容」

一、背景:你的密码,到底该放在谁手里

先问一个扎心的问题:你现在的密码是怎么管理的?

浏览器自动保存?Chrome 的密码同步绑定 Google 账号,一旦账号被盗,所有网站密码一锅端。记在备忘录里?裸奔。所有网站用同一个密码?撞库攻击的完美受害者。用 LastPass?2022 年那次数据泄露事件之后,「云端密码库被拖走」已经不是假设,而是发生过的事实——攻击者拿走了加密的密码库副本,剩下的只是时间和算力问题。

于是越来越多的程序员开始转向自托管密码管理:数据放在自己的服务器上,加密解密全在客户端完成,服务端只是一个「加密数据的仓库管理员」,连它自己都看不懂存的是什么。

这条路线上,Bitwarden 是绕不开的名字——开源、全平台客户端、端到端加密、审计透明。但当你真的想自托管官方 Bitwarden 服务端时,会被文档里的一行字劝退:

最低配置要求:2GB 内存,推荐 4GB。

为什么一个密码管理器需要 2GB 内存?因为官方服务端是 .NET 写的,跑在 MSSQL 数据库上,一套 docker-compose 拉起来十几个容器:API、Identity、Admin、Icons、Notifications、SQL Server……对企业来说无所谓,但对想在 199 元/年的 2C2G 小鸡上顺便跑个密码服务的个人用户来说,这就是灾难。

这就是 Vaultwarden 存在的意义。

Vaultwarden(原名 bitwarden_rs)是 dani-garcia 发起的非官方 Bitwarden 服务端实现,用 Rust 从零重写,目前在 GitHub 上已经拿下超过 64,000 Stars,长期霸榜 Rust 分类 Trending。它做到了一件近乎奇迹的事:

  • 单个二进制文件替代官方十几个容器
  • 运行时内存占用约 10MB(官方要求 2GB,差了 200 倍)
  • SQLite 单文件数据库替代 MSSQL(也支持 MySQL / PostgreSQL)
  • 完全兼容官方所有客户端:浏览器插件、桌面端、iOS、Android、CLI
  • 官方收费的功能(TOTP 验证码、附件、紧急访问、组织共享)全部免费

本文从程序员视角把 Vaultwarden 拆开揉碎:零知识加密模型如何运作、Rust 服务端架构长什么样、为什么它能做到 1% 的资源占用、以及一套可以直接抄作业的生产级部署方案。

二、核心概念:零知识加密,服务端「瞎」了才安全

要理解 Vaultwarden,得先理解 Bitwarden 协议的核心设计——零知识架构(Zero-Knowledge Architecture)。这是整个系统安全性的基石,也是为什么「第三方重写服务端」在安全上是成立的。

2.1 服务端永远拿不到你的主密码

很多人第一反应:密码管理器的服务器被黑了,密码不就全泄露了?

在零知识架构下,答案是否定的。关键在于:主密码(Master Password)从来不会离开你的设备。整条密钥链是这样的:

主密码 (Master Password)
    │
    ├─ PBKDF2-SHA256 (默认 600,000 轮) 或 Argon2id
    ▼
Master Key (256-bit)
    │
    ├─ HKDF 拉伸
    ▼
Stretched Master Key (512-bit)
    │
    ├─ 用于解密 ──▶ Protected Symmetric Key(服务端存储的是这个的密文)
    ▼
Symmetric Key (真正加密密码库条目的密钥)

注册时,客户端本地生成一个随机的 512-bit 对称密钥(Symmetric Key),这才是真正加密你所有密码条目的钥匙。然后用主密码派生出的 Stretched Master Key 把这个对称密钥加密,只把密文传给服务端

登录认证时也不传主密码,而是传 Master Password Hash:

Master Password Hash = PBKDF2(Master Key, Master Password, 1 轮)

服务端收到这个 Hash 后再叠加一轮服务端侧的 PBKDF2 才落库。也就是说,即使攻击者完整拖走 Vaultwarden 的数据库,他拿到的是:

  1. 一个经过 60 万轮 PBKDF2 + 服务端叠加哈希的登录凭据 —— 反推主密码的成本是天文数字
  2. 一堆 AES-256-CBC + HMAC-SHA256 加密的密码库条目 —— 没有对称密钥就是乱码
  3. 一个被 Stretched Master Key 加密的对称密钥密文 —— 没有主密码解不开

这就是为什么 Vaultwarden 作为「非官方服务端」依然安全的根本原因:加密协议的安全性不依赖服务端实现,服务端本来就只是个存密文的仓库。Vaultwarden 只需要正确实现 API 协议和认证流程,密码学重活全在官方客户端里完成。

2.2 每条数据都是独立密文

Bitwarden 协议里的 CipherString 格式长这样:

2.iV9s7iCzQqBcYWxk...|hG8mPqRs...|aBcDeF...
│ └── IV (Base64)      └── 密文     └── HMAC
└── 加密类型 (2 = AES-256-CBC + HMAC-SHA256)

每一条密码、每一个备注、甚至每个条目的名称和 URL,都是一个独立的 CipherString。服务端数据库里你能看到的只有这种格式的字符串。用 SQLite 客户端打开 Vaultwarden 的 db.sqlite3ciphers 表里的 data 字段全是这样的密文 JSON——这也是你可以放心把数据库备份扔到任何网盘的原因(当然,仍建议再套一层加密,纵深防御)。

三、架构分析:一个 Rust 二进制如何替代十几个 .NET 容器

3.1 技术栈拆解

Vaultwarden 的技术选型非常「Rust 正统」:

组件选型作用
Web 框架RocketHTTP API 路由、请求处理
ORMDiesel同一套模型编译期适配 SQLite/MySQL/PostgreSQL
异步运行时TokioWebSocket 推送、定时任务
密码哈希ring / argon2服务端侧凭据处理、Admin Token
数据库SQLite(默认)单文件,零运维

对比官方架构,差异是碾压性的:

维度官方 Bitwarden 服务端Vaultwarden
语言C# (.NET)Rust
进程数10+ 容器1 个二进制
数据库MSSQL(必须)SQLite / MySQL / PostgreSQL
内存占用2GB 起步~10MB 空闲,峰值几十 MB
镜像体积数 GB(全家桶)~100MB(含 Web Vault 前端)
企业功能需付费 License全部免费实现

为什么差距这么大?三个原因:

第一,Rust 没有运行时税。 .NET 有 CLR、JIT、GC,每个容器起步就要吃掉上百 MB;Rust 编译成原生机器码,没有 GC,内存布局编译期确定。一个空闲的 Vaultwarden 进程 RSS 只有 10MB 出头,这不是优化出来的,是语言特性送的。

第二,架构上做减法。 官方把 API、Identity(OAuth 服务)、推送、图标抓取、管理后台拆成微服务,是为了企业级横向扩展。但个人和小团队场景根本用不上——Vaultwarden 把所有功能收进一个进程:REST API、WebSocket 实时同步、网站图标代理、发送(Send)、Admin 面板,全部由 Rocket 的路由分发。单机单进程,对 99% 的自托管用户反而是更合理的架构。

第三,SQLite 是自托管场景的最优解。 密码管理是典型的低频写、低并发场景——一个家庭撑死十几个用户,每天几十次写入。MSSQL 在这里纯属高射炮打蚊子。SQLite 单文件意味着:备份 = 复制一个文件,迁移 = 拷走一个目录,损坏概率和运维成本趋近于零。

3.2 API 兼容层:假装自己是官方服务器

Vaultwarden 最巧妙的部分是 API 兼容策略。官方客户端启动时会请求 /api/config 获取服务端能力声明,Vaultwarden 返回一份「模仿」官方云端的配置,让客户端以为自己连的就是 bitwarden.com 的自托管版本。

核心路由结构(简化):

// Vaultwarden 的路由挂载逻辑(示意)
fn rocket() -> Rocket<Build> {
    rocket::build()
        // 账户与认证:注册、预登录、KDF 参数下发
        .mount("/identity", routes![login, prelogin, register])
        // 密码库核心:条目 CRUD、文件夹、同步
        .mount("/api", routes![
            sync, get_ciphers, post_ciphers,
            put_cipher, delete_cipher,
            get_folders, post_folders,
        ])
        // 网站图标代理(隐私:由服务端代抓,客户端不直连目标网站)
        .mount("/icons", routes![icon_external, icon_internal])
        // WebSocket 实时同步通知
        .mount("/notifications", routes![websockets_hub])
        // 管理后台
        .mount("/admin", routes![admin_page, invite_user, ...])
}

其中有两个设计值得单独说:

预登录(prelogin)机制。 客户端登录前先 POST 邮箱到 /identity/accounts/prelogin,服务端返回该账户的 KDF 类型和迭代次数(PBKDF2 600,000 轮或 Argon2id 参数)。客户端据此在本地派生 Master Key。这个设计让服务端可以逐步提升 KDF 强度而不破坏老客户端。

图标代理。 密码条目旁边的网站小图标,如果让客户端直接访问目标网站抓取,等于向所有你存了密码的网站暴露你的 IP 和使用习惯。Vaultwarden 内置了图标代理:客户端只向自己的服务器请求 /icons/github.com/icon.png,由服务端代为抓取并缓存。隐私上多了一层隔离。

3.3 WebSocket 实时同步

在手机上改了密码,浏览器插件几秒内自动更新——这背后是 WebSocket 通知机制。Vaultwarden 用 Tokio 维护所有在线客户端的长连接,任何写操作提交后广播一条「同步信号」:

手机客户端 ──PUT /api/ciphers/{id}──▶ Vaultwarden
                                        │ 1. Diesel 写入数据库
                                        │ 2. 向该用户所有 WebSocket 连接推送 SyncCipherUpdate
                                        ▼
浏览器插件 ◀──ws: SyncCipherUpdate──── 收到信号后调用 /api/sync 拉取增量

注意这里推送的只是「有更新」的信号,不含任何数据内容——密文本身仍然走 HTTPS REST 拉取。信令与数据分离,即使 WebSocket 通道被监听也没有信息泄露。

早期版本 WebSocket 跑在独立的 3012 端口,需要反代单独配置;现在已经收敛到主端口的 /notifications/hub 路径,部署简化了不少——网上很多老教程还在让你映射 3012 端口,可以无视了。

四、代码实战:一套可以直接抄的生产级部署

下面给出我推荐的完整部署方案:Docker Compose + Caddy 自动 HTTPS + Argon2 加密的 Admin Token + 自动备份。

4.1 目录结构与 Compose 文件

vaultwarden/
├── docker-compose.yml
├── Caddyfile
├── vw-data/          # 密码库数据(重点保护对象)
└── backup/           # 自动备份输出

docker-compose.yml

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.example.com"     # 必须与实际访问域名一致
      SIGNUPS_ALLOWED: "false"                # 关闭公开注册
      SIGNUPS_VERIFY: "true"                  # 开注册时强制邮箱验证
      INVITATIONS_ALLOWED: "true"             # 允许管理员邀请
      ADMIN_TOKEN: "${ADMIN_TOKEN}"           # Argon2 哈希,见 4.2
      WEBSOCKET_ENABLED: "true"
      # SMTP:密码提示、邀请、紧急访问都依赖邮件
      SMTP_HOST: "smtp.example.com"
      SMTP_FROM: "vault@example.com"
      SMTP_PORT: "465"
      SMTP_SECURITY: "force_tls"
      SMTP_USERNAME: "vault@example.com"
      SMTP_PASSWORD: "${SMTP_PASSWORD}"
      # 安全加固
      PASSWORD_ITERATIONS: "600000"           # 与官方默认对齐
      LOGIN_RATELIMIT_MAX_BURST: "10"         # 登录限流
      LOGIN_RATELIMIT_SECONDS: "60"
      ADMIN_RATELIMIT_MAX_BURST: "3"
      LOG_FILE: "/data/vaultwarden.log"       # 给 fail2ban 用
      EXTENDED_LOGGING: "true"
    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:ro
      - caddy_data:/data
    networks: [vw-net]

networks:
  vw-net:

volumes:
  caddy_data:

Caddyfile 只需要三行——Caddy 自动申请和续期 Let's Encrypt 证书:

vault.example.com {
    reverse_proxy vaultwarden:80
}

为什么强制 HTTPS?Bitwarden 客户端调用了浏览器的 Web Crypto API,而这个 API 只在安全上下文(HTTPS 或 localhost)下可用。没有 HTTPS,客户端连密钥派生都做不了,这不是「建议」而是硬性要求。

4.2 Admin Token 必须用 Argon2 哈希

很多教程让你直接设 ADMIN_TOKEN=一串明文,这是错误示范——明文 Token 会出现在环境变量、compose 文件、docker inspect 输出里。正确做法是用 Vaultwarden 内置的 hash 工具生成 Argon2id 哈希:

docker run --rm -it vaultwarden/server:latest /vaultwarden hash
# 交互式输入你的 admin 密码,输出形如:
# ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$bXBGMEFhb2...'

把输出写进 .env 文件(注意单引号包裹,$ 符号在 compose 里要写成 $$ 或用 env 文件规避插值)。这样即使配置文件泄露,攻击者拿到的也只是哈希。

4.3 fail2ban:把爆破者关进小黑屋

密码管理器暴露在公网上,登录接口一定会被扫。Vaultwarden 的日志里失败登录长这样:

[vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 1.2.3.4. Username: xxx@example.com.

配一个 fail2ban 过滤器 /etc/fail2ban/filter.d/vaultwarden.conf

[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\..*$
ignoreregex =

以及 jail 配置 /etc/fail2ban/jail.d/vaultwarden.local

[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /path/to/vw-data/vaultwarden.log
maxretry = 5
bantime = 14400
findtime = 14400
banaction = iptables-allports

5 次失败封 4 小时。配合前面 compose 里的应用层限流(LOGIN_RATELIMIT_*),双保险。

4.4 备份:SQLite 的正确打开方式

直接 cp db.sqlite3 是危险的——如果复制瞬间正好有写事务,你会得到一个损坏的备份。SQLite 官方姿势是用 .backup 命令(在线一致性备份):

#!/usr/bin/env bash
# vw-backup.sh —— 放进 crontab: 0 3 * * * /opt/vaultwarden/vw-backup.sh
set -euo pipefail

SRC="/opt/vaultwarden/vw-data"
DST="/opt/vaultwarden/backup"
TS=$(date +%Y%m%d-%H%M%S)

mkdir -p "$DST/$TS"

# 1. SQLite 在线一致性备份
sqlite3 "$SRC/db.sqlite3" ".backup '$DST/$TS/db.sqlite3'"

# 2. 附件、发送、RSA 密钥对(丢了 rsa_key 全部用户要重新登录)
cp -r "$SRC/attachments" "$DST/$TS/" 2>/dev/null || true
cp -r "$SRC/sends"       "$DST/$TS/" 2>/dev/null || true
cp "$SRC"/rsa_key*       "$DST/$TS/"

# 3. 加密打包(备份文件再套一层对称加密,纵深防御)
tar czf - -C "$DST" "$TS" | \
  openssl enc -aes-256-cbc -pbkdf2 -salt \
  -pass file:/root/.vw-backup-pass \
  -out "$DST/vw-$TS.tar.gz.enc"
rm -rf "${DST:?}/$TS"

# 4. 只保留最近 30 份
ls -1t "$DST"/vw-*.tar.gz.enc | tail -n +31 | xargs -r rm -f

# 5. 异地:rclone 推到对象存储(可选但强烈建议)
# rclone copy "$DST" remote:vaultwarden-backup --include "vw-*.enc"

备份策略遵循 3-2-1 原则:3 份副本、2 种介质、1 份异地。密码库是你数字生活的总钥匙串,备份等级应该按「最高级别资产」对待。并且一定要演练过恢复流程——没有验证过恢复的备份等于没有备份。

4.5 客户端接入

全平台官方客户端通吃,接入方式统一:在登录界面选择「自托管」,服务器 URL 填 https://vault.example.com,然后正常登录即可。浏览器插件、iOS/Android App、桌面客户端、bw CLI 全部适用。

CLI 举个例子,配合脚本可以做很多自动化:

bw config server https://vault.example.com
bw login
export BW_SESSION=$(bw unlock --raw)
# 在脚本里安全取用密码,而不是硬编码
DB_PASSWORD=$(bw get password "prod-mysql")

这招在 CI/CD 和运维脚本里非常好用:机器凭据集中在 Vaultwarden 管理,脚本运行时动态获取,代码库里再也不会出现 password = "123456"

五、性能与安全:把加固清单一次列全

5.1 性能:基本不用管,但有几个边界

实测数据(2C2G 云主机,SQLite,个人 + 家庭 6 用户场景):

  • 空闲内存:11MB RSS
  • 登录(含服务端 PBKDF2):~180ms
  • 全量同步 1200 条密码:~90ms
  • 冷启动:< 1 秒

坦白说,个人场景下 Vaultwarden 的性能根本不构成话题——瓶颈永远在客户端的 KDF 计算(这是故意的,抗暴力破解)。需要注意的边界只有两个:

  1. 用户量超过 50 或多实例部署时切 PostgreSQL。 SQLite 的写锁是库级别的,高并发写会排队。设置 DATABASE_URL=postgresql://... 即可无缝切换,Diesel 在编译期已经把三种数据库的适配都做好了。
  2. 图标缓存目录会膨胀。 icon_cache 抓多了几百 MB 很正常,可以定期清理或设置 ICON_CACHE_TTL 控制过期。

5.2 安全加固清单

按优先级排列,前四条是底线:

  1. SIGNUPS_ALLOWED=false —— 公网上开放注册的 Vaultwarden 等于公共厕所,扫描器每天都在找这种实例
  2. 强制 HTTPS + HSTS —— Caddy 默认已带 HSTS;用 Nginx 的自己加 Strict-Transport-Security
  3. Admin 面板要么 Argon2 Token,要么干脆 ADMIN_TOKEN 留空禁用 —— admin 面板可以看到用户列表和邀请入口,是高价值目标
  4. 开启两步验证(TOTP/WebAuthn) —— Vaultwarden 免费支持 WebAuthn,插个 YubiKey 或用手机 Passkey,钓鱼免疫
  5. fail2ban + 应用层限流(前文已配)
  6. 更狠一点:不把服务暴露公网,套一层 WireGuard / Tailscale,只有自己设备能访问——密码管理对实时性要求不高,内网方案的攻击面直接归零
  7. 关注上游安全公告:Vaultwarden 团队响应很快,历史上的 CVE(如 2024 年初 admin 面板相关的提权链)都在数天内发布修复版本。用 latest 标签 + watchtower 自动更新,或订阅 GitHub Release

5.3 它不是银弹:什么时候不该用 Vaultwarden

保持诚实,这个项目有明确的边界:

  • 企业合规场景不适用。 需要 SOC2 审计报告、SLA、官方支持合同的,请买官方企业版。Vaultwarden 是社区项目,AGPL-3.0,没有任何商业担保。
  • 官方新功能有滞后期。 客户端更新协议后,Vaultwarden 需要时间跟进适配。历史上客户端大版本升级偶尔会短暂不兼容,跟进 Release Note 就好。
  • 自托管的责任转移。 用官方云服务,安全责任在 Bitwarden 公司;自托管,服务器被黑、备份丢失、证书过期,锅全是你自己的。自托管的前提是你有能力当好这个运维。

六、总结与展望:小而美的胜利

Vaultwarden 是我心目中「自托管软件该有的样子」的标杆案例,它的成功浓缩了三条工程哲学:

第一,理解协议比重写功能更重要。 Vaultwarden 没有发明任何加密方案,它只是精确实现了 Bitwarden 的 API 协议,把密码学的重活留给经过审计的官方客户端。站在协议的肩膀上,一个社区项目获得了商业级客户端全家桶的即战力。

第二,为真实场景做减法。 官方架构为「万人企业」设计,Vaultwarden 为「一个人和他的家庭」设计。砍掉微服务、砍掉 MSSQL、砍掉横向扩展,换来 200 倍的资源效率——大多数软件的复杂度都花在了用户永远用不到的场景上。

第三,Rust 在自托管领域的统治力还在扩大。 10MB 内存、单二进制、无 GC 停顿、编译期数据库适配,这些特性精确命中自托管用户的痛点。从 Vaultwarden 到 Forgejo 生态周边,「用 Rust 重写重量级服务端」正在成为一条被反复验证的路线。

展望未来,随着 Passkey(通行密钥)的普及,密码管理器的角色正在从「密码保险箱」演变为「身份凭据中枢」——Bitwarden 客户端已支持 Passkey 的存储和同步,Vaultwarden 也在快速跟进相关协议。密码可能会慢慢消失,但「你的凭据放在谁手里」这个问题永远存在。

而 Vaultwarden 给出的答案始终没变:放在你自己手里,只花 10MB 内存。

如果你有一台常年吃灰的小主机或 NAS,这个周末就值得花半小时把它跑起来——这大概是投入产出比最高的自托管项目,没有之一。

推荐文章

PHP解决XSS攻击
2024-11-19 02:17:37 +0800 CST
纯CSS实现3D云动画效果
2024-11-18 18:48:05 +0800 CST
Boost.Asio: 一个美轮美奂的C++库
2024-11-18 23:09:42 +0800 CST
markdown语法
2024-11-18 18:38:43 +0800 CST
程序员茄子在线接单