适用场景
密钥轮换指加密密钥在其生命周期内定期更换,并在更换后仍能解密历史数据。只要系统中存在需要长期保存的敏感字段(身份证号、银行卡号、会话令牌、API 密钥),密钥就应当具备轮换能力:员工离职、代码仓库泄露、合规审计都会要求「密钥可在不中断业务的前提下更换」。
直接对业务数据做加解密容易踩两个坑:一是把密钥硬编码在代码或配置文件中,一旦泄露等于全部数据裸奔;二是轮换时无法解密旧数据,导致只能停服迁移。信封加密与托管密钥服务(KMS)正是为解决这两个问题设计的。
前置条件
- 具备云厂商 KMS 权限(本文示例以兼容 AWS KMS API 的接口为准,阿里云 KMS 的密钥类型与权限模型可对应替换),或已部署 HashiCorp Vault 的 transit 引擎;
- Python 3.8 以上环境,安装 boto3 与 cryptography;
- 业务数据库具备可加列的变更权限,用于记录密钥版本;
- 已明确合规要求中的密钥轮换周期(常见为 90 天或 1 年)。
pip3 install boto3 cryptography
原理说明
信封加密(Envelope Encryption)把密钥分成两层:
- 主密钥(KEK / CMK):由 KMS 托管,永不离开 KMS 的硬件安全模块,应用只能请求它执行加密、解密与生成数据密钥;
- 数据密钥(DEK):由 KMS 现用现生成,用于实际加密业务数据。KMS 同时返回明文 DEK 与用主密钥加密后的密文 DEK。
标准流程是:应用拿到明文 DEK 后,用 AES-256-GCM 加密业务数据,输入随机 IV,得到密文与认证标签;随后立即在内存中丢弃明文 DEK,只把密文 DEK 与密文数据一起持久化。解密时先调用 KMS 把密文 DEK 还原为明文 DEK,再解密数据。
这套结构带来三个关键收益:主密钥不落地,泄露风险收敛到 KMS 的访问控制;轮换只发生在主密钥层,历史数据的密文 DEK 中已经内嵌了密钥版本,无需重新加密即可解密;每一次解密都会在 KMS 侧留痕,可被审计与告警。
操作步骤
1. 创建主密钥并开启自动轮换
KEY_ID=$(aws kms create-key --description "app-dek-kek" \
--query KeyMetadata.KeyId --output text)
aws kms create-alias --alias-name alias/app-kek --target-key-id "$KEY_ID"
# 开启年度自动轮换,KMS 会保留旧版本密钥供解密
aws kms enable-key-rotation --key-id "$KEY_ID" --rotation-period-in-days 365
# 验证轮换状态
aws kms get-key-rotation-status --key-id alias/app-kek
关键点:–rotation-period-in-days 365 表示自动轮换周期,轮换只影响后续新生成的密钥版本,历史版本仍可用于解密。alias/app-kek 让代码引用别名而非密钥 ID,未来换密钥无需改代码。
2. 实现信封加解密
import os, json, base64, boto3
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
kms = boto3.client("kms", region_name="cn-hangzhou")
KEY_ID = "alias/app-kek"
def encrypt(plaintext: bytes) -> str:
"""生成数据密钥 -> AES-256-GCM 加密 -> 连同密文密钥一起返回"""
dk = kms.generate_data_key(KeyId=KEY_ID, KeySpec="AES_256")
dek, blob = dk["Plaintext"], dk["CiphertextBlob"]
iv = os.urandom(12)
ct = AESGCM(dek).encrypt(iv, plaintext, None)
del dek # 尽快清除明文密钥引用
return json.dumps({
"v": 1,
"key": base64.b64encode(blob).decode(),
"iv": base64.b64encode(iv).decode(),
"ct": base64.b64encode(ct).decode(),
})
def decrypt(token: str) -> bytes:
o = json.loads(token)
dek = kms.decrypt(CiphertextBlob=base64.b64decode(o["key"]))["Plaintext"]
return AESGCM(dek).decrypt(base64.b64decode(o["iv"]),
base64.b64decode(o["ct"]), None)
字段 v 用于标记密文格式版本,方便未来升级算法时做灰度解析;iv 必须是每次加密重新生成的随机值,绝不可复用,否则 GCM 模式下会直接丧失机密性。
3. 数据密钥的缓存与内存保护
KMS 调用有延迟与配额限制,高频加解密需要在内存中缓存明文 DEK。缓存必须落在 tmpfs 上并限制权限,重启即清空,避免明文密钥写到磁盘:
mkdir -p /dev/shm/dek_cache
chmod 700 /dev/shm/dek_cache
export DEK_CACHE=/dev/shm/dek_cache
# 关键约束:明文 DEK 不写日志、不进异常堆栈、不做序列化输出
grep -rn "Plaintext" /var/log/app/*.log && echo "存在密钥泄漏风险"
4. 配置最小权限策略
只授权业务实际需要的三个动作,并限定密钥别名与调用来源:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowEnvelopeOps",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:cn-hangzhou:1234567890:key/xxxxxxxx-xxxx-xxxx",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.cn-hangzhou.amazonaws.com"
}
}
}
]
}
切勿授予 kms:*,尤其要显式排除 kms:ScheduleKeyDeletion 与 kms:DisableKey。这两个动作一旦被误用,会导致全部历史数据永久无法解密。
5. 轮换与历史数据重加密
主密钥自动轮换后,新写入的数据自动使用新版本密钥,旧数据仍可正常读取。若合规要求对存量数据也做重加密,用批量任务逐条解密再加密,并在表中记录密钥版本:
def reencrypt_batch(conn, table: str, column: str, batch: int = 500) -> int:
total = 0
while True:
rows = conn.execute(
"SELECT id, {col} FROM {tbl} WHERE key_v < %s LIMIT %s".format(
col=column, tbl=table), (CURRENT_KEY_VERSION, batch)
).fetchall()
if not rows:
break
for rid, token in rows:
new_token = encrypt(decrypt(token)) # 解旧 -> 加新
conn.execute(
"UPDATE {tbl} SET {col}=%s, key_v=%s WHERE id=%s".format(
col=column, tbl=table),
(new_token, CURRENT_KEY_VERSION, rid))
conn.commit()
total += len(rows)
print("已重加密 %d 条" % total)
return total
务必按主键区间分批提交并记录水位,任务中断后可续跑;不要在单个事务里完成全表重加密,否则会长时间持锁并放大回滚成本。
6. 审计告警
KMS 的每次密钥操作都会写入审计日志。需要重点监控两类异常:非业务时段的 Decrypt 调用,以及单个身份短时间内的高频解密。同时为密钥配置过期告警,避免自动轮换被意外关闭而无人发现。
7. 无 KMS 环境的替代方案
自建环境可用 HashiCorp Vault 的 transit 引擎获得等价的「密钥不落地」能力:
vault secrets enable transit
vault write -f transit/keys/app-key type=aes256-gcm96
vault write transit/encrypt/app-key plaintext=$(base64 <<< "sensitive")
# 轮换密钥版本
vault write -f transit/keys/app-key/rotate
# 不暴露明文,直接把旧密文迁移到新密钥版本
vault write transit/rewrap/app-key ciphertext="vault:v1:xxxxx"
transit/rewrap 是信封加密之外值得记住的能力:它能在不把明文暴露给应用的前提下完成密文版本迁移。
配置验证
- 往返测试:执行 decrypt(encrypt(b”hello kms”)),结果应与原文完全一致;
- 轮换状态:aws kms get-key-rotation-status –key-id alias/app-kek 返回 KeyRotationEnabled: true,并核对下次轮换日期;
- 历史数据可解:记录一条轮换前写入的密文,在轮换执行后重新解密,应成功返回原文,验证旧密钥版本被保留;
- 权限边界:用无 Decrypt 权限的凭据调用解密,应返回 AccessDeniedException;用业务凭据尝试 kms:ScheduleKeyDeletion,同样应被拒绝;
- 密钥不落地:对应用日志与堆转储文件全文检索密钥相关字段名,确认无明文密钥输出。
aws kms decrypt --ciphertext-blob fileb://blob.bin
# 预期(无权限时): AccessDeniedException: not authorized to perform: kms:Decrypt
常见问题
FAQ 1:主密钥轮换后,历史数据需要全部重新加密吗?
不需要。密文数据密钥中已经内嵌了生成它的主密钥版本信息,KMS 在解密时会自动匹配对应版本,因此旧数据无需任何改动即可继续读取。只有两种情况下才需要主动重加密:一是合规条款明确要求存量数据也必须使用新密钥;二是从 AES-128 升级到 AES-256 这类算法层面的变更,此时需要走步骤 5 的批量重加密流程。
FAQ 2:既然有 KMS,为什么不能把明文数据密钥缓存到磁盘?
信封加密的安全模型建立在「主密钥永不离开 KMS」之上,明文数据密钥是这套模型里唯一暴露在应用内存中的环节。一旦被写入磁盘、日志或交换分区,攻击者只要拿到该文件,就等于绕过了 KMS 的全部访问控制与审计能力——KMS 反而失去了意义。所以明文 DEK 的缓存必须限制在内存或 tmpfs 中,并缩短存活时间。
FAQ 3:轮换期间业务会受影响吗?
不会,前提是不手动禁用旧密钥版本。轮换是「新增一个版本」,不是「替换所有版本」;新数据用新版本加密,历史数据继续用旧版本解密。真正会造成业务中断的操作是禁用或计划删除密钥,这两个动作都应纳入高危权限管控。压力主要来自重加密任务本身,建议放在业务低峰期并限制并发,避免数据库写入被打满。
总结
密钥轮换的落地要点有四条:用信封加密把主密钥留在 KMS、把数据密钥随数据一起存储;用别名而非密钥 ID 引用主密钥,并开启自动轮换;权限只授予 Encrypt、Decrypt、GenerateDataKey 三个动作,显式拒绝禁用与删除;存量数据重加密必须分批提交并保留水位。做到这四点,密钥更换就从一次高风险变更,退化为一次普通的配置调整。