Vaultwarden 深度解剖:用 Rust 把 Bitwarden 服务端压进十几 MB 内存——从零知识加密模型、Rocket 架构到 Diesel 多数据库落地的工程真相
官方 Bitwarden 自建一套要跑 SQL Server、Identity、Api、Admin、Notifications、Icons 一大堆容器,内存动辄两三个 G。而 Vaultwarden 用一个 Rust 单体二进制,把整套 Bitwarden 协议重写了一遍,空载内存十几 MB,一个树莓派就能扛全家人的密码库。这篇文章不讲怎么
docker run,而是把它的加密模型、Web 框架选型、ORM 抽象、通知推送和管理后台一层层扒开,讲清楚它为什么能这么小、又为什么依然安全。
如果你只是想部署,网上一堆一键脚本。但如果你想搞明白"一个密码管理器的服务端到底在管什么"——答案可能会颠覆你的直觉:它几乎什么敏感信息都看不到。这正是 Vaultwarden 这类项目最迷人的地方,也是本文的主线。
一、背景:为什么会有 Vaultwarden
Bitwarden 是目前最流行的开源密码管理器之一。它的客户端(浏览器插件、桌面端、移动端、CLI)全部开源,协议也公开。但官方的服务端(bitwarden/server,C# + .NET)体量庞大,微服务拆得很细,对自建用户极不友好:
- 依赖 Microsoft SQL Server(后来支持 PostgreSQL/MySQL,但历史包袱重)
- 多个容器:
api、identity、web、admin、notifications、icons、sso、mssql…… - 官方自建版还阉割了付费功能(组织、TOTP、附件等)
于是社区里出现了 Vaultwarden(早期叫 bitwarden_rs,2021 年因商标问题改名)。它的定位非常清晰:
用 Rust 重新实现 Bitwarden 的服务端 API,做到与官方客户端 100% 兼容,但用单个轻量二进制承载全部功能(含付费功能),面向个人和小团队自建。
关键词有三个:协议兼容、单体轻量、功能齐全。这三点的取舍决定了它全部的架构选择。
一个直观对比:
| 维度 | 官方 bitwarden/server | Vaultwarden |
|---|---|---|
| 语言 | C# / .NET | Rust |
| 容器数 | 8+ | 1 |
| 空载内存 | ~2 GB | ~15 MB |
| 数据库 | MSSQL(主)/PG/MySQL | SQLite / MySQL / PostgreSQL |
| 付费功能 | 需授权 | 默认全开 |
| 目标人群 | 企业 | 个人 / 小团队 |
注意:Vaultwarden 不是官方项目,官方不提供支持,也不建议大型组织用它替代企业版。它是"够用、够小、够快"的自建方案。
二、核心概念:先搞懂"零知识加密",才看得懂这个服务端
要理解 Vaultwarden 的架构,必须先理解一件反直觉的事:服务端根本不知道你的密码,也解不开你的密码库。这套模型叫零知识(zero-knowledge)架构。整个 Vaultwarden 的代码,本质上是围绕"如何在看不见明文的前提下,正确地存取和同步加密数据块"来组织的。
2.1 主密码不上传,上传的是"派生哈希"
当你在客户端输入主密码(master password)时,客户端会做两次 KDF(密钥派生):
# 伪代码,展示 Bitwarden 客户端侧的密钥派生逻辑
master_key = KDF(master_password, email_as_salt, iterations) # 第一层:算出主密钥
master_pw_hash = KDF(master_key, master_password, 1) # 第二层:再派生一次,得到"密码哈希"
KDF早期是 PBKDF2-SHA256(默认 60 万次迭代),新版本支持 Argon2id(抗 GPU/ASIC 更强)。- 上传到服务端的是
master_pw_hash,而不是主密码本身,也不是master_key。 - 服务端拿到
master_pw_hash后,还会再套一层服务端 KDF(PBKDF2)存进数据库,防止拖库后直接比对。
所以服务端存的是"哈希的哈希"。即使数据库被拖走,攻击者也拿不到主密码,更解不开保险库。
2.2 真正解密数据的密钥,服务端永远看不到
master_key 经过 HKDF 扩展后,用来加密/解密一个随机生成的对称密钥(叫 "user symmetric key" / "protected key"):
stretched_key = HKDF-Expand(master_key) # 拉伸成加密 + MAC 两段
user_symmetric_key = random(512 bit) # 注册时随机生成,一次性
protected_key = AES-256-CBC-Encrypt(user_symmetric_key, stretched_key) + HMAC
- 你保险库里每一条记录(登录项、笔记、卡片)都是用
user_symmetric_key加密的。 user_symmetric_key本身被stretched_key加密后,以protected_key的形式存在服务端。- 服务端存着
protected_key,但没有master_key,所以永远解不开user_symmetric_key,也就解不开任何一条数据。
这就是零知识:服务端是个"加密数据块的存储和同步中心",而不是"能读你数据的中心"。
2.3 CipherString:贯穿全代码的密文格式
Bitwarden 里所有密文都遵循一个统一的字符串格式,Vaultwarden 里叫它 CipherString:
<type>.<iv>|<ct>|<mac>
# 例如 AES-256-CBC + HMAC-SHA256:
2.abc123...==|def456...==|ghi789...==
type=0:AES-256-CBC(无 MAC,已废弃)type=2:AES-256-CBC + HMAC-SHA256(主力)type=4/6:RSA-2048-OAEP 系列(用于组织密钥分发)
关键点:Vaultwarden 服务端只把 CipherString 当成不透明的字符串来搬运和存储,从不解析里面的密文。 这大幅简化了服务端逻辑——它不需要任何加密库来处理保险库内容,只需要处理认证、路由、数据库读写。这也是它能做得这么小的根本原因之一。
理解了这三层,你就明白 Vaultwarden 服务端的"职责边界"了:认证 + 存取加密块 + 多端同步 + 组织密钥分发。它不碰明文。下面看它怎么用 Rust 把这些职责实现出来。
三、架构分析:一个 Rust 单体是怎么组织的
Vaultwarden 的技术栈可以一句话概括:
Rocket(Web 框架)+ Diesel(ORM)+ SQLite/MySQL/PostgreSQL(存储)+ 内嵌 web-vault(前端静态资源)+ WebSocket(实时通知),全部编译进一个静态二进制。
3.1 整体分层
┌─────────────────────────────────────────────┐
│ 官方 Bitwarden 客户端(浏览器/桌面/移动/CLI) │
└───────────────┬─────────────────────────────┘
│ HTTPS (REST) + WebSocket
┌───────────────▼─────────────────────────────┐
│ Vaultwarden 二进制 │
│ ┌────────────┐ ┌──────────────┐ │
│ │ Rocket │ │ web-vault │ 内嵌静态前端 │
│ │ 路由/Fairing│ │ (官方前端产物) │ │
│ └─────┬──────┘ └──────────────┘ │
│ ┌─────▼──────────────────────────────────┐ │
│ │ api::core / api::identity / api::admin │ │ 业务逻辑
│ │ api::notifications (WebSocket) │ │
│ └─────┬──────────────────────────────────┘ │
│ ┌─────▼──────────────────────────────────┐ │
│ │ db::models + Diesel(多后端宏抽象) │ │ 数据访问
│ └─────┬──────────────────────────────────┘ │
└────────┼──────────────────────────────────────┘
▼
SQLite / MySQL / PostgreSQL
代码目录(src/ 下)大致对应:
api/core/—— 保险库核心 API:ciphers、folders、organizations、sends、attachments……api/identity.rs—— 登录、令牌、刷新、二步验证api/admin.rs—— 管理后台(Vaultwarden 独有,官方前端没有)api/notifications.rs—— WebSocket 实时推送api/icons.rs—— 网站图标抓取与缓存db/models/+db/schemas/—— 数据模型与三套数据库 schemaauth.rs—— JWT 签发与校验、各种请求守卫(Guard)crypto.rs—— 服务端侧用到的少量加密(密码哈希、随机数、令牌)config.rs—— 配置系统(环境变量 + 运行时可改)
3.2 为什么选 Rocket
Rocket 是 Rust 里"人体工学"最好的 Web 框架之一,靠宏和类型系统把很多校验前置到编译期。Vaultwarden 用得最狠的是它的**请求守卫(Request Guard)**机制——把认证、限流、配置注入都做成类型:
// 简化示意:一个需要登录的接口
#[post("/ciphers", data = "<data>")]
async fn post_ciphers(
data: Json<CipherData>, // 自动反序列化 + 校验
headers: Headers, // 自定义 Guard:解析并校验 JWT,注入当前用户
mut conn: DbConn, // 自定义 Guard:从连接池取一个数据库连接
) -> JsonResult {
let user = headers.user; // 走到这里,用户一定已经通过认证
// ... 业务逻辑
}
Headers 这个 Guard 内部做的事:从 Authorization: Bearer <jwt> 取出令牌 → 验签 → 查用户 → 校验有效性 → 注入。任何一步失败,请求根本进不了函数体。这种"认证即类型"的写法,让业务代码几乎不用写重复的鉴权样板,也更难写出漏鉴权的 bug。
Rocket 的 Fairing(类似中间件)则被用来做全局的 CORS、请求日志、安全响应头等。
3.3 一个容易忽视的设计:内嵌前端
Vaultwarden 不自己写前端。它直接把官方开源的 web-vault(Bitwarden 网页版)构建产物打包进二进制或挂到静态目录。这样做的好处:
- 前端行为与官方完全一致,升级只需换一版静态资源
- 服务端专注 API,职责单一
- 单二进制即可提供完整 Web 界面
代价是:web-vault 的版本要和 Vaultwarden 支持的 API 版本对齐,否则新前端调用了老服务端没实现的接口会报错。这也是升级时偶尔踩坑的点。
四、代码实战:几个关键机制的实现细节
光讲架构太虚,挑几个真正体现工程功力的点细看。
4.1 Diesel 多数据库:用宏消灭三倍代码
Vaultwarden 要同时支持 SQLite、MySQL、PostgreSQL。三套数据库的类型、方言、连接方式都不同。如果为每个模型写三遍 CRUD,维护成本爆炸。它的解法是用宏把差异收拢:
// 简化示意 Vaultwarden 的 db_object! 宏思路
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = ciphers)]
pub struct Cipher {
pub uuid: String,
pub user_uuid: Option<String>,
pub organization_uuid: Option<String>,
pub atype: i32,
pub data: String, // 存 CipherString / JSON,服务端不解析
pub favorite: bool,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
}
db_object! 宏会为 SQLite / MySQL / PostgreSQL 各展开一套 Cipher 结构体和对应的 From 转换。业务层只用统一的 Cipher,运行时根据编译特性(feature flag)或连接类型分派到具体后端。
连接层则用一个枚举把三种连接池包起来:
// 概念示意:统一的数据库连接抽象
pub enum DbConnInner {
Sqlite(SqliteConnection),
Mysql(MysqlConnection),
Postgresql(PgConnection),
}
// 查询时用宏统一分派
macro_rules! db_run {
($conn:ident: $body:block) => {
match $conn {
DbConnInner::Sqlite(c) => { ... $body },
DbConnInner::Mysql(c) => { ... $body },
DbConnInner::Postgresql(c) => { ... $body },
}
};
}
工程启示:当你要在同一套业务逻辑下适配多个后端时,与其写运行时的 if/else 抽象层,不如用宏在编译期展开——性能零损耗,类型全保留,还能让编译器帮你检查每个分支。这是 Rust 生态里非常典型的"零成本抽象"手法。
对个人自建,强烈建议用 SQLite:单文件、零运维、备份就是复制一个文件,配合前面提到的 Litestream 还能做到近实时增量备份到对象存储。只有多写并发压力大或已有数据库基建时,才需要 MySQL/PostgreSQL。
4.2 登录与 JWT:服务端在验什么
登录流程(api/identity.rs)大致是:
- 客户端用邮箱 +
master_pw_hash请求/identity/connect/token - 服务端取出该用户存的"服务端哈希",用
crypto.rs里的验证函数比对 - 通过后,如启用二步验证(TOTP/WebAuthn/邮箱等),走二次校验
- 全部通过,签发 JWT(access token)+ refresh token
服务端密码校验的核心(简化):
// crypto.rs 思路:服务端再套一层 PBKDF2,用常量时间比较
pub fn verify_password_hash(
input_hash: &[u8], // 客户端上传的 master_pw_hash
salt: &[u8],
stored_hash: &[u8], // 数据库里存的
iterations: u32,
) -> bool {
let computed = pbkdf2_hmac_sha256(input_hash, salt, iterations);
// 常量时间比较,防时序攻击
crypto::ct_eq(&computed, stored_hash)
}
两个细节值得学:
- 再套一层服务端 KDF:客户端已经派生过了,服务端再派生一次存储,等于拖库后攻击者还要暴破一层。
- 常量时间比较(
ct_eq):绝不能用==直接比哈希,否则字节逐位比较的提前返回会泄露信息,构成时序侧信道。
签发的 JWT 用服务端私钥(RSA,首次启动自动生成 rsa_key.pem)签名。之后每个请求的 Headers Guard 用公钥验签,无状态、不查库即可确认身份,这也是它能扛住多端频繁同步的原因。
4.3 实时同步:WebSocket 通知
多设备场景下,你在手机上改了一条密码,希望电脑插件立刻刷新。Bitwarden 协议用 WebSocket(SignalR 风格)推送变更事件。Vaultwarden 在 api/notifications.rs 里实现了这套:
// 概念示意:变更后向该用户的所有活跃连接广播
pub fn notify_cipher_update(
ut: UpdateType, // 枚举:SyncCipherUpdate / SyncCipherDelete ...
cipher: &Cipher,
users: &[String], // 受影响用户
ws: &WebSocketUsers,
) {
for user_uuid in users {
ws.send_update(user_uuid, ut, &cipher.uuid);
}
}
推送的只是"某某对象变了"的信号,不含明文数据。客户端收到信号后,主动拉取加密块再本地解密。这再次呼应零知识原则:连推送通道都不经手明文。
值得一提:老版本 Vaultwarden 需要单独跑一个 WebSocket 端口 + 反代 /notifications/hub,新版本已内建,配置简化了不少。升级时如果发现"改了不实时同步",八成是反代没把 WebSocket 升级头(Upgrade/Connection)透传过去。
4.4 组织与密钥分发:RSA 上场的地方
个人保险库全靠对称密钥就够了。但"组织/共享集合"需要多人访问同一批数据,这时对称密钥怎么安全地发给每个成员?答案是 RSA 非对称加密:
- 每个用户注册时生成一对 RSA 密钥,私钥用
user_symmetric_key加密后存服务端 - 创建组织时生成一个"组织对称密钥"
- 邀请成员时,用成员的 RSA 公钥加密"组织对称密钥",发给该成员
- 成员登录后,用自己的私钥解出组织密钥,再用它解组织里的数据
服务端全程只搬运这些密文(type=4/6 的 CipherString),它有公钥、有被加密的私钥,但没有解私钥的钥匙。密钥分发这么敏感的环节,服务端依然是零知识。这套设计的优雅之处在于:把"多人共享"这个看似需要服务端参与信任的问题,用非对称加密彻底下放到了客户端。
五、性能优化:它为什么能这么小、这么快
Vaultwarden 常被夸"树莓派都能跑",背后是一系列取舍:
5.1 内存小的根因
- 不解析保险库内容:服务端把密文当字符串搬,省掉了所有加解密内存开销。
- 无 JVM/CLR 运行时:Rust 编译成原生码,没有虚拟机常驻内存。
- Rocket + Diesel 都是编译期展开:运行时对象图很小。
- SQLite 默认:不用为独立数据库进程留内存。
实测空载常驻内存通常在 10~30 MB 区间,取决于连接池大小和图标缓存。对比官方版动辄 GB 级,差了两个数量级。
5.2 编译特性裁剪
Vaultwarden 用 Cargo feature 控制编译进哪些数据库后端。只用 SQLite 时,可以只编 sqlite feature,二进制更小、依赖更少:
# 只要 SQLite,构建时关掉其它后端
# cargo build --release --no-default-features --features sqlite
[features]
default = ["sqlite"]
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
postgresql = ["diesel/postgres", "diesel_migrations/postgres"]
5.3 图标缓存与外部请求节流
api/icons.rs 负责抓取网站 favicon 给保险库项目做图标。这是唯一会主动发外部请求的模块,也是最容易出性能/隐私问题的地方。Vaultwarden 的处理:
- 本地磁盘缓存:抓过的图标存本地,设 TTL,避免重复请求。
- 可完全关闭:
ICON_SERVICE/DISABLE_ICON_DOWNLOAD等配置项让隐私敏感用户彻底断掉外联。 - 超时与大小限制:防止恶意站点拖慢服务或塞大文件。
隐私建议:如果你不想让服务器因为你保存的某个网站而去访问它(可能暴露你用了哪些服务),把图标下载关掉,用内置 ICON_SERVICE=internal 或直接禁用。
5.4 连接池调优
Diesel 用 r2d2 做连接池。默认池大小对个人足够,但如果你上了 MySQL/PostgreSQL 且并发高,可以调 DATABASE_MAX_CONNS。SQLite 由于写是串行的(单写锁),池子调太大意义不大,反而可能触发 database is locked。这是自建时最常见的坑之一:SQLite 场景别盲目加大连接池。
六、自建实战要点(不是教程,是避坑清单)
结合前面的原理,给几条真正基于架构理解的建议:
必须上 HTTPS。虽然保险库内容是端到端加密的,但登录时上传的
master_pw_hash、JWT 令牌都走 HTTP 明文会被截。反代(Caddy/Nginx/Traefik)加 TLS 是底线。WebSocket 头要透传。反代配置里务必转发
Upgrade和Connection头,否则实时同步失效。新版内建 WS,但反代仍需正确转发。备份就是备份那个 SQLite 文件 + attachments 目录 + config.json + rsa_key.pem。特别是
rsa_key.pem——它是签 JWT 的私钥,丢了不影响数据但所有人要重新登录;config.json存着运行时改的配置。关掉开放注册。
SIGNUPS_ALLOWED=false,只留自己。或用SIGNUPS_DOMAINS_WHITELIST限定域名。管理后台(/admin,用ADMIN_TOKEN保护)可手动邀请。ADMIN_TOKEN用哈希形式。新版支持存 Argon2 哈希而非明文,别把明文 token 写进 compose 文件裸奔。KDF 迭代别乱调低。为了"登录快点"把 PBKDF2 迭代数调很低,等于削弱了抗暴破能力。默认值(PBKDF2 60 万 / 或切 Argon2id)是安全与体验的平衡点。
理解它的边界:Vaultwarden 是个人/小团队方案。企业级审计、SSO/SCIM 深度集成、SLA 支持,还是得看官方。别拿它扛几百上千人的合规场景。
七、总结与展望
把 Vaultwarden 拆到这个程度,能提炼出几条超越"密码管理器"本身的工程思想:
零知识不是口号,是架构约束。因为服务端从设计上就"看不懂"数据,所以它可以做得极简——不需要加密库处理内容、不需要复杂的权限模型去保护明文、连实时推送都只发信号不发数据。"能力越小,攻击面越小",这是安全系统最朴素也最有效的设计哲学。
兼容既有协议,是四两拨千斤的杠杆。Vaultwarden 不造客户端、不造前端,只精准实现服务端 API,就复用了整个 Bitwarden 生态的客户端、审计和用户习惯。选对"实现哪一层",比"实现多少功能"重要得多。
Rust 的零成本抽象在多后端场景真香。用宏在编译期展开三套数据库代码,既没有运行时开销,又保留了完整类型检查——这是很多 GC 语言用运行时反射/动态分派换来的"灵活"所比不了的。
单体不是落后,是对场景的诚实。微服务解决的是超大规模团队的协作和伸缩问题。对"一个家庭 / 一个小团队的密码库",单体二进制才是最优解:部署简单、资源极省、故障面小。架构没有高低,只有匹配。
展望上,随着 Argon2id 成为默认、passkey/WebAuthn 无密码登录普及,Vaultwarden 这类项目会越来越多地把重心从"存密码"转向"管凭据与身份"。但只要那条零知识的红线不变——服务端永远看不见你的秘密——它的核心架构就依然成立。
对我们这些写代码的人,Vaultwarden 最大的价值或许不是"省了几十块订阅费",而是它用一万多行 Rust,给我们演示了一个安全系统该有的样子:把信任降到最低,把明文挡在门外,把复杂留给客户端,把服务端做到小到无懈可击。
本文基于 Vaultwarden 公开代码结构与 Bitwarden 公开加密白皮书整理,代码片段为讲解用的简化示意而非逐行照搬,实际实现以对应版本源码为准。自建涉及安全配置,请以官方文档最新说明为准。