Skip to content

NetworkPolicy Isolation

Tại sao network isolation là bắt buộc trong multi-tenant

Mặc định, mạng Kubernetes là phẳng và mở hoàn toàn: mọi Pod có thể kết nối tới mọi Pod khác trong cluster, bất kể namespace. Theo tài liệu GKE, "lưu lượng mạng trong một namespace là không giới hạn. Lưu lượng giữa các namespace phải được cho phép một cách tường minh" — nhưng điều này chỉ đúng sau khi bạn áp dụng NetworkPolicy. Trước đó, một Pod trong namespace tenant-a có thể gọi trực tiếp database Pod trong namespace tenant-b chỉ cần biết địa chỉ.

Trong multi-tenancy, đây là lỗ hổng nghiêm trọng. Một tenant bị xâm phạm có thể quét toàn bộ mạng cluster, gọi internal API của tenant khác, đánh cắp dữ liệu, hoặc tấn công metadata server để leo thang. NetworkPolicy là cơ chế duy nhất ở lớp L3/L4 để chặn các luồng này. Không có nó, namespace isolation chỉ là ảo tưởng ở góc độ mạng.

Internal model: NetworkPolicy hoạt động thế nào

NetworkPolicy là tài nguyên namespaced mô tả luồng được cho phép đến/từ một tập Pod. Các đặc tính quan trọng định hình cách dùng:

  1. Additive (cộng dồn), không có deny rule. NetworkPolicy chỉ liệt kê những gì được phép. Không có "deny" tường minh. Cơ chế deny đến từ việc: một khi một Pod bị chọn bởi bất kỳ NetworkPolicy nào cho một hướng (ingress/egress), mọi lưu lượng không khớp rule nào sẽ bị chặn cho hướng đó.

  2. Mặc định allow-all. Nếu không có policy nào chọn một Pod, Pod đó cho phép mọi lưu lượng. Đây là lý do bạn cần một policy default-deny tường minh.

  3. Stateful. Kết nối được cho phép theo một hướng thì traffic phản hồi tự động được cho phép (không cần rule ngược).

  4. Selector-based. Chọn Pod qua podSelector, chọn namespace nguồn/đích qua namespaceSelector, hoặc IP block qua ipBlock.

Điểm tinh tế quan trọng: NetworkPolicy mặc định chỉ áp dụng trong namespace của nó. Một policy trong tenant-a chọn Pod trong tenant-a. Để chặn lưu lượng từ namespace khác, bạn dùng namespaceSelector trong rule ingress của namespace đích.

Foundation pattern: default-deny mỗi namespace

Bước đầu tiên, bắt buộc, cho mọi namespace tenant — chặn toàn bộ ingress và egress, rồi mở dần:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: tenant-a
spec:
  podSelector: {}        # chọn MỌI Pod trong namespace
  policyTypes:
  - Ingress
  - Egress
  # không có ingress/egress rule = chặn tất cả

Sau khi áp policy này, mọi Pod trong tenant-a bị cô lập hoàn toàn — kể cả không gọi được DNS. Đây là điểm khởi đầu zero-trust. Tiếp theo bạn mở các luồng cần thiết một cách tường minh.

Cho phép DNS (gần như luôn cần)

Default-deny egress chặn cả DNS, làm hỏng mọi thứ. Phải mở DNS tới kube-system:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

Cho phép ingress chỉ trong cùng namespace

Cho phép Pod trong tenant-a gọi nhau, nhưng chặn từ namespace khác:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-same-namespace
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector: {}    # mọi Pod trong CÙNG namespace (tenant-a)

Lưu ý: podSelector: {} trong rule ingress chỉ khớp Pod trong cùng namespace với policy. Đây là cách cô lập tenant ở mức namespace.

Chặn metadata server và internal ranges (egress hardening)

Một con đường tấn công kinh điển: Pod tenant gọi metadata server 169.254.169.254 để lấy token của node service account, hoặc gọi internal services không nên truy cập. Với egress policy, chặn các dải này:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-metadata-and-internal
  namespace: tenant-untrusted
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32   # metadata server
        - 10.0.0.0/8           # internal VPC ranges
        - 172.16.0.0/12
        - 192.168.0.0/16

Với code untrusted, kết hợp việc này với Workload Identity (Chương 13) để metadata server không expose token node, và GKE Sandbox (file 07) để cô lập kernel.

GKE Dataplane V2: enforcement engine

GKE có hai cơ chế thực thi NetworkPolicy:

  1. Calico (legacy network policy, phải bật tường minh --enable-network-policy).
  2. GKE Dataplane V2 — dựa trên eBPF và Cilium, là hướng được khuyến nghị cho cluster mới.

Theo tài liệu Dataplane V2, điểm quan trọng cho multi-tenancy:

  • Network policy enforcement có sẵn (built-in). "Bạn không cần bật network policy trong cluster dùng Dataplane V2" — nó luôn được thực thi qua anetd (controller eBPF), thay thế cách tiếp cận iptables truyền thống. Không thể bật/tắt riêng.

  • Hiệu năng và scale tốt hơn. eBPF tránh chi phí iptables tuyến tính khi số Pod/rule tăng, quan trọng với cluster multi-tenant lớn nhiều policy.

  • Network policy logging. Dataplane V2 hỗ trợ logging các kết nối bị allow/deny — cực kỳ giá trị để audit và debug isolation trong môi trường multi-tenant (xem network policy logging).

