Secret 的安全风险:从明文到加密的必经之路

在讲服务网格集成 Secret 之前,我想先聊聊安全。嗯,这个话题绕不开。我自己在项目中踩过的坑,十有八九都跟 Secret 的存储方式有关。说白了,Secret 要是没管好,整个集群就像把家门钥匙挂在门外——你用起来方便,别人拿起来也方便。

这一节我们不搞花哨的,就理清楚四个核心风险点,以及我习惯怎么处理它们。你如果全部掌握,至少在 Secret 安全这块,能挡住 90% 的常见攻击。

1. 明文存储:最直接的漏洞

很多人把 Secret 当成加密工具,其实不是。Kubernetes Secret 默认只是 base64 编码,不是加密。编码和加密,这里我分得特别清楚——编码是给人看的,加密是给机器锁的。

我在项目中遇到过的真实案例:有一次,团队把数据库密码写进 YAML 文件里,直接提交到 Git。CI 跑了没问题,但三个月后有人发现 Git 历史里躺着一个 echo "cGFzc3dvcmQ=" | base64 -d…… 嗯,那周的改密会议,气氛挺沉重。

防止明文存储,我建议至少做到这几点:

  • 永远不要将 Secret 的原始值写入 YAML,即使你用了 base64。
  • 使用 External Secrets Operator 或 Sealed Secrets,让 YAML 中只保留加密后的密文。
  • CI/CD 流水线中集成 Secret 扫描(比如 truffleHog),确保不会意外泄露。

小技巧:我个人习惯在 Git 仓库根目录放一个 .gitattributes,把包含 secret 字样的文件标记为 diff=secret,并用 git diff --check 钩子禁止提交。这样能防住第一次粗心。

2. Base64 编码误解:你以为你加密了?

这可能是误解最大的地方。Base64 不是加密算法,它只是把二进制数据转成 ASCII 字符,方便在文本协议里传输。我见过太多新手直接 echo password | base64 就塞进 YAML,然后问「为什么我的 Secret 不安全?」

为什么会这样?因为 kubectl 创建 Secret 时,默认只做 base64 编码,且 etcd 如果不开启加密,就是存成 base64 后的明文字符串。任何人只要拿到 etcd 的快照或者有 etcd 的读权限,就能直接看。

方案 安全性 适用场景
纯 base64 ❌ 不安全 仅用于开发测试,千万别上生产
base64 + RBAC 只能防住集群内部分用户,防不住 etcd 管理员
etcd 加密 (AES-GCM) ✅ 推荐 生产环境标配
外部加密 + 服务网格集成 更严格的合规场景,比如 PCI-DSS

避坑指南:我曾经帮一家客户做安全审计,发现他们的 Secret 在 etcd 里是明文,但团队觉得「我们已经用了 RBAC,只能让指定 ServiceAccount 读取,所以没问题」。错!etcd 的备份文件一样可以读出全部 Secret。后来我们强制启用了 etcd 加密,并把备份文件也做了加密存储。

3. RBAC 权限控制:最小权限原则

RBAC 是 Kubernetes 控制谁做什么的核心机制,但在 Secret 上,很多人把它用成了「全有或全无」。我见过最夸张的配置是给了一个 namespace 所有 Pod 的 get 权限…… 嗯,等于没控制。

我个人习惯把 Secret 的访问权限控制到最小粒度的 ServiceAccount。步骤如下:

  1. 按业务拆分 ServiceAccount:每个微服务一个,绝不共用。
  2. 使用 Role 而不是 ClusterRole:除非 Secret 跨 namespace,否则永远用 Role。
  3. 只给 get 和 list:不需要 create、update、delete 权限给普通服务。
  4. 配合 Pod 安全策略:禁止 Pod 挂载宿主机的 Docker socket,防止绕开 RBAC。
# 一个典型的最小权限 Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: secret-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  # 只允许读取,不允许写入
  verbs: ["get", "list"]
---
# 绑定到 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: production
  name: secret-reader-binding
subjects:
- kind: ServiceAccount
  name: payment-service
  namespace: production
roleRef:
  kind: Role
  name: secret-reader
  apiGroup: rbac.authorization.k8s.io

我个人经验:曾经有个审计场景,我们需要知道「哪些 Pod 实际访问了 Secret」,默认 RBAC 日志是不记录 get 操作的。后来我启用了 Kubernetes Audit Logging 并过滤出 responseStatus.code=200 且 verb=get + resource=secrets 的条目,才真正看到谁读了什么。建议在生产环境必开。——时间不用太多,但能救命。

4. etcd 加密最佳实践:最后一道防线

etcd 是 Kubernetes 的整个状态中心,包括所有 Secret。如果 etcd 被拖走,前面的一切防护都白费。所以,etcd 加密不是可选,是必需。

Kubernetes 从 1.13 开始支持 etcd 加密,有两种主流方式:

  • AES-GCM:性能好,加密速度快,适合高吞吐环境。
  • 使用外部 KMS 插件:比如 AWS KMS、GCP Cloud KMS、Azure Key Vault、HashiCorp Vault。密钥管理更安全,但会增加延迟。

我的建议:如果你们公司有合规要求(比如 SOC2、PCI),直接用 KMS;如果没有,AES-GCM 也足够,但密钥文件本身要妥善保管。

配置示例(AES-GCM):

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: key1
          secret: ZXhhbXBsZV9rZXlfdmFsdWU=
    - identity: {}  # 回退到明文

然后需要在 kube-apiserver 启动参数中加上 --encryption-provider-config=/etc/kubernetes/encryption-config.yaml。记得滚动重启所有 apiserver。

另一个常见陷阱:etcd 加密只对新写入的数据生效。已经存在的 Secret 需要删除重建才会被加密。我在某个生产环境就是忘了这步,以为配了加密就万事大吉,结果旧 Secret 还是明文。——所以记得 kubectl delete secret <name> --ignore-not-found 然后重新创建。

核心逻辑梳理:Secret 安全的层次防御

下面这张图是我自己总结的四层防御模型。你细看就会发现,每一层都堵住一个攻击面。

Secret 安全的四层防御模型 第一层:源头管控 禁止明文写入 YAML · 使用 External Secrets / Sealed Secrets · CI扫描 第二层:存储加密 etcd 启用 AES-GCM / KMS · 备份文件加密 · 密钥独立管理 第三层:访问控制 最小权限 RBAC · 按 ServiceAccount 隔离 · Audit Logging 第四层:运行时隔离 Pod 安全策略 · 禁止挂载宿主 Docker · 服务网格 mTLS 传输加密 源头 存储 控制 运行时

你看,这四层是从左到右、从代码到运行时依次递进的。实际落地时,我建议按顺序检查:没有源头管控,后面再加密也没用;没有 etcd 加密,RBAC 再怎么精细也无济于事。你想想看,攻击者只要能拿到 etcd 快照,所有 ServiceAccount 的权限都不管用——因为数据本身已经暴露了。

这就是我们常说的“纵深防御”。每一层都是独立的安全屏障,任何一层被突破,后面还有保底。嗯,这也是我在生产环境中坚持的底线。