🚦 NetworkPolicy và luồng traffic giữa các Pod

Mục tiêu học tập

  • Giải thích được mô hình mạng phẳng: mỗi Pod có IP riêng và mặc định gọi được tới mọi Pod khác.
  • Đọc hiểu manifest NetworkPolicy: podSelector, policyTypes, ingress/egress, from/to, ports.
  • Phân biệt đúng AND (cùng một phần tử trong list) và OR (nhiều phần tử trong list).
  • Viết được bộ policy default-deny + allow theo label mà không tự khóa mình ra ngoài.
  • Nhận ra cạm bẫy lớn nhất: CNI không hỗ trợ NetworkPolicy thì policy tạo ra mà không có tác dụng gì.

Ôn tập nhanh

Ở bài Service và Ingress, bạn đã dùng selector trên label để Service tìm ra endpoint. NetworkPolicy dùng đúng cơ chế label đó, nhưng mục đích ngược lại: không phải để mở đường mà để giới hạn đường. Nếu bạn còn nhớ kubectl get pods --show-labels, bài này sẽ rất nhanh.

Lý thuyết

1. Mạng phẳng: mặc định ai cũng nói chuyện được với ai

Trong cluster, mỗi Pod có một IP riêng và mọi Pod định tuyến được tới Pod khác mà không qua NAT. Không có tường lửa mặc định ở tầng Pod. Cluster vừa dựng xong thì Pod ở namespace frontend gọi thẳng IP của Pod trong namespace db được, và ngược lại.

NetworkPolicy là resource namespaced dùng để thay đổi mặc định đó. Nó chọn Pod mục tiêu bằng label, rồi khai báo danh sách traffic được phép vào (ingress) và ra (egress).

2. Ba nguyên tắc không được quên

  • Không có policy nào chọn Pod: mọi chiều đều allow all.
  • Có ít nhất một policy chọn Pod và policyTypes gồm Ingress: Pod bị isolate chiều đó, chỉ traffic khớp rule mới vào được.
  • Nhiều policy cùng chọn một Pod: cộng dồn (union), không có thứ tự ưu tiên, không có rule deny.

Nói cách khác, bạn không viết "chặn A". Bạn viết "mở B" và phần còn lại tự bị chặn một khi đã có policy. Đây là lý do NetworkPolicy được gọi là allow-list.

flowchart TD
    A["📦 Pod nhận traffic"] --> B{"Có policy nào chọn Pod này<br/>với policyTypes Ingress?"}
    B -->|Không| C["✅ Allow all"]
    B -->|Có| D["🔒 Isolated"]
    D --> E["Chỉ hợp các rule được liệt kê<br/>không có deny, không có thứ tự"]

policyTypes nếu bỏ trống sẽ được suy ra: có ingress: thì thêm Ingress, có egress: thì thêm Egress. Cẩn thận: muốn chặn sạch egress thì phải khai policyTypes: [Egress] với egress: []. Nếu chỉ khai [Ingress], chiều ra vẫn mở toang.

3. AND và OR trong from / to

Đây là chỗ người mới sai nhiều nhất. Quy tắc: cùng một phần tử list = AND, khác phần tử = OR.

  ingress:
  - from:
    - namespaceSelector:            # AND: phải ở namespace monitoring
        matchLabels:
          kubernetes.io/metadata.name: monitoring
      podSelector:                  # VÀ có label app=prometheus
        matchLabels:
          app: prometheus
  ingress:
  - from:
    - podSelector: {matchLabels: {app: frontend}}   # OR: frontend cùng namespace
    - namespaceSelector:                            # HOẶC bất kỳ Pod nào
        matchLabels:                                # trong namespace monitoring
          kubernetes.io/metadata.name: monitoring

Label kubernetes.io/metadata.name được gắn tự động bằng tên namespace (từ Kubernetes 1.21), nên không cần tự label namespace nữa.

4. Cạm bẫy số một: CNI

NetworkPolicy chỉ là ý định khai báo. Việc thực thi nằm ở CNI plugin. Calico, Cilium, Antrea làm được. Flannel bản mặc định không làm được: kubectl apply vẫn báo thành công, kubectl get netpol vẫn liệt kê policy, nhưng traffic không hề bị chặn. Luôn kiểm tra CNI trước khi tin vào policy của bạn.

[!WARNING] Trước khi kết luận "policy sai", hãy xác nhận CNI của cluster có hỗ trợ NetworkPolicy. Nếu không, mọi thứ bạn viết chỉ là tài liệu trang trí.

Ví dụ có hướng dẫn

Ví dụ 1: default-deny ingress cho cả namespace

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: shop
spec:
  podSelector: {}
  policyTypes:
  - Ingress

podSelector: {} chọn tất cả Pod trong namespace shop. Không có rule ingress nào nghĩa là không ai vào được. Đây là policy đầu tiên bạn nên apply ở mọi namespace mới.

