第 12 章 配置与密钥:ConfigMap / Secret 与它们的坑

墨:老哥,这章讲两块你天天用、但大概率”用得不对”的东西——ConfigMap 和 Secret。先问个扎心的:你的数据库密码,此刻在 K8s 里是明文还是加密? 你八成以为 Secret 就是加密的。今天先把这个最危险的误解拆了。


12.1 为什么配置不能写死在镜像里

墨:你打个镜像,把 nginx.conf 和数据库地址都烤进去,行不行? 行——但代价大:

  • 镜像应该是不可变的(immutable),一份镜像到处跑都一样(第 2.4)。配置写死,意味着”改个开关要重新打镜像、重新发布”——部署频率被配置频率绑架;
  • 同一份代码,dev/test/prod 配置不同,写死就得打三份镜像,镜像数爆炸;
  • 改配置要重新走 CI/CD,慢,且违反”配置和代码分离”的基本工程纪律。

所以 K8s 把配置从镜像里抽出来,用 ConfigMap 单独存、单独挂。Pod 引用它,镜像保持纯净。“一份镜像 + 不同环境的 ConfigMap”= 同样的代码跑不同环境。这正是十二因素(12-factor)里”配置存环境”的原则。

墨:那配置和镜像,边界画在哪? 经验线:代码逻辑、依赖库、静态资源→镜像;环境变量、开关、地址、模板→ConfigMap;密码/证书/token→Secret。 变的东西往外抽,不变的东西烤进去。


12.2 ConfigMap:配置的”声明式抽屉”

ConfigMap(CM)是个简单的键值对象,可存:

  • data 里的键值对(如 log_level: info);
  • 或整段文件内容(如 nginx.conf: | ...)。

Pod 用两种方式消费 CM:

  1. 环境变量:envFrom / valueFrom.configMapKeyRef——把 CM 的键变成容器环境变量;
  2. 卷挂载:把 CM 当 volume 挂进容器,键变文件,值在文件里(如 /etc/config/nginx.conf)。

墨:两种方式,更新配置时行为一样吗? 不一样,这是第一道坑(12.4 讲)。先记住”卷挂载”能热更新,“环境变量”不能。


12.3 Secret:最大的误解——它不是加密的

这是全章最重要的纠正。

kubectl create secret generic db-pass --from-literal=password=xxx,然后你以为密码被”加密”存起来了。错。

Secret 在 etcd 里默认是 base64 编码,不是加密。 base64 只是编码(为了存二进制安全),任何人拿到 Secret 对象,base64 解码一下就是明文密码。而且 API Server 返回 Secret 时也是明文(只是传输走 TLS)。etcd 磁盘上默认也是明文(base64 版的明文)。

墨:那它叫 Secret 图啥? 它的”秘密”体现在访问控制:RBAC 可以限制”谁能读 Secret”(不像 ConfigMap 默认谁都能看),且默认不进 Pod 的环境变量日志、不显示在 kubectl describe 的明文里(要显式 kubectl get secret -o yaml 才看到)。但它不提供静态加密、不提供保密性——“Secret”是权限层面的隔离,不是密码学层面的加密。

⚠️ 顶级坑: 很多人以为 Secret 安全,把生产密码放进去,结果 etcd 备份文件泄露(或有人有 get secret 权限),所有密码明文外泄。Secret 默认不防”能读它的人”和”能读 etcd 的人”。

12.3.1 演化一:Encryption at Rest(静态加密)

演化(K8s 1.7+ 静态加密 GA): 引入 EncryptionConfiguration——让 API Server 在写入 etcd 前加密 Secret(用 AES/KMS 等),etcd 磁盘上存的是密文,读时 API Server 解密。这样 etcd 备份泄露也不直接暴露密码。

墨:这加密是在哪一层做的?etcd 自己不加密吗? 在 API Server 层做(写入 etcd 前加密、读出后解密),etcd 只看到密文。还可以接 KMS(密钥管理服务)——密钥不在集群里,由云 KMS(如 AWS KMS)保管,API Server 调 KMS 加解密。这样”加密密钥”和”密文”分离,安全性更高(即使有人拿到 etcd 和 API Server 内存外的东西,没 KMS 密钥解不开)。这是”秘密管理”的正确姿势:静态加密 + 密钥外包给 KMS。

