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 的数据库,他拿到的是:
- 一个经过 60 万轮 PBKDF2 + 服务端叠加哈希的登录凭据 —— 反推主密码的成本是天文数字
- 一堆 AES-256-CBC + HMAC-SHA256 加密的密码库条目 —— 没有对称密钥就是乱码
- 一个被 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.sqlite3,ciphers 表里的 data 字段全是这样的密文 JSON——这也是你可以放心把数据库备份扔到任何网盘的原因(当然,仍建议再套一层加密,纵深防御)。
三、架构分析:一个 Rust 二进制如何替代十几个 .NET 容器
3.1 技术栈拆解
Vaultwarden 的技术选型非常「Rust 正统」:
| 组件 | 选型 | 作用 |
|---|---|---|
| Web 框架 | Rocket | HTTP API 路由、请求处理 |
| ORM | Diesel | 同一套模型编译期适配 SQLite/MySQL/PostgreSQL |
| 异步运行时 | Tokio | WebSocket 推送、定时任务 |
| 密码哈希 | 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 计算(这是故意的,抗暴力破解)。需要注意的边界只有两个:
- 用户量超过 50 或多实例部署时切 PostgreSQL。 SQLite 的写锁是库级别的,高并发写会排队。设置
DATABASE_URL=postgresql://...即可无缝切换,Diesel 在编译期已经把三种数据库的适配都做好了。 - 图标缓存目录会膨胀。
icon_cache抓多了几百 MB 很正常,可以定期清理或设置ICON_CACHE_TTL控制过期。
5.2 安全加固清单
按优先级排列,前四条是底线:
SIGNUPS_ALLOWED=false—— 公网上开放注册的 Vaultwarden 等于公共厕所,扫描器每天都在找这种实例- 强制 HTTPS + HSTS —— Caddy 默认已带 HSTS;用 Nginx 的自己加
Strict-Transport-Security头 - Admin 面板要么 Argon2 Token,要么干脆
ADMIN_TOKEN留空禁用 —— admin 面板可以看到用户列表和邀请入口,是高价值目标 - 开启两步验证(TOTP/WebAuthn) —— Vaultwarden 免费支持 WebAuthn,插个 YubiKey 或用手机 Passkey,钓鱼免疫
- fail2ban + 应用层限流(前文已配)
- 更狠一点:不把服务暴露公网,套一层 WireGuard / Tailscale,只有自己设备能访问——密码管理对实时性要求不高,内网方案的攻击面直接归零
- 关注上游安全公告: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,这个周末就值得花半小时把它跑起来——这大概是投入产出比最高的自托管项目,没有之一。