Ví dụ 2: mở đúng một đường

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

Đọc theo thứ tự: Pod app=api nhận traffic từ Pod app=frontend cùng namespace, trên port 8080 của container. Vì chỉ có một rule, mọi port khác của api cũng bị chặn. Lưu ý port ở đây là container port, không phải port của Service.

Ví dụ 3: egress và cái bẫy DNS

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: db
    ports:
    - protocol: TCP
      port: 5432
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

Rule thứ hai là DNS. Bỏ nó đi thì api vẫn "nói chuyện" được với IP của db, nhưng không resolve được db.shop.svc.cluster.local, và mọi lời gọi bằng hostname sẽ chết với lỗi trông rất giống lỗi policy.

Thực hành có hướng dẫn

Bài 1: Đọc policy

Với policy allow-frontend-to-api ở trên, Pod app=api còn nhận được traffic từ đâu nữa không? Trả lời trước khi mở đáp án.

Xem đáp án

Không còn nguồn nào khác. Pod api đã bị isolate chiều ingress, và chỉ một rule duy nhất được khai. Traffic từ Ingress controller, từ Pod test, hay từ chính bạn gọi thẳng IP Pod đều bị chặn, trừ khi nguồn đó có label app=frontend trong namespace shop.

Bài 2: Viết policy cho namespace shop

Yêu cầu: frontend chỉ nhận traffic từ Ingress controller ở namespace ingress-nginx (label app.kubernetes.io/name: ingress-nginx), port 80. db chỉ nhận từ api, port 5432.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-ingress
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes: [Ingress]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
      podSelector:
        matchLabels:
          app.kubernetes.io/name: ingress-nginx
    ports:
    - protocol: TCP
      port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-ingress
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: db
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api
    ports:
    - protocol: TCP
      port: 5432

Hai policy độc lập, cộng dồn vào nhau. Không cần policy nào "chặn" frontend gọi db.

Bài 3: Debug

Bạn apply default-deny egress cho namespace shop, sau đó thêm rule cho phép api gọi db:5432. kubectl exec vào api chạy nc -zv db 5432 thì báo bad address. Nguyên nhân?

Xem hướng dẫn

bad address là lỗi resolve DNS, không phải lỗi kết nối. Egress đã bị isolate và bạn chưa allow port 53. Thêm rule DNS như Ví dụ 3, hoặc test nhanh bằng IP của Pod db (kubectl get pod -l app=db -o wide) để xác nhận đường 5432 đã thông. Cách kiểm tra tổng quát: chạy một Pod tạm trong namespace để test từ cả hai phía.

kubectl run nettest --rm -it -n shop --image=nicolaka/netshoot -- bash
# trong Pod: nc -zv api 8080 ; nslookup db

Góc chuyên gia

  • Thứ tự apply không quan trọng, nhưng thời điểm thì có. Các policy cộng dồn nên apply cùng lúc là an toàn. Cái không an toàn là apply mỗi default-deny rồi đi uống cà phê. Luôn chuẩn bị sẵn danh sách allow trước.
  • Đừng quên health check và metrics. Probe từ kubelet và Pod dùng hostNetwork không bị NetworkPolicy chi phối, nhưng Prometheus scrape thì bị. Rất nhiều dashboard "mất trắng" sau khi bật default-deny.
  • Service không phải lá chắn. Policy kiểm tra ở tầng Pod sau khi DNAT, nên gọi qua ClusterIP hay gọi thẳng Pod IP đều chịu cùng một luật.
  • Luôn bật default-deny cho cả hai chiều ở namespace mới, rồi mở dần. Bật ngược lại (mở hết rồi siết sau) gần như chắc chắn bạn sẽ bỏ sót.
  • ==Policy không có tác dụng thì kiểm tra CNI trước, kiểm tra YAML sau.==

Tóm tắt và bước tiếp theo

Bốn điều mang về: mạng Pod mặc định phẳng và mở; NetworkPolicy là allow-list namespaced, cộng dồn, không có deny; isolate chỉ xảy ra khi có policy chọn Pod với policyTypes tương ứng; và egress default-deny luôn kéo theo bài toán DNS port 53.

Tự kiểm tra nhanh:

Mặc định khi chưa có NetworkPolicy nào, traffic giữa hai Pod trong cluster là {{c1::allow all (mạng phẳng)}}.
NetworkPolicy chọn Pod mục tiêu bằng trường {{c2::podSelector}} trong spec.
Nhiều NetworkPolicy cùng chọn một Pod thì các rule được {{c3::cộng dồn (union)}}, không có thứ tự ưu tiên.
Trong một phần tử của `from`, `namespaceSelector` và `podSelector` kết hợp với nhau theo phép {{c4::AND}}.
Khi bật default-deny egress, phải allow port {{c5::53}} UDP/TCP để Pod còn resolve được DNS.
[
  { "front": "Mặc định, khi chưa có NetworkPolicy nào, traffic giữa hai Pod là gì?", "back": "Allow all, vì mạng Pod phẳng và không có tường lửa mặc định." },
  { "front": "NetworkPolicy chọn Pod mục tiêu bằng trường nào?", "back": "spec.podSelector, một label selector trong cùng namespace." },
  { "front": "Điều kiện tiên quyết để NetworkPolicy thực sự có hiệu lực?", "back": "CNI plugin hỗ trợ NetworkPolicy, ví dụ Calico, Cilium, Antrea. Flannel mặc định thì không." },
  { "front": "podSelector: {} nghĩa là gì?", "back": "Chọn tất cả Pod trong namespace đó." },
  { "front": "Khi một Pod bị policy chọn với policyTypes Ingress thì mô hình là gì?", "back": "Allow-list: chỉ traffic khớp rule được vào, phần còn lại bị chặn." },
  { "front": "Nhiều NetworkPolicy cùng chọn một Pod thì áp dụng thế nào?", "back": "Cộng dồn (union), không có thứ tự ưu tiên và không có rule deny." },
  { "front": "Trong from, namespaceSelector và podSelector nằm trong CÙNG một phần tử list nghĩa là gì?", "back": "AND: nguồn phải ở namespace khớp VÀ có label khớp." },
  { "front": "Nhiều phần tử khác nhau trong list from nghĩa là gì?", "back": "OR: khớp bất kỳ phần tử nào là đủ." },
  { "front": "Cạm bẫy lớn nhất khi bật egress default-deny?", "back": "Mất DNS: phải allow port 53 UDP/TCP tới `kube-dns`, nếu không mọi lệnh resolve tên sẽ fail và bạn tưởng policy chặn sai." },
  { "front": "Nếu bỏ trống policyTypes thì Kubernetes hiểu thế nào?", "back": "Mặc định là Ingress; nếu có egress trong spec thì tự thêm Egress." },
  { "front": "ingress trong NetworkPolicy có phải Ingress resource không?", "back": "Không. Đây là hướng traffic đi vào Pod, khác hoàn toàn Ingress resource dùng cho HTTP routing." },
  { "front": "NetworkPolicy có hiệu lực xuyên namespace không?", "back": "Không. Nó là object theo namespace và chỉ chọn Pod trong namespace của nó, nhưng được phép cho phép nguồn từ namespace khác." },
  { "front": "Vì sao nên bắt đầu bằng default-deny rồi mở dần?", "back": "Vì policy là allow-list, mở dần giúp bạn biết chính xác luồng nào thật sự cần và không bỏ sót traffic lạ." },
  { "front": "Lệnh nào giúp xem nhanh các NetworkPolicy đang có trong namespace?", "back": "kubectl get networkpolicy -n <namespace>, sau đó kubectl describe để đọc rule chi tiết." }
]

Tóm tắt và bước tiếp theo

Bốn điều cần mang theo sau bài này:

  • Mạng Pod mặc định là phẳng và allow all. NetworkPolicy là lớp allow-list duy nhất bạn có, và nó chỉ hoạt động khi CNI plugin hỗ trợ (Calico, Cilium, Antrea). Kiểm tra điều này trước khi viết policy, đừng viết xong mới phát hiện policy không có tác dụng.
  • Policy chọn Pod bằng podSelector trong namespace của chính nó. Một Pod bị chọn với policyTypes: [Ingress] thì mọi traffic vào không khớp rule đều bị chặn. Không có rule deny, không có thứ tự ưu tiên, nhiều policy thì cộng dồn.
  • Trong from/to: các phần tử trong list là OR, còn namespaceSelector với podSelector nằm trong cùng một phần tử là AND. Nhầm chỗ này là nguyên nhân số một khiến policy "chặn nhầm" hoặc "mở quá rộng".
  • Khi bật default-deny egress, luôn allow DNS (port 53) trước, rồi mới siết các luồng còn lại. Và luôn kiểm tra từ một Pod test thật, đừng chỉ đọc YAML.

Bước tiếp theo: NetworkPolicy trả lời câu hỏi "ai được nói chuyện với ai". Bài sau, RBAC, ServiceAccount và Pod Security trả lời câu hỏi "ai được làm gì trên API server, và Pod được phép chạy với quyền gì". Hai lớp này bổ trợ nhau: NetworkPolicy giới hạn traffic, còn RBAC và Pod Security giới hạn quyền của chính workload khi nó đã kết nối được. Bạn sẽ viết ServiceAccount riêng cho từng workload, gán Role/ClusterRole tối thiểu, và bật Pod Security Admission để chặn container chạy privileged.