12.3.2 演化二:别把秘密存 K8s 里——External Secrets / Vault

更进一步的演化:秘密根本不该是 K8s 的对象。生产里秘密通常在专门的密钥库(HashiCorp Vault、AWS Secrets Manager、云密钥管理)。于是有:

  • External Secrets Operator(ESO):定期从外部密钥库拉秘密,同步成 K8s 的 Secret 对象给 Pod 用。源头在 Vault,K8s 里只是副本;
  • Sealed Secrets:把 Secret 加密成”只有集群能解”的 SealedSecret,可安全存进 Git(GitOps 友好);
  • Vault Agent / CSI Secret Store:直接把秘密挂进 Pod,不经过 K8s Secret 对象。

意义: “秘密的真相之源”回到专业密钥库,K8s 只做”消费端”。这又是”核心只存元数据/引用,真相在外部专业系统”的祖传哲学(和 etcd 不存业务数据、OIDC 管身份同构)。你的密码不该和你的集群备份绑死。


12.4 热更新陷阱:卷挂载能更新,环境变量不能

这是 ConfigMap/Secret 最常被坑的地方。

12.4.1 卷挂载方式:文件会变,但应用得自己重读

CM 以 volume 挂进 Pod 后,如果 CM 内容改了,kubelet 会更新那个挂载的文件(通常几秒到几十秒延迟,靠 kubelet 的同步周期)。所以文件内容变了。

但是!应用读不读新内容,是应用自己的事。 很多程序启动时把配置读进内存后就再不读文件——CM 文件变了,应用还在用旧值。墨:那修改 CM 后,怎么让应用生效?

两条路:

  • 应用支持信号触发重读(如 nginx 收 SIGHUP 重载配置),你发信号给它;
  • 应用周期性重读文件(好设计,但少);
  • 实在不行:滚动重启 Pod(kubectl rollout restart deployment),新 Pod 挂载新 CM——这是最稳但最重的方式。

12.4.2 环境变量方式:永远不更新

如果 CM 通过环境变量注入,那改了 CM,运行中的 Pod 环境变量不变——因为环境变量是容器启动时设的,K8s 不会(也不能)去改运行中进程的环境变量。要让新值生效,必须重启 Pod(滚动更新)。

⚠️ 坑: 你改了 CM,以为应用生效了,结果应用读的是环境变量,压根没变,排障半天发现”得重启”。经验法则:需要热更新的配置用 volume 挂载(且应用支持重读),不需要热更新的(或启动即定型)才用环境变量。 或者干脆统一”改 CM 后就 rollout restart”,避免心智负担。

12.4.3 滚动更新捡新配置

正因为环境变量不热更新,生产中”改配置”的标准动作是:改 CM → kubectl rollout restart deployment。新 Pod 带着新配置起来,旧 Pod 按滚动策略退下。这样配置变更走的是”发布流程”,可控、可回滚(CM 也该版本化/存 Git)。配置变更 ≈ 一次发布,这是 GitOps 的基本姿势。


12.5 不可变配置:immutable 防止手滑

墨:CM/Secret 能改,这本身有风险吗? 有。有人手滑改了生产 CM 的一个键,没通知,一堆 Pod 悄悄挂了新配置(volume 方式)或要重启才生效(env 方式),引发诡异故障。

演化(K8s 1.21 GA immutable: true): CM/Secret 可设 immutable: true,设了就禁止修改(要改只能删了重建)。好处:(1) 防手滑——生产关键配置锁死;(2) 性能:kubelet/API Server 不用 watch 这些 CM 的变化(知道它不变),减少开销(大集群里大量 CM 的 watch 压力不小)。

墨:这和”配置要能改”矛盾吗? 不矛盾。immutable 的意思是”这个对象本身不可变”,你要新配置就建一个新的、改引用(或滚动发布时换 CM 名)。这反而更规范——每次配置变更是”新对象 + 新引用”,可追溯、可回滚,而不是”原地改老对象”(老对象改了,历史没了,回滚都回不到旧值)。又是”不可变优于原地改”的声明式哲学(和镜像不可变同源)。


12.6 配置体积与 etcd 边界(再提一次)

