返回首页

6.4:Secret 安全管理

很多团队一提到 Kubernetes Secret,就会下意识觉得“已经安全了”。但这其实是最常见的误区之一。Secret 只是 Kubernetes 里的敏感数据对象,不等于完整的加密系统,更不等于密钥管理平台。 如果一开始就把边界想错,后面的设计往往会一路偏掉::cite[162]。

本章你将学会

  • 为什么说 Secret 不是加密系统,先要理解它的边界
  • Secret 在存储、传输、访问各环节的风险点在哪里
  • 如何结合 Encryption at Rest、KMS、External Secrets 等方案增强安全性
  • 如何做敏感信息轮换、审计与日常治理

1. 先摆正认知:Secret 不是“自动安全”

Kubernetes Secret 的主要价值,是把敏感信息从镜像、代码、Pod 规范里拆出来,单独作为对象管理。这样做当然比“把密码直接写到 Deployment 里”要好,但它解决的只是组织和引用方式的一部分问题,不是从根上解决所有机密管理问题::cite[162]。

官方文档明确提醒:默认情况下,Secret 会以未加密的形式存储在 API Server 背后的 etcd 中;拥有 API 访问权限的人可以读取或修改 Secret,而能够在某个命名空间中创建 Pod 的主体,也往往能够通过挂载方式间接读取该命名空间里的 Secret::cite[162]。

换句话说,Secret 的边界至少包括这几层:

问题 Secret 能否单独解决
不把密码写进镜像和代码 可以
对接 Kubernetes 工作负载引用 可以
etcd 中默认强加密 不可以,需额外配置
外部密钥托管 不可以,需借助 KMS / 外部密钥系统
自动轮换所有凭据 不可以,需额外流程或控制器
全链路审计和泄露响应 不可以,需平台治理配合

所以更准确的说法应该是:Secret 是机密管理体系中的一个对象层,不是完整体系本身

2. Secret 的风险面:存储、传输、访问都可能出问题

2.1 存储风险

Secret 默认保存在 etcd。如果你没有启用 Encryption at Rest,那么这些数据在底层存储里是明文可恢复的::cite[162][409]。

另外,Kubernetes 的 base64 编码只是编码,不是加密。下面这个清单:

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
  namespace: demo
type: Opaque
data:
  username: YWRtaW4=
  password: c3VwZXItc2VjcmV0

password 看起来像“被处理过了”,其实只要 base64 -d 就能还原。不要把“看不懂”误以为“安全”。

2.2 传输风险

Secret 在 API 调用过程中依赖 TLS 保护,但你仍然要关注:

  • 是否通过不安全日志打印了 Secret 内容
  • 是否在 CI/CD 输出中暴露了 kubectl apply -f 的原始内容
  • 是否被开发脚本临时写入本地文件、Shell 历史或调试目录

2.3 访问风险

Secret 的高风险点不只是 getlistwatch 同样危险。官方最佳实践明确强调:授予对 Secret 的 listwatch 权限,等价于可以看到 Secret 内容,而不仅仅是对象名称::cite[512]。

此外,能够创建 Pod 的身份也很敏感,因为它可能通过挂载 Secret 的方式间接读取敏感数据::cite[162][512]。

3. 一个最基础但相对规范的 Secret 使用示例

3.1 完整 YAML

apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: v1
kind: Secret
metadata:
  name: app-db-credentials
  namespace: demo
type: Opaque
stringData:
  username: app_user
  password: S3cure-P@ssw0rd
---
apiVersion: v1
kind: Pod
metadata:
  name: app-pod
  namespace: demo
spec:
  containers:
    - name: app
      image: nginx:1.27
      volumeMounts:
        - name: db-creds
          mountPath: /etc/secrets/db
          readOnly: true
  volumes:
    - name: db-creds
      secret:
        secretName: app-db-credentials
        defaultMode: 0400

3.2 为什么推荐用卷挂载而不是环境变量优先

Secret 可以通过环境变量或卷挂载给容器,但从控制暴露面的角度看,卷挂载通常比环境变量更稳妥一些:

  • 环境变量更容易被误打印
  • 某些诊断工具会直接显示环境变量
  • 变更后的 Secret 通常更适合通过文件方式被进程重新读取

当然,这不是绝对规则,但在大多数场景下,优先文件挂载、减少环境变量暴露会更容易做安全治理::cite[512]。

