构建零信任密钥管理系统:设计决策背后的权衡
开篇:团队协作中的密钥共享困境
「我们怎么共享生产环境的数据库密码?」
这个看似简单的问题,在我构建 Sealbox 的过程中反复出现。团队需要在 CI 服务器上访问密钥,开发者需要在本地调试时获取 API 密钥,DevOps 工程师需要在不同环境间分发敏感配置。
最直观的解决方案?把私钥拷贝给需要的人。
但这违反了密码学的基本原则:“private key” 应该是 private 的。一旦开始复制私钥,你就失去了对访问权限的控制。谁在什么时候访问了什么密钥?如何撤销某个人的访问权限而不影响其他人?如何应对员工离职或设备丢失?
这些问题让我意识到,Sealbox 的「每个用户一个密钥对」架构,在面对真实的团队协作场景时,存在根本性的局限。
现有架构的局限性:“by user” 模型的问题
Sealbox 最初的设计很简洁:每个用户生成一个 RSA 密钥对,所有 Secret 都用这个用户的公钥加密。用户想要访问 Secret 时,用自己的私钥解密。
User Alice ——— Master Key ——— Multiple Secrets
│ │ │
└─ 私钥 └─ 公钥 └─ 加密数据
这个模型在单人使用时完美工作。但当团队需要共享 Secret 时,问题就暴露了:
问题一:共享困难
假设 Alice 创建了一个数据库密码,现在 Bob 也需要访问。在 “by user” 模型下,这个 Secret 只能被 Alice 的私钥解密。要让 Bob 访问,要么:
- Alice 把私钥发给 Bob(违反私钥安全原则)
- Alice 解密后把明文发给 Bob(绕过了整个安全系统)
- Alice 重新用 Bob 的公钥加密一份(需要知道 Bob 的公钥,且产生多份数据)
每种方案都有严重缺陷。
问题二:权限粗粒度
更糟糕的是,一旦 Bob 获得了 Alice 的私钥,他就能访问 Alice 的所有 Secret。无法实现「Bob 只能访问数据库密码,但不能访问 API 密钥」这样的细粒度控制。
问题三:撤销困难
当 Bob 离职时,如何撤销他的访问权限?如果他有 Alice 的私钥拷贝,唯一的办法是 Alice 生成新的密钥对,然后重新加密所有 Secret。这会影响到其他所有有权限的人。
问题四:审计不可能
谁在什么时候访问了什么 Secret?在私钥复制的模式下,这变得无法追踪。服务端只能看到「Alice 的密钥」被使用了,但不知道实际使用者是谁。
这些问题指向一个根本性的架构缺陷:“by user” 模型混淆了身份(identity)和访问实体(access entity)的概念。
在现实中,需要访问 Secret 的往往不是「用户」,而是「客户端」——开发者的笔记本、CI 服务器、生产环境的应用实例。这些客户端可能属于同一个用户,也可能属于不同用户,但它们应该有独立的安全身份。
核心洞察:“by client” 设计哲学
解决方案的核心洞察是:Master Key 应该 by client 而不是 by user。
这个概念转变看似简单,但它代表了从「基于用户身份的信任模型」到「零信任安全模型」的根本性转变。
User Alice ——— Multiple Clients ——— Shared Secrets Pool
│ │ │
└─ 管理者 └─ 独立密钥对 └─ 多重加密
零信任的核心原则
在传统安全模型中,我们信任「用户」。一旦验证了用户身份,就假设这个用户的所有操作都是可信的。但零信任模型认为:
- 每个访问请求都需要独立验证
- 没有「内在可信」的实体
- 最小权限原则:只给予完成任务所需的最小权限
在密钥管理的语境下,这意味着每个 sealbox-cli 实例——不管是开发者的笔记本、CI 服务器还是生产环境——都应该有自己独立的安全身份。
独立安全主体
“by client” 架构将每个客户端实例视为独立的安全主体:
- Alice 的笔记本:
client-alice-laptop - CI 服务器:
client-ci-server - 生产部署:
client-prod-deploy
每个客户端都有自己的 RSA 密钥对,互相独立。这样做的好处:
- 细粒度控制:可以精确控制哪个客户端能访问哪个 Secret
- 独立撤销:禁用某个客户端不影响其他客户端
- 清晰审计:知道是哪个具体客户端在什么时候访问了什么
- 故障隔离:某个客户端的私钥泄露不会影响其他客户端
权限不可变性
新架构的另一个关键设计是:Secret 的访问权限在创建时确定,不支持后续动态添加。
这看似限制,实际上是安全特性。因为服务端永远不接触明文数据,无法为新客户端重新加密现有 Secret。这强制实现了:
- 显式授权:必须明确声明谁能访问什么
- 权限不膨胀:避免「先给权限,后面再限制」的不良模式
- 明确责任:创建 Secret 的人需要仔细考虑谁真正需要访问
这种「不便」实际上促进了更好的安全实践。
技术权衡:关键设计决策的思考过程
理念很美好,但如何实现?关键的技术决策是:一个 Secret 对应一个 DataKey,但这个 DataKey 被多个客户端的公钥加密。
Envelope Encryption 的多客户端实现
在 Sealbox 中,我们使用 Envelope Encryption 模式:实际数据用 AES 对称加密(快速),AES 密钥(DataKey)用 RSA 非对称加密(安全)。
多客户端架构的核心决策是:共享 DataKey 而不是多重数据加密。
Secret("database-password")
├── DataKey: random_256_bit_key
├── encrypted_data: AES(DataKey, "mysqlpass123") [只存储一份]
└── Multiple encrypted_data_key records:
├── RSA(client_A_pubkey, DataKey)
├── RSA(client_B_pubkey, DataKey)
└── RSA(client_C_pubkey, DataKey)
为什么共享 DataKey?
这个设计有三个考虑:
- 数据一致性:所有授权客户端解密后看到相同的明文,避免同步问题
- 存储效率:
encrypted_data只存储一份,而不是每个客户端一份 - 密码学正确性:符合 Envelope Encryption 的标准实践
有人可能担心:多个客户端共享同一个 DataKey 是否安全?
答案是安全的。虽然 DataKey 相同,但每个客户端只能用自己的私钥解密自己的 encrypted_data_key 记录。没有私钥的客户端永远无法获得 DataKey,因此无法解密数据。
权限撤销的实现
当需要撤销某个客户端的访问权限时,只需删除对应的 encrypted_data_key 记录即可:
DELETE FROM secret_client_keys
WHERE secret_key = 'database-password'
AND client_id = 'client-bob-laptop';
被撤销的客户端立即失去访问能力,但其他客户端不受影响。这比重新生成所有密钥要高效得多。
权限模型的演进
从实现角度,新的权限模型要求重新设计数据库架构:
-- 客户端注册表
CREATE TABLE clients (
id BLOB PRIMARY KEY, -- UUID,客户端唯一标识
name TEXT NOT NULL, -- 客户端名称/别名
public_key TEXT NOT NULL, -- 客户端公钥
status TEXT DEFAULT 'Active' -- Active/Disabled/Retired
);
-- Secret-客户端密钥关联表
CREATE TABLE secret_client_keys (
secret_key TEXT NOT NULL,
client_id BLOB NOT NULL,
encrypted_data_key BLOB NOT NULL, -- 用该客户端公钥加密的 DataKey
PRIMARY KEY (secret_key, client_id)
);
这个设计支持了我们需要的所有权限操作:
- 精确授权:在
secret_client_keys表中为特定客户端创建记录 - 权限查询:
SELECT * FROM secret_client_keys WHERE client_id = ? - 权限撤销:
DELETE FROM secret_client_keys WHERE ... - 审计追踪:所有访问都有明确的客户端标识
与传统 RBAC 的区别
这不是传统的基于角色的访问控制(RBAC)。在 RBAC 中,用户有角色,角色有权限。在我们的模型中:
- 客户端是独立的安全主体,不依赖用户角色
- 权限直接绑定到客户端,不通过角色中介
- 权限在 Secret 创建时确定,不支持动态变更
这种设计更适合 DevOps 环境,因为「谁需要访问什么」往往基于技术架构而不是组织架构。
安全性与可用性的平衡
每个架构决策都涉及安全性与可用性的权衡。让我分享几个关键的权衡点:
权衡一:创建时授权 vs 动态权限
- 安全优势:权限不会意外扩张,每个授权都是显式的
- 可用性成本:无法临时给某人访问权限,需要重新创建 Secret
- 决策:优先安全性,因为密钥泄露的风险远大于操作不便
权衡二:客户端独立性 vs 管理复杂度
- 安全优势:一个客户端被攻破不影响其他客户端
- 可用性成本:需要管理更多密钥对,客户端注册流程更复杂
- 决策:可以通过工具和自动化降低管理成本,安全收益值得这个成本
权衡三:服务端零知识 vs 功能限制
- 安全优势:服务端被攻破也无法获得明文 Secret
- 可用性成本:无法实现某些便利功能(如 Secret 搜索、内容预览)
- 决策:零知识是密钥管理系统的核心要求,功能限制是可接受的
这些权衡没有标准答案。但在安全系统中,我的原则是:当安全性与便利性冲突时,优先安全性,然后寻找降低便利性成本的方法。
实际影响:对其他系统设计的启发
这次架构重新设计的过程让我对安全系统设计有了更深的理解。其中几个原则不仅适用于密钥管理,对其他领域的系统设计也有启发意义:
重新审视「身份」概念
很多系统都有「用户身份」的概念,但真正需要访问资源的往往不是抽象的「用户」,而是具体的「访问实体」——设备、应用实例、服务进程。
- 在容器编排中,是 Pod 而不是开发者需要访问数据库
- 在微服务架构中,是服务实例而不是团队需要调用 API
- 在 CI/CD 中,是构建器而不是提交者需要访问构建缓存
将访问权限直接绑定到实际的访问实体,而不是绕道通过用户身份,往往能获得更精确的安全控制。
「创建时确定」的权限模型
权限在资源创建时确定,不支持后续动态修改——这个看似限制性的设计,实际上在很多场景下都是更安全的选择:
- Immutable Infrastructure:基础设施配置在部署时确定,运行时不可变更
- Container Images:依赖和配置在构建时确定,运行时只读
- Database Permissions:表的权限设计在 schema 创建时确定,避免运行时权限膨胀
这种「不便」强制我们在设计阶段仔细思考权限边界,而不是「先宽松后收紧」。
零信任的渗透
零信任不只是网络安全概念,它是一种设计哲学:不信任任何默认状态,每个操作都要验证。
这个原则可以应用到:
- API 设计:每个请求都独立验证,不依赖 session 状态
- 数据库查询:每个查询都检查权限,不依赖应用层的访问控制
- 配置管理:每个配置变更都要审计,不依赖「管理员不会犯错」的假设
技术债务的真正成本
最深的感悟是:架构设计的问题比功能 bug 更难修复。
功能 bug 影响特定场景,架构问题影响整个系统的演进能力。当你发现需要重新设计核心架构时,往往意味着之前的技术债务已经成为业务发展的阻碍。
在 Sealbox 的情况下,“by user” 架构看起来工作正常,直到用户开始真正的团队协作。这个时候,修复问题的成本远远超过重新设计的成本。
设计的向前兼容性
好的架构设计应该为未来的需求留出空间。在密钥管理的语境下,这意味着:
- 支持新的加密算法(密码学在演进)
- 支持新的客户端类型(IoT 设备、移动应用等)
- 支持新的权限模型(临时访问、条件访问等)
但「向前兼容」不意味着「功能堆砌」。关键是找到稳定的抽象层,在这个抽象之上构建新功能。
总结
从 “by user” 到 “by client” 的架构演进,表面上看是为了解决团队协作中的 Secret 共享问题,但实际上反映了更深层的设计哲学转变:
从基于身份的信任到零信任模型。每个访问实体都有独立的安全身份,每个操作都需要独立验证。
从动态权限到不可变权限。权限在创建时确定,强制我们在设计阶段仔细思考安全边界。
从便利优先到安全优先。当安全性与便利性冲突时,优先保证安全性,然后寻找降低便利性成本的方法。
这些原则不仅适用于密钥管理,也适用于其他需要安全控制的系统设计。下次当你面临类似的架构决策时,不妨问问自己:
- 真正需要访问资源的是「用户」还是「访问实体」?
- 权限能否在创建时确定,避免后续的权限膨胀?
- 这个设计能否经受住零信任模型的审视?
好的安全架构设计往往不是最便利的,但它能让你在面对真实世界的复杂性时,依然保持信心。
如果你对密钥管理系统的设计理念感兴趣,可以阅读 为什么我构建了 Sealbox 了解更多背景故事。或者查看 Rust Builder Pattern 指南 了解 API 设计中的类似权衡思考。