Cilium Cilium

编写网络策略:从 L3/L4 到 L7 规则

Kubernetes 起初让每个 Pod 都能访问其他所有 Pod —— CiliumNetworkPolicy 就是用来收窄这一点的,必要时甚至可以细到 HTTP 方法。

12 min 中级

默认情况下,Kubernetes 网络没有任何限制:任何 Pod 都可以在任意端口上访问集群中任何其他 Pod。对于任何有多租户需求,或安全要求高于”内部全部信任”的集群,这个默认值通常是不对的。CiliumNetworkPolicy 就是用来收窄它的工具,从简单的允许列表,到能检查实际 HTTP 请求的规则。

起点:一切皆开放

在做任何更改之前,先确认默认行为:

kubectl run client --image=curlimages/curl --command -- sleep infinity
kubectl exec client -- curl -s http://some-other-service.default.svc.cluster.local

在没有任何策略的情况下,无论 client 是什么、some-other-service 做什么,这个请求都会成功。一旦任何策略针对某个方向(ingress 或 egress)选中了某个 Pod,该 Pod 在该方向上的流量就变为默认拒绝:只有策略明确允许的内容才能通过。完全没有策略选中的 Pod 则保持完全开放。在编写你的第一个策略之前,这是最需要理解的一点:范围写得过窄的策略,可能会悄悄切断你本不打算触碰的流量,影响它碰巧选中的任何 Pod。

L3/L4:按身份和端口限制

最常见的策略形态是限制哪些 Pod 可以在哪些端口上访问哪些 Pod,使用 Cilium 基于标签的身份(见本系列关于 Cilium 如何使用 eBPF 的指南),而不是 IP 地址:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP

这样解读:它作用于带有 app: api 标签的 Pod(endpointSelector),并且只允许来自带有 app: frontend 标签的 Pod、且只在 TCP 8080 端口上的入站流量(ingress)。任何其他 Pod 和任何其他端口现在都被拒绝访问 api Pod,因为这个策略的存在使它们进入了 ingress 的默认拒绝状态。

Egress 的工作方式相同,只是方向相反

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: api-egress-to-database
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: api
  egress:
    - toEndpoints:
        - matchLabels:
            app: database
      toPorts:
        - ports:
            - port: "5432"
              protocol: TCP

api Pod 现在只能在 5432 端口上向 database Pod 发起出站连接。你可以把 ingress 和 egress 规则合并在同一个策略中,也可以分开写,取决于哪种写法对你的环境更清晰;功能上,针对相同 Pod 的、endpointSelector 匹配的多个独立策略仍会正确地叠加组合。

允许进出集群外部的流量

fromEndpoints/toEndpoints 按标签匹配 Pod。对于进出 Pod 网络之外的流量,则改用基于 DNS 或 CIDR 的规则:

  egress:
    - toFQDNs:
        - matchName: "api.external-service.com"
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP

toFQDNs 是 Cilium 在原生 Kubernetes NetworkPolicy 之外的扩展,让你可以允许出站访问一个有名字的外部域名,而不必预先知道并固定它的 IP 地址,这对任何 IP 不由你控制、且可能无通知变更的外部服务都很重要。

L7:读取实际请求内容的策略

以上所有内容都工作在 L3/L4:基于身份和端口,而不查看流量内部。Cilium 可以更进一步,根据它所理解的 L7 协议的内容做出决策,其中最常见的是 HTTP:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: api-read-only-for-reporting
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: reporting
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "/reports/.*"

这允许 reporting 服务访问 8080 端口上的 api,但仅限路径匹配 /reports/.* 的 GET 请求。对同一端口的 POST,或对未列出路径的 GET,都会被拒绝,尽管仅靠 L3/L4 规则的话,同样的 Pod、同样的端口本会被整体放行。这是由 Cilium 内嵌的 Envoy 代理为规则所适用的 Pod 执行的;与纯 L3/L4 的 eBPF 执行相比,L7 策略有真实但通常不大的性能代价,对于 L3/L4 限制已足以满足你实际安全需求的、吞吐量很高的路径,值得记住这一点。

信任之前先测试

应用不当的策略是”失败即关闭”,即拒绝流量而非放行,这是更安全的失败模式,但如果被拒绝的是不该拒绝的流量,仍然是一次故障。在对任何承载真实流量的服务应用新的限制性策略之前:

kubectl apply --dry-run=server -f policy.yaml

然后先在较低的环境中应用,生成应当被允许的流量和应当被拒绝的流量,确认两者都符合预期,再将同样的策略推广到任何生产服务。

从全部放行开始,再有意识地收窄

对于没有任何现有策略的集群,合理的采纳路径是:不要试图在第一天就写出完整、正确的策略集。先观察真实的流量模式(Hubble,在这项技术的落地页中介绍,正是为此而生),编写与实际观察到的合法流量相匹配的策略,先在非生产环境中应用,然后才将生产流量收窄到与之匹配。针对猜测而非观察到的流量模式编写限制性策略,是造成故障的常见原因,这类故障看起来像是 Cilium 的问题,实际上却是策略没有考虑到某种没人想到要列出来的合法流量模式。

热门指南