国密改造 Kubernetes Secret 存储:SM4 加密的完整实践指南
前言
Kubernetes 的 Secret 资源默认以 Base64 编码存储在 etcd 中。Base64 不是加密——任何人拿到 etcd 数据都可以瞬间还原。在生产环境中,这导致两个严重问题:
- 合规风险:等保 2.0 和密评要求敏感数据加密存储,Base64 编码完全不满足要求
- 安全风险:etcd 备份、快照、灾难恢复场景下,所有 Secret 等同于明文
方案选型
Kubernetes 提供了多种 Secret 加密方案,各有适用场景:
| 方案 | 加密范围 | 侵入程度 | 适用场景 |
|---|---|---|---|
| EncryptionConfiguration(etcd 层) | 全部 Secret | 低 | 全局透明加密 |
| External Secrets Operator | 单个 Secret | 中 | 对接外部 KMS |
| CSI Secret Store Driver | 单个 Secret | 高 | 精细化权限控制 |
现实约束:Kubernetes 原生不支持 SM4
在深入实施方案之前,必须明确一个关键事实:上游 Kubernetes 的 EncryptionConfiguration 原生仅支持 aescbc、aesgcm、secretbox、envelope、kms、noop、identity 等加密提供者,不包含 SM4 算法。
这意味着要实现 SM4 加密,有以下三条路径:
| 路径 | 难度 | 维护成本 | 适用场景 |
|---|---|---|---|
| A. 使用 KMS 加密提供者对接国密 KMS | 中 | 低 | 生产推荐方案 |
| B. 定制编译支持 SM4 的 kube-apiserver | 高 | 高 | 有定制能力的团队 |
| C. 在应用层使用 SM4 加密后存入 Secret | 低 | 中 | 快速验证/小规模部署 |
路径 A:EncryptionConfiguration + KMS 加密提供者
架构原理
Kubernetes 的 KMS 加密提供者允许通过 gRPC 协议调用外部 KMS 服务进行数据加解密。这是实现 SM4 加密最推荐的方案:
kube-apiserver → KMS Plugin (gRPC) → 国密 KMS 服务 (SM4)配置示例
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfig
resources:
- resources:
- secrets
providers:
- kms:
name: sm4-kms-plugin
endpoint: unix:///var/run/kms-plugin/plugin.sock
timeout: 3s
cacheSize: 1000
- identity: {} # 兜底:不加密KMS Plugin 实现(Python 示例)
"""
SM4 KMS Plugin - Kubernetes KMS gRPC 服务实现
依赖: pip install grpcio grpcio-tools gmssl
"""
import os
import base64
from gmssl import sm4, func
def sm4_pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
"""SM4 PKCS#7 填充"""
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len] * pad_len)
def sm4_pkcs7_unpad(data: bytes) -> bytes:
"""SM4 PKCS#7 去填充"""
pad_len = data[-1]
if pad_len < 1 or pad_len > 16:
raise ValueError("Invalid PKCS#7 padding")
return data[:-pad_len]
def sm4_ecb_encrypt_block(key: bytes, block: bytes) -> bytes:
"""SM4 ECB 单块加密(使用 gmssl 库的 one_round 接口)"""
crypt = sm4.CryptSM4()
crypt.set_key(key, sm4.SM4_ENCRYPT)
return bytes(crypt.one_round(crypt.sk, list(block)))
def sm4_ecb_decrypt_block(key: bytes, block: bytes) -> bytes:
"""SM4 ECB 单块解密"""
crypt = sm4.CryptSM4()
crypt.set_key(key, sm4.SM4_DECRYPT)
return bytes(crypt.one_round(crypt.sk, list(block)))
def sm4_cbc_encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
"""SM4 CBC 模式加密"""
padded = sm4_pkcs7_pad(plaintext)
prev = iv
ciphertext = b""
for i in range(0, len(padded), 16):
block = padded[i:i + 16]
xored = bytes(a ^ b for a, b in zip(block, prev))
encrypted = sm4_ecb_encrypt_block(key, xored)
ciphertext += encrypted
prev = encrypted
return ciphertext
def sm4_cbc_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
"""SM4 CBC 模式解密"""
prev = iv
plaintext = b""
for i in range(0, len(ciphertext), 16):
block = ciphertext[i:i + 16]
decrypted = sm4_ecb_decrypt_block(key, block)
plain = bytes(a ^ b for a, b in zip(decrypted, prev))
plaintext += plain
prev = block
return sm4_pkcs7_unpad(plaintext)
def generate_sm4_key() -> str:
"""生成随机 SM4 密钥(16 字节 = 128 位),base64 编码"""
key = os.urandom(16)
return base64.b64encode(key).decode("utf-8")
def generate_key_identity() -> str:
"""生成密钥标识符(用于密钥轮换)"""
return base64.b64encode(os.urandom(16)).decode("utf-8")
if __name__ == "__main__":
# 生成密钥
key_b64 = generate_sm4_key()
key = base64.b64decode(key_b64)
identity = generate_key_identity()
print(f"SM4 Key (base64): {key_b64}")
print(f"Key Identity: {identity}")
print(f"Key length: {len(key)} bytes = {len(key) * 8} bits")
# 加密测试
iv = os.urandom(16)
secret_data = b'{"password": "secret123", "token": "abc"}'
encrypted = sm4_cbc_encrypt(key, iv, secret_data)
decrypted = sm4_cbc_decrypt(key, iv, encrypted)
print(f"\n加密测试:")
print(f" 原始: {secret_data}")
print(f" 密文 (base64): {base64.b64encode(encrypted).decode()}")
print(f" 解密: {decrypted}")
print(f" 验证: {secret_data == decrypted}")
print(f"\n请妥善保存以上密钥,丢失后将无法解密已加密数据")密钥生成与管理
# 使用 Python 脚本生成 SM4 密钥
python3 kms_plugin.py > /tmp/sm4-keys.env
# 将密钥安全存储到 KMS 服务中
# 不要将密钥硬编码在配置文件中
cat /tmp/sm4-keys.env
# SM4 Key (base64): aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789ABCDEF=
# Key Identity: XYZ123abc456DEF789ghiJKL012mnoPQR=路径 C:应用层 SM4 加密
对于不想引入 KMS 插件的小型集群,可以在应用层直接加密 Secret 数据:
SM4 加密工具函数
"""
SM4 加密工具 - 用于应用层加密 Secret 数据
依赖: pip install gmssl
"""
import os
import base64
from gmssl import sm4
def sm4_pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len] * pad_len)
def sm4_pkcs7_unpad(data: bytes) -> bytes:
pad_len = data[-1]
if pad_len < 1 or pad_len > 16:
raise ValueError("Invalid PKCS#7 padding")
return data[:-pad_len]
def sm4_encrypt(key_b64: str, plaintext: str, iv_b64: str = None) -> dict:
"""
SM4-CBC 加密
Args:
key_b64: base64 编码的 SM4 密钥(16 字节)
plaintext: 待加密的明文字符串
iv_b64: 可选的 base64 编码 IV(16 字节),不提供则随机生成
Returns:
{
"iv": base64 编码的 IV,
"ciphertext": base64 编码的密文
}
"""
key = base64.b64decode(key_b64)
if len(key) != 16:
raise ValueError(f"SM4 密钥长度必须为 16 字节,实际为 {len(key)} 字节")
iv = base64.b64decode(iv_b64) if iv_b64 else os.urandom(16)
plaintext_bytes = plaintext.encode("utf-8")
# CBC 模式加密
prev = iv
ciphertext = b""
padded = sm4_pkcs7_pad(plaintext_bytes)
for i in range(0, len(padded), 16):
block = padded[i:i + 16]
xored = bytes(a ^ b for a, b in zip(block, prev))
crypt = sm4.CryptSM4()
crypt.set_key(key, sm4.SM4_ENCRYPT)
enc_block = bytes(crypt.one_round(crypt.sk, list(xored)))
ciphertext += enc_block
prev = enc_block
return {
"iv": base64.b64encode(iv).decode("utf-8"),
"ciphertext": base64.b64encode(ciphertext).decode("utf-8")
}
def sm4_decrypt(key_b64: str, iv_b64: str, ciphertext_b64: str) -> str:
"""SM4-CBC 解密"""
key = base64.b64decode(key_b64)
iv = base64.b64decode(iv_b64)
ciphertext = base64.b64decode(ciphertext_b64)
prev = iv
plaintext = b""
for i in range(0, len(ciphertext), 16):
block = ciphertext[i:i + 16]
crypt = sm4.CryptSM4()
crypt.set_key(key, sm4.SM4_DECRYPT)
dec_block = bytes(crypt.one_round(crypt.sk, list(block)))
plain = bytes(a ^ b for a, b in zip(dec_block, prev))
plaintext += plain
prev = block
return sm4_pkcs7_unpad(plaintext).decode("utf-8")
# 使用示例
if __name__ == "__main__":
# 生成密钥(生产环境应从 KMS 获取)
key = base64.b64encode(os.urandom(16)).decode("utf-8")
print(f"SM4 Key: {key}")
secret = '{"username": "admin", "password": "s3cret!"}'
result = sm4_encrypt(key, secret)
decrypted = sm4_decrypt(key, result["iv"], result["ciphertext"])
print(f"加密: {result}")
print(f"解密: {decrypted}")
assert secret == decrypted, "加解密验证失败"
print("加解密验证通过")创建加密 Secret
# 1. 加密 Secret 数据
SM4_KEY=$(python3 -c "import os,base64; print(base64.b64encode(os.urandom(16)).decode())")
IV=$(python3 -c "import os,base64; print(base64.b64encode(os.urandom(16)).decode())")
# 2. 将加密数据存储为 Secret
kubectl create secret generic app-credentials \
--from-literal=sm4-ciphertext="<加密后的密文>" \
--from-literal=sm4-iv="$IV" \
--from-literal=sm4-key-ref="kms://my-kms/key/sm4-app-key" \
--dry-run=client -o yaml | kubectl apply -f -密钥轮换
Kubernetes KMS 加密提供者原生支持密钥轮换。通过在 EncryptionConfig 中配置多个密钥标识符:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfig
resources:
- resources:
- secrets
providers:
- kms:
name: sm4-kms-v2
keyId: sm4-key-2026-07
endpoint: unix:///var/run/kms-plugin/plugin.sock
- kms:
name: sm4-kms-v1
keyId: sm4-key-2026-01
endpoint: unix:///var/run/kms-plugin/plugin.sock
- identity: {}轮换流程:
- 在 KMS 中生成新 SM4 密钥(密钥版本 v2)
- 将新密钥配置添加到 EncryptionConfig 第一位
- 重启 kube-apiserver
- 触发全量 Secret 重新加密:
# 对所有 Secret 执行一次写回,触发新密钥加密
kubectl get secrets --all-namespaces -o json | \
kubectl apply -f -注意:此操作会将所有 Secret 从 etcd 读出、用新密钥加密、再写回。对于大规模集群,建议在维护窗口执行。
踩坑实录
坑 1:SM4 密钥长度必须是 16 字节
现象:加密插件启动时报错 invalid key length。
原因:SM4 的密钥长度固定为 128 位(16 字节),而 AES-256 的密钥长度为 32 字节。直接将 AES 密钥用于 SM4 会失败。
解决:
import os, base64
# 正确:16 字节 = 128 位
key = os.urandom(16)
print(f"SM4 key: {base64.b64encode(key).decode()}")
print(f"Key length: {len(key)} bytes = {len(key)*8} bits")坑 2:gmssl 库的 CryptSM4 类行为
现象:调用 sm4.CryptSM4().crypt_ecb() 时,16 字节输入返回 32 字节输出。
原因:gmssl 库的 CryptSM4.crypt_ecb() 方法在加密模式下会自动添加 PKCS#7 填充。对于恰好 16 字节的输入,它会填充到 32 字节。
解决:使用 one_round() 接口手动控制块加密,或使用 crypt_cbc() 接口处理 CBC 模式:
from gmssl import sm4
key = bytes.fromhex("0123456789abcdef0123456789abcdef")
c = sm4.CryptSM4()
c.set_key(key, sm4.SM4_ENCRYPT)
# 推荐方式:使用 one_round 手动控制填充
block = b"0123456789abcdef" # 16 字节
output = bytes(c.one_round(c.sk, list(block)))
print(f"ECB output length: {len(output)} bytes") # 正好 16 字节坑 3:etcd 加密后体积增长
现象:加密后的 Secret 体积增大约 30-50%。
原因:SM4-CBC 模式需要 16 字节 IV + PKCS#7 填充(最多 16 字节)。
解决:
- 监控 etcd 磁盘使用率
- 调整
--quota-backend-bytes参数 - 定期清理过期 Secret
坑 4:备份恢复中的密钥管理
现象:etcd 快照恢复后,Secret 无法解密。
原因:EncryptionConfig 中的 KMS 密钥标识符与 etcd 快照不绑定,但密钥本身需要可访问。
解决:
- 将 KMS 服务与 etcd 分开备份
- 密钥轮换时保留旧密钥至少一个轮换周期
- 制定明确的密钥灾难恢复预案
合规要点
根据 GM/T 0054-2018《信息系统密码应用基本要求》和 GB/T 39786-2021《信息安全技术 信息系统密码应用基本要求》:
| 要求 | 对应措施 |
|---|---|
| 敏感数据加密存储 | etcd 层 SM4 加密 Secret |
| 密钥安全管理 | KMS 托管 SM4 密钥,密钥轮换机制 |
| 密码算法合规 | 使用 SM4 国密算法,不使用非合规替代 |
| 审计日志 | 记录所有 Secret 的加密/解密操作 |
总结
本文介绍了 Kubernetes Secret 的 SM4 国密加密两条实用路径:
- KMS 插件方案:通过 Kubernetes KMS 加密提供者对接国密 KMS 服务,是生产环境的推荐方案
- 应用层加密方案:在应用代码中使用 SM4 加密后存入 Secret,适合小规模部署
- Kubernetes 原生 EncryptionConfiguration 不支持 SM4,需要通过 KMS 插件或应用层实现
- SM4 密钥长度严格为 16 字节(128 位),不同于 AES-256 的 32 字节
- 密钥轮换支持多版本密钥配置,需在维护窗口执行全量重新加密
- 备份恢复时需确保 KMS 密钥可访问
合规检查清单
- [ ] Secret 在 etcd 中以 SM4 加密存储,非 Base64 明文
- [ ] SM4 密钥长度验证为 16 字节
- [ ] 密钥轮换流程文档化并定期演练
- [ ] KMS 服务有独立的备份恢复预案
- [ ] 审计日志记录所有 Secret 的加密/解密操作
- [ ] 使用 GM/T 0054-2018 和 GB/T 39786-2021 标准进行合规评估
参考来源
- Kubernetes EncryptionConfiguration: https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/
- GM/T 0054-2018《信息系统密码应用基本要求》
- GB/T 39786-2021《信息安全技术 信息系统密码应用基本要求》
- KMS Plugin API (k8s.io/kms): https://github.com/kubernetes/kms
- gmssl Python 库: https://github.com/duanhongyi/gmssl
- Tongsuo 国密 OpenSSL: https://github.com/Tongsuo-Project/Tongsuo