4. 先把底线补上:Encryption at Rest

Kubernetes 官方给出的第一条 Secret 安全建议,就是启用 Encryption at Rest。也就是让 API Server 在把 Secret 持久化到 etcd 前,先经过加密提供器处理::cite[162][512]。

4.1 EncryptionConfiguration 示例(aescbc)

下面是一个最基础的 EncryptionConfiguration 示例:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <BASE64_32_BYTE_KEY>
      - identity: {}

几点注意:

  • identity 代表不加密,通常放在后面作为兼容回退
  • 新写入的 Secret 会使用排在前面的 provider
  • 已经写入的数据不会自动重加密,需要后续做 rewrite / migration::cite[409]

4.2 可选提供器怎么选

Kubernetes 文档列出了多种 provider,比如 identityaescbcsecretboxkms。其中:

Provider 说明
identity 不加密,仅用于兼容或回退
aescbc 常见的本地静态密钥方案
secretbox 另一种本地加密方案
kms 把数据密钥保护交给外部 KMS

如果你只是先把 etcd 明文问题补掉,aescbc 是入门最快的方案;如果你希望密钥托管、轮换、审计都更专业,应该优先考虑 KMS::cite[409][410]。

5. 更进一步:在 v1.32 中优先考虑 KMS v2

Kubernetes 官方关于 KMS Provider 的说明里已经很明确:如果条件允许,应优先使用 KMS v2,因为 KMS v1 从 v1.28 起已被标记为 deprecated,并且从 v1.29 起默认关闭;KMS v2 在性能和实现上都更适合较新的 Kubernetes 集群::cite[410]。

这意味着在 Kubernetes v1.32 里,你做新集群 Secret 加密设计时,默认思路应该是:

能用 KMS v2,就不要再从 KMS v1 起步。

5.1 KMS v2 示例片段

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:
          apiVersion: v2
          name: my-kms
          endpoint: unix:///var/run/kmsplugin/socket.sock
          cachesize: 1000
      - identity: {}

这里的重点不是把 YAML 背下来,而是理解:

  • kube-apiserver 不直接保管最终主密钥
  • 它通过 KMS 插件与外部密钥管理服务协作
  • 这样你就可以把主密钥轮换、访问控制、审计能力放到更成熟的密钥系统中去::cite[410]

6. External Secrets:把“真实秘密源”放在集群外

如果团队已经在云厂商 Secret Manager、Vault、企业密钥平台中维护密钥,最常见的做法不是把它们手工复制进 Kubernetes,而是使用 External Secrets Operator(ESO) 这类控制器,让 Kubernetes Secret 变成“同步结果”,而不是“唯一真相”::cite[483]。

ESO 的基本思路是:

  1. 定义 SecretStoreClusterSecretStore,告诉控制器从哪里取值
  2. 定义 ExternalSecret,声明要同步哪些外部密钥
  3. 控制器拉取外部 API 数据,并生成 / 更新 Kubernetes Secret::cite[483]

6.1 ExternalSecret 示例

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: team-vault
  namespace: demo
spec:
  provider:
    vault:
      server: https://vault.example.com
      path: kv
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes
          role: demo-app
          serviceAccountRef:
            name: app-sa
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: app-db-secret
  namespace: demo
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: team-vault
    kind: SecretStore
  target:
    name: app-db-secret
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: prod/app/db
        property: username
    - secretKey: password
      remoteRef:
        key: prod/app/db
        property: password

6.2 这种模式的好处

  • 敏感源头仍在外部密钥系统
  • Kubernetes 只保留运行时所需副本
  • 支持周期性同步与集中轮换
  • 凭据治理不再分散到一堆 Helm values、CI 变量和手工命令里::cite[483]

7. Secret 访问控制:真正高风险的是谁能读、谁能挂

官方 Secret 最佳实践强调两件很重要的事:

  1. 对 Secret 做最小权限 RBAC
  2. 对“能创建 Pod 的身份”同样保持警惕::cite[512]

7.1 一个合理的 Secret 只读角色

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-secret-reader
  namespace: demo
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["app-db-credentials"]
    verbs: ["get"]

这个角色比下面这种写法安全得多:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: dangerous-secret-reader
  namespace: demo
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list", "watch"]

后者的问题是:

  • 范围太大
  • 可枚举命名空间里所有 Secret
  • 易被调试脚本、运维脚本批量导出

