首页 / Kubernetes (k8s) 入门教程 / 安全上下文与网络策略

Kubernetes (k8s) 入门教程

安全上下文与网络策略

本教程共 65 篇 · 第 47 篇 · 更新于 2026-08-14 · 约 24 分钟阅读

KubernetessecurityContextNetworkPolicy安全加固最小权限网络隔离

本节目标:把 Pod 加固的两条主线串起来——securityContext 管”这个容器自己能干什么”,NetworkPolicy 管”这个 Pod 能跟谁说话”。

前面三章讲的是谁能操作 Kubernetes。这一章讲的是Pod 跑起来之后自身有多大权力。两件事,两套机制。

安全上下文(第 16 章)和网络策略(第 30 章)你已经见过基础用法了。这一章不重复语法细节,而是把它们放到”加固”的视角下汇总——哪些配置真正值得配、配错了会怎样、组合起来怎么用。

47-1 加固的核心思路:假设容器会被拿下

安全设计不能假设”我的应用没漏洞”。合理的假设是:攻击者已经在你的某个容器里拿到了 shell

那么接下来该问的是:

  • 他在容器里是什么身份?root 还是普通用户?
  • 他能不能提权到宿主机?
  • 他能改容器的文件系统吗?
  • 他能从这个容器连到数据库、连到别的服务、连到外网吗?

securityContext 回答前三个问题,NetworkPolicy 回答第四个。

47-2 securityContext 加固清单

securityContext 可以配在两个层级:

  • Pod 级spec.securityContext)——对 Pod 内所有容器生效。
  • 容器级spec.containers[*].securityContext)——只对该容器生效,会覆盖 Pod 级同名设置

有些字段只在 Pod 级有(如 fsGroup),有些只在容器级有(如 capabilitiesreadOnlyRootFilesystemprivileged)。记不住就查一下,不用背。

下面这份是我认为最值得配的几项,按重要性排:

不要以 root 运行

这是投入产出比最高的一条。

spec:
  securityContext:
    runAsNonRoot: true      # 强制非 root,否则 Pod 起不来
    runAsUser: 10001        # 指定 UID
    runAsGroup: 10001       # 指定 GID

runAsNonRoot: true 是个校验开关——kubelet 会检查最终用户是不是 root,是就拒绝启动容器。runAsUser指定具体 UID。两个一起配最稳妥。

Warning

只写 runAsNonRoot: true 而镜像里没有非 root 用户,Pod 会卡在 CreateContainerConfigError,报错 container has runAsNonRoot and image will run as root。这不是配置错误,是镜像本身的问题。要么改 Dockerfile 加 USER,要么用 runAsUser 强行指定一个 UID(但要确保应用对文件系统的权限够用)。

多容器 Pod 建议给不同容器配不同 UID,避免它们互相干扰彼此的文件。

禁止权限提升

Linux 允许子进程申请比父进程更多的权限(通过 setuid 之类的机制)。关掉它:

containers:
  - name: app
    securityContext:
      allowPrivilegeEscalation: false

这一项是 PSS restricted 级别的硬要求,而且几乎没有副作用。默认就该配上。

丢弃所有权能,按需加回

Linux 把 root 的权力拆成了三十来个权能(Capability)。比如 SYS_TIME 允许改系统时钟,NET_ADMIN 允许改路由表和网卡配置。root 拥有全部权能。

容器默认会拿到一批权能,其中不少你根本用不上。全丢掉:

containers:
  - name: app
    securityContext:
      capabilities:
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"]   # 只在确实需要绑 1024 以下端口时加

NET_BIND_SERVICE 是唯一被 PSS restricted 允许加回的权能——它让非 root 进程能绑定 80、443 这类特权端口。

Tip

很多人为了让 Nginx 监听 80 端口就给容器加 NET_BIND_SERVICE。其实更干净的做法是让容器监听 8080,让 Service 把 80 映射过去。容器内的端口号跟外部访问的端口号本来就不需要一致。

绝对不要 privileged

# 反面教材,别学
securityContext:
  privileged: true

privileged: true 基本等于把容器隔离全部关掉。容器里的 root 就是宿主机的 root,能访问所有设备,能挂载文件系统。拿下这个容器就等于拿下这台节点。