CM/Secret 也是 etcd 里的对象(第 5.5)。单个 CM/Secret 上限 1MB。有人把整个大配置文件、甚至几 MB 的证书链塞进 Secret,会创建失败或撑 etcd。墨:大配置(如几 MB 的配置文件)该放哪? 放外部配置服务/对象存储,CM 里只放路径引用;或拆小。别用 K8s 对象当硬盘(呼应第 5.3、10.3)。


12.7 一道串联:配置/密钥/存储的交集

你发现 CM/Secret 既是”配置”(第 12 章),也是”特殊卷”(第 10.2.3),Secret 还和”静态加密/外部密钥库”相关(第 12.3)。它们本质上都是”把外部数据注入 Pod”的机制,只是:

  • CM:非敏感、可热更、大点也行(但别超 1MB);
  • Secret:敏感、需访问控制、最好静态加密或外置;
  • 两者都能 env 或 volume 注入,volume 可热更、env 需重启。

墨:你排障时,怎么快速判断”Pod 用的是新配置还是旧的”?

kubectl exec 进 Pod 看挂载文件内容(volume 方式);env 方式看 env 输出;或 kubectl describe cm 看当前值对比。若对不上,多半是 env 方式没重启、或应用没重读文件。先确认”注入方式 + 应用是否重读”这两点,八成能定位。


12.8 本章演化线小结

  • 配置抽离镜像(不可变 + 环境分离),CM 存非敏感、Secret 存敏感;
  • Secret 默认 base64 非加密——权限隔离 ≠ 密码学保密,最大误解;
  • 演化一:Encryption at Rest(API Server 层加密 etcd,可接 KMS 外包密钥);
  • 演化二:秘密外置(External Secrets / Vault / Sealed Secrets),真相源回专业密钥库;
  • 热更新陷阱:volume 挂载文件可变(应用需重读/signal),env 变量永不更新(须重启);
  • 标准动作:改 CM 后 rollout restart,配置变更走发布流程;
  • immutable: true 防手滑 + 减 watch 开销,不可变优于原地改。

12.9 本章思考题

🔧 思考题 1

你用 env 方式从 ConfigMap 注入 LOG_LEVEL,线上想临时调成 debug 排障。你 kubectl edit cm 改了值,但应用日志级别没变。请解释为什么,并给出两种让新值生效的做法,分析各自动量和风险。

【参考答案】 env 方式在容器启动时由 kubelet 把 CM 值设为环境变量,运行中进程的环境变量不可被 K8s 修改。改 CM 对象不会改变已运行容器的 env,所以日志级别不变。两种生效做法:(1) kubectl rollout restart deployment:滚动重建 Pod,新 Pod 启动读新 CM 值。动量:稳、可靠、走发布流程可回滚。风险:滚动期间短暂实例数波动(用 maxSurge/maxUnavailable 控,第 7.2.2),且如果应用没正确处理优雅退出可能丢在途请求。(2) 改 volume 挂载方式 + 应用支持信号重载(如 nginx -s reload):不重启 Pod,kubelet 更新挂载文件,发 SIGHUP 让应用重读。动量:零中断、快。风险:要求应用支持热重载(很多 Go 程序没实现,得自己写信号处理逻辑);且 volume 方式改动需改 Pod spec 重新发布一次(一次性改动)。排障临时调级别,推荐做法(1)最快(改动小),但注意”改完记得改回并再 restart”——否则 debug 级别一直开着,日志量爆炸。这题考 env 不热更新的本质。

🔧 思考题 2

有人说”Secret 就是加密的,放心用”。你作为 reviewer 怎么反驳?如果他坚持用 K8s Secret 存生产数据库密码,你会要求他至少补哪两道防护?

【参考答案】 反驳:K8s Secret 默认只是 base64 编码(非加密),etcd 磁盘默认明文,任何有 get secret 权限的人或能读 etcd 备份的人都能秒解明文。它提供的是 RBAC 访问控制层面的隔离,不是密码学保密——“Secret”这名字容易误导。所以”放心用”是错的。补两道防护:(1) 启用 Encryption at Rest:配 EncryptionConfiguration 让 API Server 写 etcd 前加密 Secret,最好接 KMS(密钥外包云密钥管理),这样 etcd 备份泄露也不暴露明文;(2) 严格 RBAC + 审计:限制谁能 get/read Secret(默认很多角色能看),并对 Secret 访问开审计日志,异常读取可回溯。进阶:直接外置密钥(Vault/External Secrets),K8s 里只留同步副本甚至不留(用 CSI Secret Store 直接挂)。底线:若用原生 Secret 存生产密码,至少加密静态存储 + 最小权限 + 审计,且 etcd 备份必须加密保管。这题是生产安全红线。

