被挡住的时机不同,要修的地方也不同
一句话总结
练习编写配置,与练习确认配置确实得到执行,是两回事。事故总是发生在后者。
为什么必须在真正强制执行的环境中练习
前面的模块多次使用了 securityContext 和 Pod Security。然而,运行那些实验的环境既没有 kubelet,也没有容器运行时,所以即使违规的 Pod 也会直接变成 Running。
这让我们练习了如何编写配置,却没有练习如何确认配置确实生效。而实际工作中的事故恰恰发生在后者——配置看似全都写好了,实际却没有应用,而这种状态仍然显得一切正常。
同一个“不可行”分散在三个层面
| 层面 | 这里有什么 | 症状 | 何时出现 |
|---|---|---|---|
| 准入 | Pod Security Admission、策略引擎 | kubectl apply 被拒绝 |
部署前(CI) |
| kubelet | runAsNonRoot、镜像 USER 检查 |
Pod 已创建,出现 CreateContainerConfigError |
刚部署后 |
| 运行时 | 只读根文件系统、capability、seccomp | Pod 为 Running,只有应用失败 |
走到相应代码路径时 |
最好的是准入。反馈立即到达,而且错误资源根本不会进入集群。
最糟的是运行时。Pod 看起来运行正常,症状因此像应用错误。平时一切正常、只在特定代码路径失败的情况也很常见。
所以应遵循这个顺序:能在前一层阻止的问题,不要留给后一层。
Pod Security Admission 要从 warn 开始
如果直接启用 enforce,现有工作负载会在下一次重新部署时全部被阻止。这本身就是故障。
pod-security.kubernetes.io/warn: restricted # 먼저 이것만
pod-security.kubernetes.io/enforce: restricted # 경고가 0 이 된 뒤에
顺序不是先切断再统计,而是先统计再切断。
收紧容器后会破坏什么
提前了解启用安全设置后实际会在哪里失败,便能在故障发生前修正镜像。下面只整理最常遇到的情况。
只读根文件系统。 大多数应用都会在某处写临时文件,包括日志、缓存、套接字,以及语言运行时使用的临时目录。把根目录设为只读后,这些写入全部会失败,但症状通常表现为应用权限错误,很难立刻联想到安全设置。解决办法是为每个需要写入的路径挂载 emptyDir;与其猜测应用写入哪里,不如先在收紧后的状态运行一次,收集所有失败路径,这样更快。
以非 root 用户运行。 首先遇到的是无法监听 1024 以下端口。原本在容器内监听 80 端口的镜像必须改为 8080,同时修改 Service 的 targetPort。此外,如果镜像内文件归 root 所有,新用户可能无法读取,因此构建镜像时也要一并转移所有权。
删除全部 capability。 大多数应用一个都不需要,但监听低端口需要 NET_BIND_SERVICE,使用 ping 需要 NET_RAW。应当按照只加回确实需要的能力这一顺序操作;如果在不知道需要什么的情况下保留全部能力,收紧就失去了意义。
seccomp 默认配置。 不被允许的系统调用会返回 EPERM;如果库把这个错误吞掉,症状就会出现在看似毫不相关的地方。本实验环境中某些容器实验受阻,也是同类原因。
总之,顺序应当是:先以收紧后的配置运行,收集失败项,只为这些失败开放例外,再逐步从镜像中消除例外清单。 反过来,如果从“先全部放开,以后再收紧”开始,那个“以后”永远不会到来。
实务中真正重要的事
能在前一层阻止的问题,不要留给后一层。 在准入阶段拦截,CI 会立即发现;在运行时才触发,Pod 会看似正常地运行,直到特定代码路径失败,症状就像应用错误。同一条规则由哪一层强制执行,会让响应成本相差十倍。
等警告归零后再启用 enforce。 直接启用会让现有工作负载在下次部署时全部受阻,这本身就是故障。必须遵循先用 warn 统计、再实施阻断的顺序。
runAsNonRoot 失败看起来并不像 Pod 创建失败。 Pod 会成功创建,只留下 CreateContainerConfigError,因此往往很久以后才会怀疑镜像中的 USER。在构建镜像时明确指定 USER,是成本最低的预防方式。
下一个实验将在真实集群上逐项验证这三点。