密钥权限隔离架构:全局密钥与租户专属密钥
这一章我们聊点实在的——密钥的隔离架构。
做过多租户系统的朋友应该都有体会:密钥管理这事儿,看着简单,真落地了全是坑。我记得第一次接触这个场景,是在一个SaaS平台上。客户要求每个租户的数据必须完全隔离,连加密密钥都不能共用。嗯,当时我就想,这不就是把密钥分分类,存到不同地方吗?结果真正动手才发现,远没那么简单。
说白了,密钥隔离的核心就两个问题:谁来用 和 放在哪。前者决定了密钥的归属,后者决定了密钥的安全边界。
全局密钥 vs 租户专属密钥
先说说最常见的两类密钥。
全局密钥,也叫系统密钥。整个平台共用一把。它通常用来加密一些非敏感数据,或者作为底层的基础密钥。比如,你可能会用它来加密配置文件里的数据库密码、第三方API的。
租户专属密钥,顾名思义,每个租户独享一把。这是数据隔离的核心手段。敏感数据,比如用户的个人资料、财务信息,都应该用租户自己的密钥来加密。
为什么要这样分?我经历过一个反面案例。有个项目初期为了省事,所有租户的数据全用一把全局密钥加密。后来有个大客户提出要独立部署,要求我们把他的数据导出来。结果你猜怎么着?数据是导出来了,但密钥没法跟着走——因为这是全局的,其他租户还在用。最后只能改代码、重新加密,折腾了整整两个迭代。你说这教训值不值?
所以,我现在的建议很明确:一开始就设计好密钥的归属。全局密钥只负责平台自身的元数据加密。租户数据,一律用租户专属密钥。
| 对比维度 | 全局密钥 | 租户专属密钥 |
|---|---|---|
| 使用范围 | 整个平台共享 | 单个租户独享 |
| 管理成本 | 低,一把搞定 | 高,需要按租户管理 |
| 安全隔离 | 弱,泄露即全局沦陷 | 强,一个租户出问题不影响其他 |
| 性能影响 | 快,无额外查找开销 | 略有损耗,需要根据租户ID定位密钥 |
| 典型场景 | 系统配置、非敏感元数据 | 用户财务数据、隐私信息 |
密钥空间隔离策略
密钥有了,怎么存?怎么找?这就涉及到密钥空间隔离。
核心思路:每个租户拥有独立的密钥命名空间,互相不可见、不可访问。
听起来像什么?其实就是一个巨大的哈希表,key 是租户ID,value 是该租户的所有密钥集合。这种做法我在好几个生产项目里用过,效果不错。
具体实现上,有两种常见策略:
- 物理隔离:每个租户独立的密钥存储实例。比如独立的数据库表、独立的Redis namespace。优点是隔离彻底,缺点是运维成本高。
- 逻辑隔离:共用一个存储,但通过前缀或标识来区分。比如密钥ID格式为
tenant_123:key_data。优点是管理简单,缺点是需要小心处理边缘情况。
我个人更倾向逻辑隔离+定期审计的组合。为什么呢?因为物理隔离听起来很酷,但真正维护几十上百个密钥存储实例的时候,你会发现监控、备份、恢复都成了噩梦。我曾经有段时间每天半夜被警报叫醒,就是因为某个租户的独立Redis实例内存打满了。
实战建议:先用逻辑隔离快速上线,后续再根据实际需要,对高安全级别的租户单独切到物理隔离。
下面这个图展示了整体架构,你可以直观感受一下密钥空间是怎么划分的:
隔离策略的代码落地
光说不练假把式。我们来看看代码里怎么实现。
假设你用的是 Redis 做密钥存储。最简单的逻辑隔离,可以这样:
# 密钥命名空间常量
NAMESPACE_GLOBAL = "global"
NAMESPACE_TENANT_PREFIX = "tenant"
# 构建租户专属密钥的Redis Key
def build_tenant_key(tenant_id: str, key_name: str) -> str:
return f"{NAMESPACE_TENANT_PREFIX}:{tenant_id}:{key_name}"
# 示例:获取租户A的加密密钥
tenant_a_enc_key = build_tenant_key("tenant_a", "encryption_key")
# 结果: tenant:tenant_a:encryption_key
# 全局密钥则不加租户标识
global_key = f"{NAMESPACE_GLOBAL}:root_key"
# 结果: global:root_key
注意:千万别在密钥ID里直接拼入明文租户名,容易导致路径遍历攻击。建议用租户的UUID或哈希值。
我曾经见过一个项目,密钥空间管理混乱到什么程度?有人把全局密钥和租户密钥放在同一个哈希表里,key格式还完全不统一。结果运维想清理一个离职租户的密钥时,误删了一把全局密钥。整个平台加密数据全部无法读取,回滚花了整整一天。你说这何必呢?
一种更安全的做法:密钥派生
你可能会问,每个租户都存一把独立的密钥,万一存储被脱库了呢?
这个问题问得好。真落到实战里,我们通常会再加一层保护——密钥派生。具体说,全局只存一把主密钥(Master Key)。每个租户的密钥,通过主密钥+租户特定信息(比如租户ID、盐值)派生出来。
import hashlib
import os
def derive_tenant_key(master_key: bytes, tenant_id: str, salt: bytes = None) -> bytes:
# 如果外部没传盐,自动生成一个
if salt is None:
salt = os.urandom(16)
# 使用HKDF或PBKDF2派生
derived = hashlib.pbkdf2_hmac(
'sha256',
master_key,
tenant_id.encode() + salt,
iterations=100000
)
return derived, salt
# 存储时,只需要保存盐值即可
# master_key 放在硬件安全模块或单独的密钥管理系统
master = b"this_is_a_32_byte_master_key_xxxx"
tenant_key, salt = derive_tenant_key(master, "tenant_a")
# tenant_key 用来加密租户数据
# salt 可以明文存储在数据库里
这种方案的好处很明显:即使数据库泄露,黑客拿到的也只是派生密钥或盐值,没有主密钥,依然无法破解其他租户的数据。
关键原则:主密钥永远不能落盘。放在内存、硬件安全模块(HSM)或云端密钥管理服务(KMS)里。
嗯,这里要注意一下。派生方案虽然安全,但性能上会有一定消耗。每次加解密都要走一遍派生计算。我建议在应用启动时或租户首次访问时,把派生出来的密钥缓存在本地内存里,设置一个合理的过期时间。这样既保证了安全,又不影响性能。