Hạn chế cần biết của Dataplane V2

  • endPort không có hiệu lực. Theo tài liệu, "nếu bạn chỉ định trường endPort trong NetworkPolicy trên cluster có Dataplane V2, nó sẽ không có hiệu lực". Bạn không thể dùng dải port (port range) — phải liệt kê từng port. Đây là giới hạn thực tế khi cần segmentation chi tiết theo port.

  • Cilium Identity exhaustion. Cilium gán một "identity" cho mỗi tập label Pod duy nhất; giới hạn là 65.536 identities. Nếu cấu hình sai (ví dụ Pod có label giá trị duy nhất như timestamp), số identity tăng vọt và cạn kiệt, làm hỏng enforcement. Trong multi-tenant với nhiều label động, đây là một failure mode cần giám sát.

Production architecture patterns

Pattern 1: Zero-trust per-namespace (chuẩn cho untrusted)

Mỗi namespace tenant nhận: (1) default-deny-all, (2) allow-dns, (3) allow-same-namespace, (4) allow egress chỉ tới các đích cần thiết (ingress controller namespace, một số external API qua FQDN nếu dùng Cilium). Mọi thứ khác bị chặn. Đây là baseline cho SaaS untrusted.

Pattern 2: Tiered services với namespaceSelector

Cho phép một namespace shared-gateway gọi vào các namespace tenant, nhưng tenant không gọi chéo nhau. Dùng namespaceSelector với label trên namespace:

yaml
ingress:
- from:
  - namespaceSelector:
      matchLabels:
        role: shared-gateway

Kết hợp với HNC tree labels (file 02) để cho phép một tenant cha gọi toàn bộ cây con của nó nhưng không sang tenant khác.

Pattern 3: FQDN-based egress (Cilium-specific)

NetworkPolicy chuẩn của Kubernetes chỉ làm việc với IP/CIDR cho egress ngoài cluster — vô dụng với API có IP động (ví dụ api.stripe.com). Cilium (qua CiliumNetworkPolicy, một CRD mở rộng) hỗ trợ egress theo FQDN. Trong multi-tenant cần cho phép tenant gọi một số external API cụ thể, đây là công cụ duy nhất khả thi. Lưu ý đây là CRD Cilium, không phải NetworkPolicy chuẩn.

Real-world scenario

SaaS phân tích dữ liệu, mỗi khách hàng một namespace: Mỗi namespace khách hàng có default-deny. Pod ứng dụng chỉ được: gọi DNS, nhận ingress từ namespace platform-ingress (Gateway), gọi egress tới Cloud SQL qua Private Service Connect IP cụ thể, và gọi storage.googleapis.com qua Cilium FQDN policy. Mọi lưu lượng tới namespace khách hàng khác, tới metadata server, tới internal admin services đều bị chặn. Network policy logging được bật để audit, mọi kết nối deny được gửi tới Cloud Logging làm bằng chứng compliance.

Common mistakes / anti-patterns

Anti-pattern 1: Không có default-deny — chỉ allow rules

Vì sao xảy ra: Đội chỉ thêm allow rule cho các luồng họ biết, nghĩ rằng phần còn lại bị chặn ngầm.

Hệ quả ở scale: Nếu một Pod không bị chọn bởi policy nào cho ingress, nó allow-all. Một Deployment mới quên gắn label đúng sẽ mở toang. Isolation không nhất quán, có lỗ hổng âm thầm.

Cách phòng tránh: Luôn bắt đầu bằng default-deny-all (podSelector: {}) cho mỗi namespace, rồi mới mở. Default-deny chọn mọi Pod nên không Pod nào bị bỏ sót.

Anti-pattern 2: Quên egress, chỉ làm ingress

Vì sao xảy ra: Tư duy "chặn ai vào" mà quên "chặn đi đâu".

Hệ quả ở scale: Một Pod bị xâm phạm vẫn gọi được metadata server, internal services, hoặc exfiltrate dữ liệu ra ngoài. Ingress-only không ngăn lateral movement và data exfiltration.

Cách phòng tránh: Default-deny cả Egress. Đặc biệt với untrusted, kiểm soát egress chặt là phòng tuyến chống exfiltration và token theft.

Anti-pattern 3: Quên mở DNS, rồi tắt luôn NetworkPolicy

Vì sao xảy ra: Áp default-deny, mọi thứ vỡ (DNS không resolve), đội hoảng và xóa hết policy.

Hệ quả ở scale: Cluster quay về allow-all hoàn toàn — mất toàn bộ isolation, thường vĩnh viễn vì "policy gây sự cố".

Cách phòng tránh: Luôn thêm allow-dns cùng lúc với default-deny. Test trên một namespace trước. Hiểu rằng default-deny egress chặn DNS là hành vi đúng, cần mở tường minh.

Anti-pattern 4: Dựa vào port range với Dataplane V2

Vì sao xảy ra: Dùng endPort để cho phép dải port, không biết Dataplane V2 bỏ qua nó.

Hệ quả ở scale: Policy trông như giới hạn dải port nhưng thực tế không có hiệu lực — hoặc cho phép rộng hơn ý định. Một lỗ hổng isolation âm thầm.

Cách phòng tránh: Trên Dataplane V2, liệt kê từng port tường minh. Kiểm tra hành vi thực tế bằng network policy logging.

References