从默认放行到零信任:用 Kubernetes NetworkPolicy 隔离三层应用

2026-09-08 20 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

Kubernetes 集群中的 Pod 通常可以彼此通信。部署了 Frontend、Backend 和 Database,并不意味着三层之间天然存在网络边界。要实现零信任,需要先把命名空间切换为默认拒绝,再基于标签、端口和方向逐条开放业务真正需要的链路。

NetworkPolicy 是标准 Kubernetes API,但 API 对象本身不会拦截数据包。真正执行规则的是 CNI 插件。下面的实验假设集群使用 Calico,或者使用另一种明确支持 Ingress 与 Egress NetworkPolicy 的 CNI。

策略生效前,先确认两个前提

第一,确认 CNI 具有策略执行能力。可以先查看集群中的网络组件:

kubectl get pods -A | grep -Ei 'calico|cilium|antrea|weave'

如果使用的是不支持 NetworkPolicy 的网络插件,YAML 可能成功创建,但流量不会被阻断。

第二,给工作负载设计稳定且受控的标签。本文使用:

  • tier=frontend
  • tier=backend
  • tier=database

NetworkPolicy 把标签当作网络身份。如果攻击者或普通开发账号可以任意修改 Pod、Deployment 的标签,就可能把自己伪装成 Backend。因此,网络策略必须和 RBAC、准入控制一起使用。

搭建一个可测试的三层环境

下面的命令创建独立命名空间、三个工作负载以及对应的 ClusterIP Service。示例中的数据库密码仅适用于临时实验,生产环境应改用 Secret。

kubectl create namespace production-app

kubectl run frontend \
  --image=nicolaka/netshoot \
  --labels=tier=frontend \
  -n production-app \
  -- sleep 3600

kubectl run backend \
  --image=nginx \
  --labels=tier=backend \
  -n production-app

kubectl run database \
  --image=postgres:18 \
  --labels=tier=database \
  -n production-app \
  --env=POSTGRES_DB=myapp \
  --env=POSTGRES_USER=appuser \
  --env=POSTGRES_PASSWORD=securepass123

kubectl expose pod frontend --port=80 --target-port=80 -n production-app
kubectl expose pod backend --port=80 --target-port=80 -n production-app
kubectl expose pod database --port=5432 --target-port=5432 -n production-app

kubectl get pods -n production-app --show-labels
kubectl get services -n production-app

等待三个 Pod 进入 Running 状态。应用策略前,可以从 Frontend 验证默认放行行为:

kubectl exec -n production-app frontend -- curl --max-time 3 http://backend

正常情况下会返回 Nginx 页面。

先全部拒绝,再精确开孔

完整零信任基线应同时覆盖 Ingress 和 Egress。只有 Ingress 默认拒绝时,Pod 仍然可以主动访问其他服务或互联网;只有 Egress 默认拒绝时,其他 Pod 仍可能主动连接它。

下面是一组可直接应用的策略:

cat > network-policies.yaml <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production-app
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-allow-frontend
  namespace: production-app
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              tier: frontend
      ports:
        - protocol: TCP
          port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-allow-backend
  namespace: production-app
spec:
  podSelector:
    matchLabels:
      tier: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              tier: backend
      ports:
        - protocol: TCP
          port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-allow-egress
  namespace: production-app
spec:
  podSelector:
    matchLabels:
      tier: frontend
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              tier: backend
      ports:
        - protocol: TCP
          port: 80
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-allow-egress
  namespace: production-app
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              tier: database
      ports:
        - protocol: TCP
          port: 5432
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
YAML

kubectl apply -f network-policies.yaml
kubectl get networkpolicy -n production-app

这里有三个容易混淆的语义:

  1. podSelector: {} 选择当前命名空间中的全部 Pod。
  2. fromto 中只有 podSelector 时,它选择的是策略所在命名空间中的 Pod。
  3. 多个策略不会按顺序覆盖。Kubernetes 会合并它们允许的流量,效果相当于 OR。新增策略通常是在扩大许可范围,而不是创建一条优先级更高的拒绝规则。

Egress 最容易漏掉 DNS

Frontend 访问 http://backend 时,需要先通过 CoreDNS 把 Service 名解析为 ClusterIP。启用默认拒绝 Egress 后,如果没有显式允许 53/UDP 和 53/TCP,常见现象不是连接被拒绝,而是域名解析超时。