8. 敏感信息轮换怎么做才像“生产方案”

很多团队第一次做 Secret 管理,只考虑“能不能用”,不考虑“怎么换”。但真正进入生产后,轮换能力比创建能力更重要。

8.1 建议把 Secret 分成两类

类型 示例 轮换方式
平台内部 Secret 内部 Basic Auth、Webhook Token 由平台统一定期轮换
外部依赖凭据 数据库密码、云访问令牌、第三方 API Key 优先由上游系统轮换,再同步入集群

8.2 一个简单轮换流程

  1. 在外部系统或密码生成器中生成新值
  2. 更新 Secret 或更新外部秘密源
  3. 让应用通过 reload / restart 重新加载
  4. 验证新旧凭据切换成功
  5. 回收旧凭据
  6. 在审计系统中记录轮换时间、责任人、影响范围

8.3 支持热更新,不要只靠重启想象

并不是所有应用都会自动重新读取挂载文件或环境变量。你要提前确认:

  • 应用是否支持 SIGHUP / reload
  • 是否需要 sidecar 监控文件变化
  • 是否必须重启 Pod 才能生效

否则“Secret 已更新”不等于“业务已切换”。

9. 审计实践:没有审计,Secret 管理就不完整

Secret 治理除了加密和轮换,还需要审计能力。建议至少关注下面几类事件:

  • 谁读取了 Secret
  • 谁修改了 Secret
  • 谁在某 namespace 里获得了 secrets 的 RBAC 权限
  • 谁拥有创建 Pod 的能力
  • 哪些 Pod 正在挂载哪些 Secret

结合 Kubernetes Audit、GitOps 代码审查、RBAC 审批流,你至少能回答这几个关键问题:

  • 某个 Secret 最近一次是谁改的?
  • 某个命名空间里谁有读取 Secret 的能力?
  • 某个事故窗口内有哪些 Pod 可能暴露了敏感信息?

10. 常见误区

10.1 误区一:base64 就算加密

不是。base64 只是编码。

10.2 误区二:给运维一个 Secret 只读权限问题不大

如果这个“只读”里包含 list/watch,问题就已经不小了::cite[512]。

10.3 误区三:开启 Encryption at Rest 就万事大吉

它只能解决 etcd 持久化层的一部分风险,不能替代访问控制、外部密钥托管、凭据轮换和审计::cite[409][410]。

10.4 误区四:Kubernetes 里保存了 Secret,就不用外部密钥系统了

如果你的密钥生命周期、审批、跨系统复用、强审计要求都比较高,外部密钥系统通常仍然是更合适的“源头真相”。Kubernetes Secret 更适合做运行时分发层::cite[483]。

11. 一套适合 v1.32 的 Secret 安全基线

如果你的集群是 Kubernetes v1.32,我建议默认采用下面这套基线:

  • 必须启用 Secret 的 Encryption at Rest
  • 新设计优先 KMS v2,不从 KMS v1 起步::cite[410]
  • 所有业务默认禁止 list/watch secrets
  • 能创建 Pod 的权限与 Secret 权限一起做审查
  • 优先用文件挂载而不是环境变量暴露敏感值
  • 高价值密钥优先放在外部密钥系统,通过 External Secrets / CSI 等机制下发::cite[483]
  • 把轮换、审计、失效回收作为 Secret 生命周期的一部分,而不是事后补丁

12. 本章小结

这一章最核心的结论可以浓缩成一句话:

Secret 很重要,但 Secret 不是终点,它只是机密管理体系里的一个运行时对象层。

你需要记住:

  • Secret 默认并不会自动解决 etcd 明文存储问题::cite[162]
  • get/list/watch secrets 都是高风险权限::cite[512]
  • Kubernetes v1.32 场景下,Encryption at Rest 是底线,KMS v2 是更推荐的增强方案::cite[409][410]
  • 如果你希望把“秘密源头”放在集群外,External Secrets 是非常常见的工程化方案::cite[483]
  • 真正成熟的 Secret 管理,一定包含:最小权限、存储加密、外部托管、轮换机制、审计能力

下一章我们进入 Admission Controller,看看 Kubernetes 是如何在请求进入集群前统一执行安全与规范策略的。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

6.1:RBAC 权限体系

下一篇

9.4 节点异常处理