真正需要它的场景极少(某些存储驱动、某些网络插件)。业务应用一律不需要。PSS 的 baseline 级别就会挡掉它。

根文件系统设为只读

containers:
  - name: app
    securityContext:
      readOnlyRootFilesystem: true
    volumeMounts:
      - name: tmp
        mountPath: /tmp
      - name: cache
        mountPath: /var/cache/nginx
volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}

只读根文件系统挡住了一大类攻击:攻击者进来后写不了脚本、装不了工具、改不了二进制。

代价是你得把应用真正需要写的目录用 emptyDir 挂出来。/tmp 基本是必须的,剩下的看应用——Nginx 要 /var/cache/nginx/var/run,Java 应用可能要临时目录。

Note

排查方法:先不设只读,跑起来,用 kubectl exec 进去看哪些目录被写了。或者直接设成只读,看应用报什么 permission denied,一个个补上去。第一次配比较费时间,但配完之后就一直有效。

Seccomp:限制系统调用

Seccomp 是 Linux 内核从 2.6.12 就有的特性,能沙箱化进程权限,限制它从用户态发起哪些系统调用。

spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault

RuntimeDefault 用容器运行时(containerd / CRI-O)提供的默认配置,屏蔽了一批危险系统调用。这是 PSS restricted 的要求,配上基本无痛。

想更细就用 type: LocalhostlocalhostProfile 指向节点上的自定义配置文件,但那需要在每个节点铺文件,运维成本高。

卷的属主:fsGroup

以非 root 运行之后经常撞上这个问题:挂载的持久卷是 root 拥有的,应用写不进去。

spec:
  securityContext:
    fsGroup: 10001
    fsGroupChangePolicy: "OnRootMismatch"

fsGroup 让 kubelet 把卷的属主组改成指定 GID。fsGroupChangePolicy: "OnRootMismatch" 表示只在根目录属主不匹配时才递归改权限——大卷上这个优化很关键,不然每次挂载都要遍历几十万个文件,Pod 启动能卡好几分钟。

一份完整的加固模板

把上面这些拼起来,这就是能通过 PSS restricted 的标准配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hardened-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hardened-app
  template:
    metadata:
      labels:
        app: hardened-app
    spec:
      automountServiceAccountToken: false     # 不需要访问 API
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          image: nginxinc/nginx-unprivileged:1.27
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

把这个当模板存下来,新应用从它开始改,比事后加固省事得多。

47-3 NetworkPolicy:东西向流量隔离

securityContext 管容器内部,NetworkPolicy 管容器之间。默认集群是扁平网络,任意 Pod 能访问任意其他 Pod——攻击者拿下一个前端容器就能横向扫描、直连数据库。

完整语法第 30 章已讲过(30-2 CNI 前置、30-3 双方向、30-5 选择器、30-6 默认拒绝),这里只做要点回顾。

要点回顾

  • 大前提:NetworkPolicy 由 CNI 实现,Calico、Cilium、Antrea 支持,Flannel 不支持——不报错、不生效
  • 白名单模型:只有允许、没有拒绝;策略相加取并集;连接要通,源端出口与目标端入口必须同时放行。
  • 默认拒绝是基线:podSelector: {} 选中整个命名空间,policyTypes 声明隔离方向;默认拒绝出站会连 DNS 一起拦掉,须单独放行 kube-dns 的 53(TCP+UDP)。
  • 选择器语义:from/to 数组里少一个 -,AND 变 OR,权限范围放大。
  • 按命名空间选要用 kubernetes.io/metadata.name 标签;访问集群外用 ipBlock;端口范围用 endPort

与 securityContext 的配合

同一场攻击的两个环节:securityContext 管”容器被拿下后能干什么”,NetworkPolicy 管”容器被拿下后能连到哪里”。

  • 先收紧容器,再收紧网络。 默认拒绝上线前先配好 securityContext,否则连不上时分不清是权限还是网络问题。
  • NetworkPolicy 只管集群内。 拦不住出集群的流量——攻击者外连 C2 要靠云安全组或 CNI 出口网关能力。
  • 三层配合。 PSA(第 46 章)强制配置、securityContext 限制容器能力、NetworkPolicy 限制可达性,缺一层都可能被绕过。