先确认 kube-system 具有示例中使用的标准命名空间标签:

kubectl get namespace kube-system --show-labels

上面的规则允许访问 kube-system 中所有使用 53 端口的工作负载,兼容性较好,但范围稍宽。生产环境可以查看 CoreDNS 的实际标签,再组合 namespaceSelectorpodSelector,只选中 DNS Pod。例如,若集群使用 k8s-app=kube-dns

- to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
  ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

同一个 peer 中的 namespaceSelectorpodSelector 是 AND 关系:既要位于目标命名空间,又要具有指定 Pod 标签。

用正向和反向用例验证边界

策略创建成功不等于策略符合预期。验证时至少要覆盖允许路径、跨层绕行、错误端口和跨命名空间访问。

Frontend 到 Backend 应成功:

kubectl exec -n production-app frontend -- \
  curl --max-time 3 http://backend

Frontend 使用 Backend 的未授权端口应失败:

kubectl exec -n production-app frontend -- \
  nc -zvw 3 backend 8080

Frontend 直接访问 Database 应失败,因为 Frontend Egress 不允许这条链路,同时 Database Ingress 只信任 tier=backend

kubectl exec -n production-app frontend -- \
  nc -zvw 3 database 5432

为了验证 Backend 到 Database,可以创建一个带 Backend 标签的诊断 Pod。这个测试也直观说明了为什么标签修改权限必须受到保护:

kubectl run backend-probe \
  --image=nicolaka/netshoot \
  --labels=tier=backend \
  -n production-app \
  -- sleep 3600

kubectl exec -n production-app backend-probe -- \
  nc -zvw 3 database 5432

无标签 Pod 访问 Backend 应被拒绝:

kubectl run unlabeled-probe \
  --image=nicolaka/netshoot \
  -n production-app \
  -- sleep 3600

kubectl exec -n production-app unlabeled-probe -- \
  curl --max-time 3 http://backend

还应从另一个命名空间测试:

kubectl run external-probe \
  --image=nicolaka/netshoot \
  -n default \
  -- sleep 3600

kubectl exec -n default external-probe -- \
  curl --max-time 3 http://backend.production-app.svc.cluster.local

未授权流量通常表现为超时,而不是立即返回 connection refused。超时意味着数据包可能被策略丢弃;connection refused 往往意味着流量已到达目标地址,但目标端口没有进程监听。

外部 API 与策略叠加的边界

如果 Frontend 需要调用外部 HTTPS API,可以使用 ipBlock 限制目标网段,并排除已知地址:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-allow-external-api
  namespace: production-app
spec:
  podSelector:
    matchLabels:
      tier: frontend
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 203.0.113.0/24
            except:
              - 203.0.113.1/32
      ports:
        - protocol: TCP
          port: 443

203.0.113.0/24 是文档示例网段,使用前必须替换。还要注意,ipBlock 观察到的是 NAT 前还是 NAT 后的地址,可能受 CNI、云网络和 Service 实现影响。NetworkPolicy 也不是域名防火墙;如果需求是按域名、HTTP 路径或用户身份授权,应考虑支持相应能力的出口网关、服务网格或 CNI 扩展策略。

此外,这条策略会和已有的 frontend-allow-egress 合并,而不会替换它。删除旧策略之前,不要假设新策略能够收紧旧策略已经允许的流量。

上线检查清单

将 NetworkPolicy 推向生产环境时,可以按以下顺序降低误封风险:

  • 确认 CNI 同时执行 Ingress 和 Egress 策略。
  • 盘点真实通信矩阵,包括 DNS、监控、日志、时间同步和外部 API。
  • 为每条允许链路同时检查源端 Egress 和目标端 Ingress。
  • 使用稳定标签,并通过 RBAC 或准入策略限制标签修改权限。
  • 从默认拒绝开始,以最小端口和最小身份范围开放流量。
  • 同时编写允许与拒绝测试,不能只验证正常路径。
  • 观察超时、DNS 错误和 CNI 日志,再逐步扩大部署范围。

NetworkPolicy 的关键不在于写出一份复杂 YAML,而在于把应用依赖变成一张可以测试的通信矩阵:Frontend 只能到 Backend,Backend 只能到 Database,其余路径默认不存在。这样即使某一层被攻陷,横向移动和数据外泄的空间也会明显缩小。


相关推荐