构建零信任密钥管理系统:设计决策背后的权衡

8 min read
Read in English

开篇:团队协作中的密钥共享困境

「我们怎么共享生产环境的数据库密码?」

这个看似简单的问题,在我构建 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 访问,要么:

  1. Alice 把私钥发给 Bob(违反私钥安全原则)
  2. Alice 解密后把明文发给 Bob(绕过了整个安全系统)
  3. 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” 架构将每个客户端实例视为独立的安全主体:

每个客户端都有自己的 RSA 密钥对,互相独立。这样做的好处:

  1. 细粒度控制:可以精确控制哪个客户端能访问哪个 Secret
  2. 独立撤销:禁用某个客户端不影响其他客户端
  3. 清晰审计:知道是哪个具体客户端在什么时候访问了什么
  4. 故障隔离:某个客户端的私钥泄露不会影响其他客户端

权限不可变性

新架构的另一个关键设计是: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?

这个设计有三个考虑:

  1. 数据一致性:所有授权客户端解密后看到相同的明文,避免同步问题
  2. 存储效率encrypted_data 只存储一份,而不是每个客户端一份
  3. 密码学正确性:符合 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)
);

这个设计支持了我们需要的所有权限操作:

与传统 RBAC 的区别

这不是传统的基于角色的访问控制(RBAC)。在 RBAC 中,用户有角色,角色有权限。在我们的模型中:

这种设计更适合 DevOps 环境,因为「谁需要访问什么」往往基于技术架构而不是组织架构。

安全性与可用性的平衡

每个架构决策都涉及安全性与可用性的权衡。让我分享几个关键的权衡点:

权衡一:创建时授权 vs 动态权限

权衡二:客户端独立性 vs 管理复杂度

权衡三:服务端零知识 vs 功能限制

这些权衡没有标准答案。但在安全系统中,我的原则是:当安全性与便利性冲突时,优先安全性,然后寻找降低便利性成本的方法

实际影响:对其他系统设计的启发

这次架构重新设计的过程让我对安全系统设计有了更深的理解。其中几个原则不仅适用于密钥管理,对其他领域的系统设计也有启发意义:

重新审视「身份」概念

很多系统都有「用户身份」的概念,但真正需要访问资源的往往不是抽象的「用户」,而是具体的「访问实体」——设备、应用实例、服务进程。

将访问权限直接绑定到实际的访问实体,而不是绕道通过用户身份,往往能获得更精确的安全控制。

「创建时确定」的权限模型

权限在资源创建时确定,不支持后续动态修改——这个看似限制性的设计,实际上在很多场景下都是更安全的选择:

这种「不便」强制我们在设计阶段仔细思考权限边界,而不是「先宽松后收紧」。

零信任的渗透

零信任不只是网络安全概念,它是一种设计哲学:不信任任何默认状态,每个操作都要验证。

这个原则可以应用到:

技术债务的真正成本

最深的感悟是:架构设计的问题比功能 bug 更难修复。

功能 bug 影响特定场景,架构问题影响整个系统的演进能力。当你发现需要重新设计核心架构时,往往意味着之前的技术债务已经成为业务发展的阻碍。

在 Sealbox 的情况下,“by user” 架构看起来工作正常,直到用户开始真正的团队协作。这个时候,修复问题的成本远远超过重新设计的成本。

设计的向前兼容性

好的架构设计应该为未来的需求留出空间。在密钥管理的语境下,这意味着:

但「向前兼容」不意味着「功能堆砌」。关键是找到稳定的抽象层,在这个抽象之上构建新功能。

总结

从 “by user” 到 “by client” 的架构演进,表面上看是为了解决团队协作中的 Secret 共享问题,但实际上反映了更深层的设计哲学转变:

从基于身份的信任到零信任模型。每个访问实体都有独立的安全身份,每个操作都需要独立验证。

从动态权限到不可变权限。权限在创建时确定,强制我们在设计阶段仔细思考安全边界。

从便利优先到安全优先。当安全性与便利性冲突时,优先保证安全性,然后寻找降低便利性成本的方法。

这些原则不仅适用于密钥管理,也适用于其他需要安全控制的系统设计。下次当你面临类似的架构决策时,不妨问问自己:

好的安全架构设计往往不是最便利的,但它能让你在面对真实世界的复杂性时,依然保持信心。


如果你对密钥管理系统的设计理念感兴趣,可以阅读 为什么我构建了 Sealbox 了解更多背景故事。或者查看 Rust Builder Pattern 指南 了解 API 设计中的类似权衡思考。

Found this worth reading or have thoughts to share?