默认拒绝的运维建议与排查清单

  • 先在测试命名空间试运行,确认放行规则齐全(尤其 DNS)再推生产。
  • 逐步收紧:逐命名空间、逐应用推进,配合流量观察(Cilium flow log、Calico 策略审计)确认没误伤再继续。
  • 排查清单(命令见 47-5):① CNI 是否支持、策略是否生效;② 策略是否选中目标 Pod(kubectl describe networkpolicy);③ 源端出口和目标端入口两端都查;④ 临时调试 Pod 标签是否与业务 Pod 一致。

完整语法、示例与调试方法详见第 30 章。

47-4 两条主线怎么配合

securityContextNetworkPolicy 解决的是同一场攻击的不同环节:

攻击环节防御手段
在容器内提权到 rootrunAsNonRootallowPrivilegeEscalation: false
从容器逃逸到宿主机privilegeddrop ALL 权能、Seccomp
在容器内植入工具readOnlyRootFilesystem: true
偷取 API 凭据automountServiceAccountToken: false
横向扫描其他 PodNetworkPolicy 默认拒绝
直连数据库NetworkPolicy 限定 tier 标签
反向连接外部 C2NetworkPolicy 限制出站

还有一层在上面:用 PSA(第 46 章)把 securityContext 的要求变成命名空间级强制。不然全靠开发自觉写 securityContext,漏一个就是漏一个。

三者的分工是这样的:

  • PSA 在命名空间层面强制”必须配加固字段”。
  • securityContext 是具体配置,满足 PSA 的要求。
  • NetworkPolicy 独立地限制网络可达性。

47-5 排查与验证

验证 securityContext 生效了:

# 看容器内的用户
kubectl exec -it hardened-app-xxx -- id
# uid=10001 gid=10001

# 试着写根文件系统,应该失败
kubectl exec -it hardened-app-xxx -- touch /test
# touch: /test: Read-only file system

# 看容器实际拿到的权能
kubectl exec -it hardened-app-xxx -- cat /proc/1/status | grep Cap

验证 NetworkPolicy

# 起个临时调试 Pod 试连通性
kubectl run netshoot --rm -it \
  --image=nicolaka/netshoot -- /bin/bash
# 容器内:
#   nc -zv backend-svc 8080
#   nslookup backend-svc

# 看某个 Pod 被哪些策略选中
kubectl get networkpolicy -n production -o yaml

# describe 能看到策略解析后的规则
kubectl describe networkpolicy backend-policy -n production
Tip

用临时 Pod 测试时注意:它的标签和你的业务 Pod 不一样,所以适用的策略也不一样。要模拟真实情况,给临时 Pod 打上和业务 Pod 相同的标签,用 kubectl run --labels="tier=frontend"

47-6 加固优先级建议

一次全上容易搞砸。按这个顺序推进比较稳:

  1. automountServiceAccountToken: false——零风险,改一行,先全铺。
  2. allowPrivilegeEscalation: false + capabilities.drop: ["ALL"]——绝大多数应用无感。
  3. seccompProfile.type: RuntimeDefault——基本无感。
  4. runAsNonRoot + runAsUser——可能要换镜像或调文件权限,逐个应用推。
  5. readOnlyRootFilesystem: true——要摸清写入目录,最费时间,放最后。
  6. NetworkPolicy 默认拒绝——先在测试命名空间试,一定先加 DNS 放行。

每一步之后跑一遍完整回归。别指望一次改完不出问题。

小结

  • 加固的正确假设是”攻击者已经进来了”,然后限制他能干什么。
  • securityContext 加固清单:非 root、禁提权、drop ALL 权能、只读根文件系统、Seccomp RuntimeDefault、fsGroup 处理卷权限。
  • 绝对不要 privileged: true
  • NetworkPolicy 需要 CNI 支持,否则静默不生效。
  • 两种隔离方向独立声明;策略相加不冲突;连接要通两端都得放行。
  • 默认拒绝出站会拦掉 DNS,必须单独放行 53 的 TCP + UDP。
  • from/to 数组里少一个 -AND 就变成 OR,权限范围会大幅放宽。
  • securityContext 是配置,PSA 是强制手段,NetworkPolicy 是网络侧,三层配合。

安全部分到这里结束。下一章进入弹性与发布——先讲 HPA 自动扩缩。