🔧 思考题 3

把 immutable: true 设在生产 ConfigMap 上。后来你真要改配置,怎么做才”规范且可回滚”?对比”原地 edit 老 CM”和”新建 CM + 改引用”两种改法的可追溯性差异。

【参考答案】 immutable CM 不能 edit,规范做法:新建一个 CM(如 app-config-v2),改 Deployment 的引用指向新 CM,然后 rollout restart。回滚只需把引用指回 app-config-v1 再 restart。对比:(1) 原地 edit 老 CM:对象被覆盖,旧值丢失,无法回滚到”改之前的精确状态”(除非你有 Git 里的旧 YAML 且重新 apply,但 etcd 里老版本已没);且违反 immutable 初衷。(2) 新建 + 改引用:每次变更是”新对象”,旧对象仍在,天然可回滚、可追溯(v1/v2 并存,谁在用一目了然)。这正是”不可变优于原地改”——和镜像/镜像 tag 同理(你不会 docker commit 改老镜像,而是建新 tag)。可追溯性差异:新建法让”配置变更历史”显式留在对象名/引用里;原地改法把历史埋进”最后一次写入”,靠外部系统(Git)才能找回。结论:immutable 强制你走”新建+引用”的规范路径,顺便获得可追溯/可回滚——它不只是防手滑,更是逼出好实践。

🔧 思考题 4(进阶)

GitOps 模式下,ConfigMap/Secret 应该直接存 Git 吗?Secret 明文存 Git 显然不行,那”Sealed Secrets”或”External Secrets”分别怎么解决”Git 里安全存秘密、集群里能解”?从”信任边界”角度对比两者。

【参考答案】 CM 可存 Git(非敏感),但 Secret 明文存 Git 是重大泄露风险(Git 历史难清)。两种解法:(1) Sealed Secrets:用集群独有的公钥把 Secret 加密成 SealedSecret(密文)存 Git;只有集群里的 controller 有私钥能解密还原成真 Secret。信任边界:加密密钥在集群内,Git 里只有密文,就算 Git 泄露也解不开(除非同时拿到集群私钥)。适合”秘密本就该在集群、只是要能进 Git 做 GitOps”。(2) External Secrets (ESO):Git 里根本不放秘密内容,只放”去哪个外部密钥库(Vault/AWS SM)取哪个秘密”的引用;集群里的 ESO 周期性拉取外部库、同步成 K8s Secret。信任边界:秘密真相源在外部专业密钥库(Vault),K8s 只是消费端副本;Git 泄露无任何秘密(只有引用)。对比:Sealed Secrets 信任边界是”集群私钥保密”,秘密仍驻留 K8s 生态;ESO 信任边界是”外部密钥库安全”,K8s 不持有真相。若公司已用 Vault,ESO 更优(单一真相源、密钥轮转在 Vault 统一管);若没有外部库、想轻量 GitOps,Sealed Secrets 够用。两者都解决了”Git 可存、集群可解”,只是秘密的”家”一个在集群、一个在外部库。


12.10 小结

  • 配置抽离镜像(不可变 + 环境分离);CM 非敏感、Secret 敏感;
  • Secret 默认 base64 非加密——权限隔离 ≠ 密码学保密,最大误解;
  • 静态加密(Encryption at Rest + KMS)让 etcd 密文;外置密钥(Vault/ESO/Sealed)让真相回专业库;
  • 热更新坑:volume 可热更(应用需重读/signal),env 永不更新(须重启);改配置走 rollout restart;
  • immutable: true 防手滑 + 减 watch,逼出”新建+引用”的可追溯实践;
  • 单 CM/Secret ≤ 1MB,大配置外置(别用 K8s 对象当硬盘)。

下一章进安全模型——认证/RBAC 我们在第 4 章讲了入门,这章把”Pod 能碰什么、网络能通到哪、容器能以什么权限跑”这三层补全。重点讲 PSP 怎么被废弃、Pod Security 怎么接班——又一场”老方案不够好”的演化。