AI时代的Kubernetes教程36招-第26招-Pod Security、SecurityContext、Seccomp和Capabilities怎么守住运
从 Privileged 到 Restricted:数字人 Namespace 的 Pod 运行时边界收口
准入层挡得住声明,挡不住系统调用;真正的边界落在运行时。
[读者] 在 Kubernetes 上跑数字人与推理服务的平台 / 后端工程师
[痛点] Pod 靠 privileged + hostPath 跑通业务,却过不了生产安全合规
[现在读] Pod Security Admission 已 GA,enforce 一开,旧 spec 直接创建失败
[读完] 能为一个命名空间设计 Restricted 路径,并封堵四类高危入口
[旧方案] privileged Pod + hostPath + hostNetwork 直连
|
v
[新需求] 生产命名空间至少 Baseline,核心业务 Restricted
|
v
[冲突] 旧 Pod spec 逐条命中 Restricted 的禁止项与必填项
|
v
[后果] 发布流水线中断,滚动升级被准入层一拒到底
我是老李,在一家做数字人直播的公司带云原生平台。数字人这条链路很特别:一个直播间背后挂着 TTS、口型驱动、实时渲染几个服务,模型文件大、GPU 贵、端到端延迟按毫秒算。
早期为了让它先跑起来,我们的 Deployment 模板写得很“野”:hostNetwork: true 图省事直连内网网关,privileged: true 让容器直接摸到显卡设备,hostPath 把 /dev/nvidia* 和几十 GB 的模型缓存目录整个挂进去。大半年里没人觉得有问题——它确实又快又稳。
转折点是一次集群安全扫描。报告结论很短:这个命名空间里几乎每一个 Deployment,按 Pod Security Standards 评判都是 Privileged 级别,而生产环境的最低门槛是 Baseline,核心业务要 Restricted。
一个 Pod 声明,究竟能“碰到”多少宿主机的东西?
该在哪一层拦它:准入时拒掉 spec,还是运行时锁死系统调用?
如果策略一开业务全挂,我们是该把标准放宽,还是该把工作负载改对?
01、故事
digital-human 命名空间里跑着一批 Deployment:TTS 合成、口型驱动、实时渲染、模型预热。它们共享同一个镜像基线,也共享同一份“能跑就行”的 Pod 模板。
任务是:在不改变业务语义、不牺牲推理吞吐的前提下,把这一批工作负载从事实上的 Privileged 级别,收敛到 Restricted。
当时的方案由三根支柱撑着。第一根是 hostNetwork: true,容器直接用宿主机网络栈,省掉 Service 和 NAT 这一层。第二根是 privileged: true,容器里的进程直接拿到全部 capabilities,能打开 /dev/nvidia*、能改 cgroup。第三根是 hostPath,把节点上的模型缓存目录挂进容器,避免每个 Pod 重复拉几十 GB 权重。
变化来自平台侧的一次合规要求:生产命名空间必须显式声明 Pod Security 级别,并且要通过同一条发布流水线。
冲突就在这里。Restricted 里没有“privileged 但受控”这种中间态——privileged 本身就是禁止项;hostPath 不在 Restricted 允许的 volume 类型白名单里;hostNetwork 属于 host namespaces,在 Baseline 这一级就已经被禁。
旧声明最省事
新边界最诚实
模板一改就得对
谁也别想蒙混过
02、问题
旧方案失效得很快:一旦命名空间打上 enforce: restricted,新建 Pod 会被 API Server 直接拒绝,连调度器都看不到它。
业务影响是直接的:滚动升级卡住,扩容卡住,回滚也卡住——因为回滚的目标 ReplicaSet 用的还是旧模板。
技术表现则相当具体。在 enforce 生效后,kubectl apply 返回的错误会一次性列出所有违规点:
Error from server (Forbidden): error when creating "dh-render.yaml":
pods "dh-render-5f7c9d" is forbidden: violates PodSecurity "restricted:latest":
privileged (container "render" must not be privileged),
hostPath volumes (volumes "dev", "model-cache"),
host namespaces (hostNetwork=true),
allowPrivilegeEscalation != false (container "render" must set
securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "render" must set
securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "render" must set
securityContext.runAsNonRoot=true),
seccompProfile (pod or container "render" must set
securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
这段报错(示例输出)其实是一份很好的免费清单:它把 Restricted 的禁止项和必填项分开列了出来。前者是“不能有”,后者是“必须有”,两者缺一不可。
可验证的完成标准因此可以写成三条:命名空间在 enforce: restricted 下,所有线上工作负载能被正常创建并进入 Running;容器内进程的 UID 非 0 且 NoNewPrivs=1;推理服务的接口成功率与收敛前保持一致。
报错即清单
禁止加必填
先读再动手
别凭印象改
03、原理
这一章只讲三件事:级别怎么分、声明怎么翻译、内核怎么执行。
第一层,级别。 Pod Security Standards 定义了三档:Privileged 完全不受限;Baseline 阻止已知的权限提升手法(host namespaces、privileged、hostPath、hostPort、非默认 capabilities 等);Restricted 在 Baseline 之上加了一组必填项,包括 allowPrivilegeEscalation=false、runAsNonRoot=true、capabilities.drop=["ALL"]、必须设置 seccompProfile(RuntimeDefault 或 Localhost),同时把 volume 类型限制在白名单内。
第二层,声明。 SecurityContext 是 Pod spec 里描述“这个容器该以什么身份、什么权限运行”的字段。Pod 级和容器级都能写,容器级优先。它本身不执行任何策略,只是把意图翻译成传给容器运行时的参数。
第三层,内核。 真正生效的是内核:runAsNonRoot / runAsUser 决定 UID;readOnlyRootFilesystem 决定根挂载是不是只读;allowPrivilegeEscalation: false 对应内核的 no_new_privs,让 setuid 二进制不再能提权;capabilities 决定进程的 capability 集合;seccomp 决定哪些系统调用会被内核直接拒掉。
反直觉判断: Pod Security Standards 是一个纯声明层的约束,它只读 Pod spec 的字面值,根本不看进程的实际行为。一个完全合规的 Restricted Pod,只要 seccompProfile 填了 RuntimeDefault,它跑起来之后照样能在运行时默认允许的系统调用范围内做很多事。所以“过了 Restricted”不等于“容器被锁死了”——这两件事分属两层,需要分别设计。
第二个反直觉判断: allowPrivilegeEscalation: false 不是 Kubernetes 在拦你,而是它在告诉内核给进程打上 no_new_privs。也就是说,这条策略的强度完全取决于内核版本和容器运行时实现,而不是 API Server 有多严格。读策略时如果只看 YAML 写没写,就永远不知道它到底生效没有。
声明是本意
内核才是真
两层分开看
才知漏在哪
04、架构
[输入] Pod spec(kubectl apply / controller 创建 Pod)
|
v
[模块] Pod Security Admission(读取 Namespace 上的 PSA 标签)
|
v
[数据/状态] enforce / audit / warn 三种模式 × privileged / baseline / restricted 三个级别
|
v
[处理] securityContext → 容器运行时参数 → 内核(UID / capabilities / no_new_privs / seccomp / 只读 rootfs)
|
v
[输出] 准入放行 + 运行时受限的容器进程
先说边界。这一套组合能拦住的是:Pod 声明层面的越权(host 命名空间、hostPath、privileged、额外 capabilities),以及运行时的系统调用面与提权路径。它拦不住的是:镜像本身的漏洞、应用层的任意代码执行,以及被显式挂载进容器的敏感路径——因为后者属于“声明合法但语义危险”,准入层读不出来。
再说收益。PSA 最大的好处是零依赖接入:不需要装 webhook,只要给命名空间打标签,API Server 自己就会拦。SecurityContext 则把安全意图写进 Git,跟着 workload 一起评审、一起回滚。
代价同样明确。Restricted 会强迫你回答几个以前可以回避的问题:镜像能不能以非 root 跑、根文件系统能不能只读、进程到底需要哪几个 capability、需要放行哪些系统调用。这些答案往往要改 Dockerfile 和启动脚本,不是加几行 YAML 就能糊过去。
适用条件是:命名空间级别的隔离已经成立(即一个命名空间对应一个信任域),且你能接受“策略口径会随 Kubernetes 版本演进”这件事。这也是为什么下面的实战里会把版本标签钉死。
标签即策略
声明即契约
运行时兜底
三层各司其职
05、实战一次
环境:Kubernetes v1.25 及以上(Pod Security Admission 自 v1.25 GA),kubectl 已配置集群访问。
先给命名空间打标签。注意三件事:enforce 是真正拦截的,audit 只写审计日志,warn 只回 warning;版本标签用来钉住策略口径,避免升级集群时策略悄悄变严。
# deploy/security/pod-security/01-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: digital-human
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.29
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.29
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.29
然后写一个能过 Restricted 的最小工作负载。关键点在 Pod 级和容器级各承担一半:Pod 级负责身份(runAsNonRoot、runAsUser、fsGroup、seccompProfile),容器级负责权限(allowPrivilegeEscalation、capabilities、readOnlyRootFilesystem)。
# deploy/security/pod-security/02-dh-render.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dh-render
namespace: digital-human
spec:
replicas: 1
selector:
matchLabels:
app: dh-render
template:
metadata:
labels:
app: dh-render
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: render
image: registry.example.com/dh/render:1.0.0
ports:
- name: http
containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: run
mountPath: /var/run
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
volumes:
- name: tmp
emptyDir: {}
- name: run
emptyDir: {}
这里有个必须配对的改法:readOnlyRootFilesystem: true 之后根文件系统整体只读,应用往 /tmp、/var/run 写东西会直接 EACCES。用 emptyDir 显式挂出可写路径,是 Restricted 下的标准做法——它同时满足 volume 白名单,又把可写面缩到最小的几个目录。
镜像侧也要配合。runAsNonRoot: true 的语义是“如果镜像要跑成 root,就别启动”,所以镜像里必须有非 root 用户,并把文件所有权交给它:
# 片段:镜像以非 root 用户运行
FROM registry.example.com/base/runtime:1.0.0
RUN groupadd -g 10001 app && useradd -u 10001 -g 10001 -M -s /sbin/nologin app
COPY --chown=10001:10001 ./app /opt/app
USER 10001:10001
WORKDIR /opt/app
ENTRYPOINT ["/opt/app/render-server"]
部署与验证:
kubectl apply -f deploy/security/pod-security/01-namespace.yaml
kubectl apply -f deploy/security/pod-security/02-dh-render.yaml
kubectl -n digital-human get pod -l app=dh-render
kubectl -n digital-human exec deploy/dh-render -- id
kubectl -n digital-human exec deploy/dh-render -- grep -E 'Seccomp|NoNewPrivs' /proc/1/status
kubectl -n digital-human exec deploy/dh-render -- grep CapEff /proc/1/status
未在当前环境实测,以下为预期结果(示例输出):
$ kubectl -n digital-human exec deploy/dh-render -- id
uid=10001 gid=10001 groups=10001
$ kubectl -n digital-human exec deploy/dh-render -- grep -E 'Seccomp|NoNewPrivs' /proc/1/status
Seccomp: 2
Seccomp_filters: 1
NoNewPrivs: 1
$ kubectl -n digital-human exec deploy/dh-render -- grep CapEff /proc/1/status
CapEff: 0000000000000000
Seccomp: 2 表示进程处于 seccomp filter 模式,说明 RuntimeDefault 生效了;NoNewPrivs: 1 说明 allowPrivilegeEscalation: false 真的落到了内核;CapEff 全 0 说明 drop: ["ALL"] 之后进程不持有任何有效 capability。
三个数都对了,V1 才算跑通。但它只证明了“新写的 Pod 合规”,还没证明“旧的高危入口被堵住”。后者是下一章的事。
先跑通一个
再谈怎么收
数字不撒谎
内核会作证
06、排查
V1 跑通之后,真正的工作才开始:把旧工作负载一个个搬过来。下面三条诊断链来自这次搬迁过程。
诊断一:镜像以 root 运行,runAsNonRoot 直接卡住创建。
现象是 Pod 停在 CreateContainerConfigError,既不 Running 也不 Crash。怀疑是镜像 UID 问题,检查 kubectl describe pod 的描述信息,证据是:
Error: container has runAsNonRoot and image will run as root
根因是 runAsNonRoot: true 由 kubelet 在创建容器之前校验镜像的 USER,校验依据是镜像元数据,而不是运行时实际 UID。修复方式是在 Dockerfile 里显式 USER 10001:10001,或者在 securityContext 里写死 runAsUser: 10001,让校验有明确依据。
错误尝试: 一开始我们把 runAsNonRoot 删掉,只写 runAsUser: 10001,以为这样“既限定了 UID 又能过 Restricted”。结果是 Pod 确实起来了,但 Restricted 明确要求 runAsNonRoot=true,准入层立刻把创建请求拒掉——错误在于把“能跑”当成“合规”,而 PSA 校验的是声明字段本身,不是运行结果。
诊断二:根文件系统只读,应用启动即崩。
现象是容器 CrashLoopBackOff,日志第一条就是 open /tmp/render.pid: permission denied。怀疑是 readOnlyRootFilesystem,检查 kubectl logs 拿到的写失败路径,根因是应用在启动阶段要往 /tmp 和 /var/run 写 PID 文件和 socket。修复方式是把这两个路径用 emptyDir 挂出来,让可写面从“整个根文件系统”缩到两三个目录。
诊断三:RuntimeDefault 下某个系统调用被拒。
现象是业务进程报 operation not permitted,但 capability 已经是零、UID 也是非 root,看起来“权限已经够低了”。怀疑是 seccomp,检查节点侧的内核审计日志(dmesg 或 auditd 记录)确认是哪个 syscall 被拦。根因是容器运行时的默认 seccomp profile 并不放行所有系统调用,某些冷门 syscall 会被直接返回 EPERM。修复优先级是:先看业务能不能不用这个 syscall;确实必须用时,才用 Localhost 类型的自定义 profile 精确放行,而不是退回 Unconfined。
最后是那个最大的错误。
错误尝试: 我们曾打算一步到位,直接用 pod-security.kubernetes.io/enforce: restricted 配 restricted:latest,一次把整个命名空间收干净。结果是所有新 Pod 立刻被拒,滚动升级全线中断,连紧急回滚都做不到。错误有三层:一是跳过了 warn / audit 的观察期,没先摸清存量工作负载会命中哪些规则;二是用了 latest 这个会随集群版本漂移的版本标签,等于把策略口径交给升级动作去决定;三是把“策略收紧”和“工作负载改造”当成同一件事做,而这两者本该分开推进。
先看再改
别一步到位
版本要钉死
存量要盘点
07、优化
基于上一章的证据,V2 做四件事,每件都对应一个根因。
第一件:把落地顺序改成 warn → audit → enforce。
根因是“一次性 enforce 会把存量工作负载整体打爆”。修改是让标签分三步走:先只开 warn,创建请求返回 warning 但放行,用 warning 清单盘点存量;再开 audit,把违规写进审计日志做长期统计;最后才开 enforce。新行为是策略收紧的过程可观测、可回退。验证方式是对存量清单逐个跑 kubectl apply --dry-run=server,看返回的 violation 列表是否收敛到空。
第二件:把版本标签从 latest 改成具体版本。
根因是 latest 会随集群升级改变策略口径,导致“什么都没改,Pod 突然创建失败”。修改是把 enforce-version / audit-version / warn-version 全部写成明确的版本号。新行为是集群升级不会静默改变准入结果,策略变更必须显式改 YAML。验证方式是对比升级前后同一份清单的 --dry-run=server 输出。
第三件:镜像统一非 root 构建。
根因是 runAsNonRoot 校验的是镜像元数据。修改是在基础镜像层统一建 10001 用户、COPY --chown、USER 10001:10001。新行为是任何一个符合模板的新镜像都能直接过 Restricted,不需要在 securityContext 里做特例。验证方式是对每个镜像跑一次带 runAsNonRoot 的创建测试。
第四件:去掉 host 侧的一切挂载,改用声明式替代。
这是四类高危入口的正面处理:hostNetwork 换成 Service + Ingress;privileged 拿 GPU 换成 NVIDIA device plugin,用 resources.limits 声明 nvidia.com/gpu;hostPath 的模型缓存换成 PVC 或 initContainer 预热;runtime socket 一律不挂。新行为是 Pod spec 里不再出现任何 host 路径。验证方式是在准入前先用 --dry-run=server 跑一遍,确认输出的 violation 列表为空。
改完之后,生产端口从 privileged 降到了 restricted:Pod 声明里不再有任何 host 侧引用,可写路径只剩显式挂载的两个 emptyDir,进程的有效 capability 为空集。业务侧的推理链路本身没有改动。
一步一收紧
一事一根因
声明式替代
才算真收口
08、演进
[同一输入] 同一个 Deployment(dh-render)
|
+--[V1] privileged + hostNetwork + hostPath
| 行为:一次创建成功,直连宿主
| 代价:边界形同虚设,节点可被旁路
|
+--[V2] Restricted + securityContext + Seccomp/内核约束
| 行为:必须显式声明身份 / 权限 / 可写路径
| 代价:镜像与启动脚本要改,需要过渡期
|
[Trade-off]
得到什么:可审计的运行时边界,提权路径被切断
失去什么:一部分“图省事”的写法(hostPath 共享、host 网络直连)
适用边界:命名空间即信任域,且工作负载可以改镜像
正确性上,V2 明显更高:V1 的安全模型是“靠人自觉”,V2 是“靠准入层强制”。V1 里任何一个新同学写错一个字段,就是一个能触达宿主机的 Pod;V2 里这种 Pod 根本创建不出来。
稳定性上,V2 反而更好。V1 的隐患在于共享 hostPath 目录——多个 Pod 同时读写同一份缓存,写坏了会互相影响,而且没人能说清是谁写坏的。V2 把缓存变成每个 Pod 自己的存储,故障域被切小了。
复杂度上,V2 更高。V1 加几行 YAML 就完事;V2 要改 Dockerfile、要挂 emptyDir、要把 hostNetwork 那段网络配置搬到 Service 上、要处理 GPU 从“直挂设备”到“device plugin”的迁移。这些是实打实的工作量。
成本上,V2 有一次性投入和持续的边际成本:镜像重建、缓存预热策略、GPU 资源声明。换来的是安全事故面的收敛——这笔账在数字人这种把模型和 GPU 都堆在同一批节点上的场景里,是划算的。
适用范围上,V2 的前提是命名空间已经是一个信任域。如果多个互不信任的租户挤在同一个命名空间里,Restricted 也救不了你,那是命名空间划分的问题,不是策略级别的问题。
遗留问题有三个:一是 Localhost 类型的自定义 seccomp profile 需要分发到每个节点,运维成本不低;二是 GPU 工作负载的 capability 需求比普通业务复杂,需要逐个核对;三是镜像基线的非 root 改造是长期工程,不是一次能做完的。
旧路图省事
新路图可控
取舍看场景
边界要认账
09、洞见
9.1 准入是命名空间级的,所以先划信任域再谈策略
PSA 的 enforce 标签打在命名空间上,同一个命名空间里所有工作负载共享同一个上限。这意味着一件事:只要有一个工作负载确实需要 privileged,整个命名空间的策略上限就被拉到 privileged,里面所有 Pod——包括未来任何一次误提交——都跟着一起“被允许”。
所以在讨论“要不要用 Restricted”之前,先要回答的是“这个命名空间里是不是所有工作负载都互相信任”。如果答案是否,正确的动作不是研究怎么让特权 Pod 通过 Restricted(它过不了),而是把特权 Pod 拆出去,放到独立命名空间里,让那个命名空间去承担 privileged 这个级别。安全策略的粒度从来不是“某个 Pod”,而是“某个命名空间”。
9.2 反直觉判断:最危险的不是 privileged,是 runtime socket
privileged: true 很显眼,做安全扫描的人一眼就能看到,而且它在策略里是硬禁止项。真正隐蔽的是把容器运行时的 socket 挂进容器——无论是通过 hostPath 挂 Docker 的 socket,还是挂 containerd 的 socket。在它所在的命名空间里,这个 Pod 声明的级别看起来可能很干净,“只挂了一个文件”。
但只要容器内进程能访问那个 socket,它就可以请求运行时启动一个真正 privileged 的容器,一步绕过所有准入策略。准入层看到的是“一个 volume”,运行时看到的是“一个能创建任意容器的控制面”。声明和语义之间的这道缝,是准入层管不到的地方。
所以排查高危入口时,第一步该搜的不是 privileged: true,而是所有能把 socket、/proc、/sys、/dev 带进容器的挂载。
9.3 反直觉判断:readOnlyRootFilesystem 是成本最低的入侵可见性
大家通常把 readOnlyRootFilesystem 当成一条“加固”措施:防止攻击者写文件落盘。但它真正的价值在运维侧——它把一个模糊的问题变成了一个明确的问题。
在可写根文件系统下,应用往哪写、写了什么,基本没人知道;进程被入侵后落个后门,你只能靠扫描去发现。而在只读根文件系统下,“应用想写哪里”必须在 YAML 里显式声明成挂载点。这份挂载清单本身就是一份关于“这个服务需要什么可写能力”的文档,任何一次意外的写失败都会在启动时就暴露出来,而不是在夜里三点。代价是你要为每一个可写路径做一次显式声明,并跟代码一起维护。
9.4 capability 按“最小可用”配,不要按“最小权限”配
drop: ["ALL"] 之后要不要 add 回来,是个具体问题。数字人渲染服务基本不需要额外 capability;但如果某个服务要绑 80/443,Restricted 允许你 add: ["NET_BIND_SERVICE"],而不是让你整段放行。
这里的判断标准是“最小可用”:先 drop ALL,跑一遍完整业务路径,看哪个能力缺失导致失败,再逐条加回来,每条都要有理由。而不是照着某个“最佳实践清单”一次性加一堆——那些清单往往来自别人的业务模型,跟你的不一样。
边界在命名空间
危险在挂载项
加固即观测
能力按需加
10、系统落地
原来我们有一套“能跑就行”的 Pod 模板,里面默认带着 hostNetwork、privileged 和 hostPath。它能支撑业务,但它的安全模型完全依赖写模板的人自觉。
本篇新增了三样东西。第一是命名空间级的策略声明:enforce / audit / warn 三个标签加固定版本,把安全要求从“评审意见”变成“准入规则”。第二是一份可复用的 Pod 模板:Pod 级管身份、容器级管权限,emptyDir 显式挂出可写路径,可以直接被新服务复制。第三是一份高危入口检查清单,四类入口(hostPath、hostNetwork、privileged、runtime socket)逐条有对应的替代方案。
现在能做的事情变多了:新服务上线时,如果它的 Pod spec 不符合 Restricted,流水线会在 kubectl apply --dry-run=server 这一步就失败,而不是等到安全扫描。存量服务则可以在 warn 模式下逐步改造,不阻塞发布节奏。
还缺的也有三块。一是 Localhost 类型的自定义 seccomp profile 还没有统一分发机制,目前只能用 RuntimeDefault,遇到确实需要精确放行的场景只能个案处理。二是镜像基线的非 root 改造还没覆盖全部服务,部分老服务需要特殊处理。三是策略变更缺少自动化回归——现在靠人跑 --dry-run=server,未来应该进 CI。
下一步的演进方向是:把 PSA 的 warn 输出接进 CI,每次提交 PR 都跑一遍;把 seccomp profile 纳入节点初始化流程统一分发;把“四类高危入口”做成一条静态检查规则,在代码评审阶段就拦住。
模板可复用
清单可检查
策略进流水线
才算落地了
11、小结
Q1 → 一个 Pod 能碰到宿主机的路径有三类:host 命名空间(network / pid / ipc)、host 路径(hostPath,尤其是 runtime socket)、提权路径(privileged、额外 capabilities、allowPrivilegeEscalation)
Q2 → 两层都要拦:准入层用 PSA 把禁止项和必填项钉死在命名空间上,运行时层用 securityContext + seccomp + capabilities 收窄内核面;只做前者,容器里依然自由
Q3 → 不该放宽标准,该改工作负载:hostNetwork 换 Service、privileged 换 device plugin、hostPath 换 PVC / emptyDir,标准不动,代价由改造承担
状态 → 数字人命名空间可在 enforce: restricted 下稳定创建 Pod,进程 UID 非 0、NoNewPrivs=1、CapEff 为空集、根文件系统只读
声明先钉死
内核再收窄
标准不松口
改造来买单
12、作业
12.1 理解题:Restricted 相比 Baseline,多出来的到底是“禁止项”还是“必填项”?
参考答案:Restricted 是在 Baseline 的禁止项之上,额外增加了一组必填项。禁止项部分是继承的(hostPath、host namespaces、privileged 等基线就已经禁止);新增的是强制性要求:allowPrivilegeEscalation 必须为 false、runAsNonRoot 必须为 true、capabilities 必须 drop ALL、必须显式设置 seccompProfile 为 RuntimeDefault 或 Localhost,并且 volume 类型被限制在白名单内。所以从 Baseline 升到 Restricted,工作量的主要来源是“补必填项”,而不是“删更多东西”。
12.2 实战题:一个服务需要绑定容器内的 80 端口,同时要过 Restricted,securityContext 该怎么写?
参考答案:以非 root 用户运行且要绑 1024 以下端口时,需要 NET_BIND_SERVICE capability。最小写法是在容器 securityContext 中先 drop: ["ALL"],再单独 add: ["NET_BIND_SERVICE"]——Restricted 明确允许这一个能力被加回来。同时 Pod 级要有 runAsNonRoot: true、runAsUser、seccompProfile: RuntimeDefault,容器级要有 allowPrivilegeEscalation: false。另一种思路是改用 8080 这类高位端口,由 Service 做端口映射,这样 capability 可以保持为空集,是更彻底的方案。
12.3 排障题:命名空间已设为 enforce: restricted,某个 Pod 报 CreateContainerConfigError,describe 显示 container has runAsNonRoot and image will run as root,怎么定位和修复?
参考答案:这个报错来自 kubelet 在创建容器前对镜像元数据的校验,不是运行时报错。定位方法是用 kubectl describe pod 看 Events,或者直接读镜像的 Config.User 字段。根因是镜像元数据里没有非 root 的 USER。修复有两条路:修改 Dockerfile 显式 USER 10001:10001(推荐,一劳永逸);或者在 securityContext 里写明 runAsUser: 10001,让 kubelet 有明确依据。注意不要通过删除 runAsNonRoot 来绕过——Restricted 要求该字段必须为 true。
12.4 架构判断题:有人提出“我们的命名空间里只有一两个 Pod 需要 privileged,所以先给整个命名空间设成 privileged,其它的靠 code review 保证不用特权”,这个方案能接受吗?
参考答案:不建议。Pod Security Admission 的策略等级是命名空间级的,命名空间内所有工作负载共享同一个上限。一旦设为 privileged,该命名空间里所有 Pod——包括未来任何一次误提交——都失去了准入层的保护,而 code review 属于流程控制,不具备强制力。更合理的做法是把需要特权的少数工作负载拆到独立命名空间,那个命名空间用 privileged,其余业务命名空间保持 restricted。这样“信任域”才和“命名空间”真正对齐。
判断题见功力
边界题见取舍
会写会查会拆
才算真的会
13、思考
这次收口真正教会我的,不是 Restricted 有几个字段,而是“边界”这个词在容器安全里有两层含义。
一层是声明边界:Pod spec 里写了什么。PSA 管的是这一层,它是入口检查,强制力强、成本低,但只看字面值。
另一层是运行时边界:进程实际能做什么。SecurityContext、seccomp、capabilities 管的是这一层,它作用于内核,不受声明是否“看起来正确”的影响。
两层都不做,等于没有边界;只做一层,等于把边界交给运气。声明层合规但运行时自由,一个 runtime socket 挂载就能把声明层的努力全部抵消;运行时收得紧但声明层不设限,下次有人写错一个字段,就是一个能触达宿主机的 Pod。
所以真正的收口顺序应该是:先用命名空间划出信任域,再用准入层把下限钉死,最后用运行时约束把上限压窄。三者缺一,边界就不成立。四类高危入口之所以要单独梳理,正是因为它们是“声明合法、语义危险”的典型——它们不违反任何一条你能想到的规则,但它们让容器与宿主之间的那道墙消失了。
声明定下限
运行压上限
两层都立住
边界才算真

浙公网安备 33010602011771号