NetworkPolicy Enforcement — Calico iptables vs Dataplane V2 eBPF
Vì sao chủ đề này quan trọng
Trong một cluster Kubernetes mặc định, mọi Pod nói chuyện được với mọi Pod. Đây là mô hình “flat network, default-allow” — tiện cho phát triển nhưng là cơn ác mộng bảo mật ở production. Khi một Pod bị xâm nhập (qua lỗ hổng ứng dụng, image độc, hay secret rò rỉ), kẻ tấn công ở trong một mạng phẳng có thể di chuyển ngang (lateral movement) tới database, internal API, hay control plane mà không gặp rào cản mạng nào. NetworkPolicy là kiểm soát chính để giới hạn blast radius này.
Nhưng NetworkPolicy không phải “bật là xong”. Cách nó được thực thi khác nhau căn bản giữa Calico (iptables/ipset theo IP) và Dataplane V2 (eBPF theo identity), và sự khác biệt đó quyết định policy của bạn có ổn định ở scale hay không, có quan sát được hay không, và có làm được những thứ như egress theo FQDN hay không. File này đi từ mô hình NetworkPolicy của Kubernetes, qua hai cách thực thi, tới logging và các anti-pattern thực chiến.
Internal model: NetworkPolicy của Kubernetes
Ngữ nghĩa cốt lõi
NetworkPolicy là một đối tượng namespaced, dùng label selector để chọn Pod áp dụng (podSelector) và định nghĩa các rule ingress/egress (Network policies). Vài nguyên tắc dễ sai:
- Additive (cộng dồn), không có deny rule. NetworkPolicy chỉ có “allow”. Không có khái niệm “deny rule” trong spec chuẩn. Bạn tạo trạng thái deny bằng cách chọn Pod nhưng không cho phép gì — khi một Pod bị ít nhất một policy chọn ở chiều nào đó, mọi traffic chiều đó không khớp allow sẽ bị chặn.
- Default-allow cho tới khi bị chọn. Pod không bị bất kỳ policy nào chọn thì vẫn allow tất cả. Đây là lý do phải có policy “default-deny” chủ động.
- Ingress và egress độc lập. Một policy có thể chỉ điều chỉnh ingress; egress vẫn allow trừ khi có policy egress chọn Pod đó.
- Yêu cầu plugin thực thi. Bản thân API NetworkPolicy không tự thực thi; cần một network plugin hỗ trợ (Calico, Cilium/Dataplane V2). Trên GKE, Dataplane V2 thực thi NetworkPolicy như một phần của dataplane (Dataplane V2).
Mô hình default-deny
Pattern nền tảng của mọi thiết kế bảo mật mạng nghiêm túc: trong mỗi namespace nhạy cảm, áp một policy chọn tất cả Pod và không allow gì ở chiều cần khóa, rồi mở từng đường hợp lệ bằng các policy bổ sung.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {} # chọn mọi Pod trong namespace
policyTypes:
- Ingress
- Egress
# không có ingress/egress allow => chặn tất cả hai chiềuSau đó mở có chọn lọc, ví dụ chỉ cho api gọi db trên cổng 5432, và cho phép egress DNS.
Cách thực thi 1: Calico (iptables/ipset theo IP)
Khi cluster dùng add-on dựa trên Calico, các đối tượng NetworkPolicy được dịch thành iptables rule và ipset trên từng node (Network policy with Calico). Cơ chế:
- Calico duy trì ipset chứa tập IP của các Pod khớp một selector (ví dụ tất cả Pod có label
app=api). - Policy “cho
app=apitớiapp=db:5432” trở thành rule iptables: nếu source ∈ ipset(api) và đích ∈ ipset(db) và port=5432 → ACCEPT; còn lại → DROP. - Khi Pod scale/đổi IP, Calico cập nhật ipset.
Trần cấu trúc (như đã phân tích ở file 2 và 4):
- Theo IP nên nhạy với churn. Pod đổi IP liên tục → cập nhật ipset/iptables liên tục.
- Kế thừa chi phí iptables. Số rule tăng theo số policy × số Pod khớp; lock contention và resync vẫn hiện diện.
- Khó làm L7/FQDN sạch sẽ. iptables thuần khớp ở L3/L4; egress theo tên miền (FQDN) không phải thế mạnh.
Cách thực thi 2: Dataplane V2 (eBPF theo Cilium identity)
Dataplane V2 thực thi NetworkPolicy bằng eBPF, dựa trên Cilium security identity thay vì IP (xem file 2). Khác biệt cốt lõi:
- Policy theo identity (label), không theo IP. Cilium gom các Pod cùng tập label bảo mật thành một identity (số nguyên). Rule policy nói “identity A được tới identity B trên port X”. Khi Pod mới của cùng workload xuất hiện, nó nhận lại identity cũ — rule không đổi, chỉ ánh xạ IP→identity cập nhật.
- Ổn định khi churn cao. Scale 10→100 Pod không tạo rule mới; đây là lợi thế quyết định ở môi trường rollout/autoscaling liên tục.
- Thực thi trong kernel bằng eBPF. Quyết định allow/deny xảy ra ở eBPF hook, O(1) tra cứu identity, không duyệt chuỗi.
- Hỗ trợ FQDN-based egress (qua CRD của GKE). GKE Dataplane V2 cho phép định nghĩa egress theo tên miền thông qua CRD
FQDNNetworkPolicy, điều rất khó làm với iptables thuần (FQDN network policy).
So sánh trực diện
| Tiêu chí | Calico (iptables) | Dataplane V2 (eBPF) |
|---|---|---|
| Đơn vị policy | IP/CIDR + ipset | Cilium identity (label) |
| Ổn định khi Pod churn | Phải cập nhật ipset liên tục | Rule giữ nguyên, chỉ map đổi |
| L3/L4 | Có | Có |
| FQDN egress | Khó/không sạch | Có (CRD) |
| Logging allow/deny | Hạn chế | Built-in (NetworkLogging) |
| Observability flow | Hạn chế | Hubble |
NetworkPolicy logging: quan sát allow/deny
Một trong những lý do mạnh nhất để dùng Dataplane V2 là network policy logging tích hợp. Trên cluster Dataplane V2, bạn cấu hình một CRD NetworkLogging để ghi lại các kết nối được phép (allow) và bị chặn (deny) (Network policy logging).
apiVersion: networking.gke.io/v1alpha1
kind: NetworkLogging
metadata:
name: default
spec:
cluster:
allow:
log: false # bật khi cần điều tra, tắt khi yên để giảm volume
delegate: false
deny:
log: true # luôn nên bật để thấy kết nối bị chặn
delegate: falseGiá trị thực chiến: khi một kết nối bị chặn, thay vì gói “biến mất im lặng” (như với iptables thuần), bạn có log cho biết source, đích, port và policy liên quan. Điều này biến việc debug “vì sao A không gọi được B” từ đoán mò thành tra log. Lưu ý chi phí: bật allow.log ở cluster lớn tạo lượng log rất lớn — nên bật theo nhu cầu điều tra, còn deny.log thường nên để bật vì volume thấp hơn và giá trị bảo mật cao.
Production architecture patterns
Pattern 1: Default-deny theo namespace nhạy cảm + mở có chọn lọc
Bắt đầu với default-deny-all ở các namespace chứa dữ liệu nhạy cảm (payments, identity, data), rồi mở từng đường: cho phép DNS egress, cho phép đúng caller tới đúng port. Đây là nền tảng giới hạn lateral movement.
Pattern 2: Luôn cho phép DNS khi đặt egress default-deny
Sai lầm kinh điển: bật egress default-deny rồi quên cho DNS → mọi tên miền không resolve được, ứng dụng “hỏng” theo cách khó hiểu. Luôn kèm một policy cho phép egress tới kube-system/CoreDNS trên cổng 53 (UDP/TCP).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: payments
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53Pattern 3: FQDN egress cho phụ thuộc bên ngoài (Dataplane V2)
Khi Pod cần gọi một số API bên ngoài cụ thể (ví dụ *.googleapis.com, payment gateway), dùng FQDN policy thay vì mở 0.0.0.0/0. Điều này giới hạn egress về đúng tên miền cần thiết, giảm đường exfiltration.
Pattern 4: Tận dụng identity model để policy ổn định
Thiết kế policy theo label workload (app, tier, team) thay vì cố định IP/CIDR. Trên Dataplane V2, điều này tận dụng identity model nên policy không cần sửa khi Pod scale.
Real-world scenarios
Scenario A: Lateral movement bị chặn nhờ default-deny
Một ứng dụng web bị khai thác RCE. Nhưng namespace payments có default-deny ingress và chỉ cho phép app=checkout tới db:5432. Pod web (khác namespace, khác label) không thể chạm tới DB → kẻ tấn công bị chặn ở rào mạng dù đã chiếm được một Pod. Đây chính là giá trị blast-radius của NetworkPolicy.
Scenario B: Egress default-deny làm sập app vì quên DNS
Team bật egress default-deny cho namespace mới. Ứng dụng bắt đầu lỗi “name resolution failed”. Gốc rễ: policy chặn cả egress DNS tới CoreDNS. Sửa: thêm policy allow DNS (Pattern 2). Bài học: DNS là phụ thuộc ẩn, luôn phải mở khi default-deny egress.
Scenario C: Policy theo IP “vỡ” khi autoscaling
Trên cluster Calico, một policy được viết theo CIDR cố định của vài Pod. Khi autoscaling thay Pod (IP mới ngoài CIDR), kết nối hợp lệ bị chặn ngẫu nhiên. Sửa: viết policy theo label selector, không theo IP. Trên Dataplane V2, identity model giải quyết vấn đề này tự nhiên.
Scenario D: Không biết vì sao kết nối bị chặn
Trên cluster iptables, một kết nối nội bộ thỉnh thoảng bị chặn nhưng không có dấu vết. Sau khi chuyển sang Dataplane V2 và bật deny.log, log chỉ rõ policy nào đang chặn flow nào → sửa trong vài phút thay vì vài giờ.
Common mistakes / anti-patterns
- Không có default-deny ở namespace nhạy cảm. Mạng phẳng = lateral movement tự do khi bị xâm nhập.
- Bật egress default-deny mà quên DNS. App lỗi resolve, khó debug.
- Viết policy theo IP/CIDR thay vì label. Vỡ khi Pod churn; nặng cập nhật trên Calico.
- Mở egress
0.0.0.0/0cho tiện. Mất kiểm soát exfiltration; nên dùng FQDN policy. - Không bật
deny.logtrên Dataplane V2. Mất khả năng điều tra kết nối bị chặn. - Gán label cẩu thả. Vì policy/identity dựa trên label, label sai có thể vô tình mở quyền truy cập.
- Quên rằng NetworkPolicy là namespaced. Quên cấu hình
namespaceSelectorcho traffic cross-namespace hợp lệ.
GCP-native implementation guidance
Cho phép một caller cụ thể tới DB
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-checkout-to-db
namespace: payments
spec:
podSelector:
matchLabels:
app: db
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
app: checkout
ports:
- protocol: TCP
port: 5432FQDN egress policy (Dataplane V2)
apiVersion: networking.gke.io/v1alpha3
kind: FQDNNetworkPolicy
metadata:
name: allow-external-apis
namespace: payments
spec:
podSelector:
matchLabels:
app: checkout
egress:
- matchName:
- "api.stripe.com"
matchPattern:
- "*.googleapis.com"
ports:
- protocol: TCP
port: 443Bật network policy logging
apiVersion: networking.gke.io/v1alpha1
kind: NetworkLogging
metadata:
name: default
spec:
cluster:
allow:
log: false
deny:
log: trueKết nối với Architecture Framework
- Security: NetworkPolicy default-deny là kiểm soát nền tảng để giới hạn lateral movement và blast radius (Security pillar).
- Operational excellence: policy logging biến việc debug kết nối bị chặn thành quan sát có dữ liệu, giảm thời gian xử lý sự cố (Operational excellence).
References
- Network policies (Kubernetes)
- GKE network policy (Calico)
- GKE Dataplane V2
- Network policy logging
- FQDN network policies
- Google Cloud Architecture Framework — Security
ipBlock và except: kiểm soát theo CIDR cho traffic ngoài cluster
Bên cạnh podSelector/namespaceSelector (dành cho traffic trong cluster), NetworkPolicy hỗ trợ ipBlock để khớp theo CIDR — hữu ích cho traffic đến/đi với địa chỉ ngoài cluster (on-prem, internet, VPC peer). ipBlock còn có except để loại trừ dải con:
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/8
except:
- 10.0.5.0/24 # chặn riêng dải nhạy cảm trong dải lớn được phépHai lưu ý dễ sai: (1) ipBlock khớp theo IP sau các phép NAT mà Kubernetes thấy, nên với traffic external-to-pod đã qua LB/SNAT, IP nguồn có thể không phải IP client thật — đừng dùng ipBlock để xác thực client nếu source IP đã bị che (xem externalTrafficPolicy, file 3). (2) ipBlock là L3/L4; với egress ra tên miền động (CDN, SaaS đổi IP liên tục), ipBlock không bền — đó là lúc dùng FQDN policy trên Dataplane V2.
CIDR-based vs FQDN egress: chọn đúng công cụ
| Nhu cầu egress | Công cụ phù hợp | Vì sao |
|---|---|---|
| Tới dải on-prem/VPC cố định | ipBlock | IP ổn định, L3 đủ dùng |
| Tới SaaS/API đổi IP liên tục | FQDN policy (DPv2) | IP không cố định, cần khớp theo tên |
| Tới Google APIs | FQDN *.googleapis.com | Dải IP rộng và biến động |
| Chặn toàn bộ rồi mở chọn lọc | default-deny + allow cụ thể | Giảm bề mặt exfiltration |
Anti-pattern phổ biến: mở ipBlock: 0.0.0.0/0 cho egress vì “service cần gọi nhiều API ngoài”. Điều này vô hiệu hóa kiểm soát exfiltration. Lời giải đúng là liệt kê FQDN cần thiết, hoặc đặt một egress proxy (Secure Web Proxy) và chỉ cho egress tới proxy đó.
Thứ tự đánh giá và sự vắng mặt của “deny rule”
Vì NetworkPolicy chỉ có allow và mang tính cộng dồn, không có khái niệm thứ tự ưu tiên giữa các policy như firewall truyền thống. Kết quả cuối cùng là hợp (union) của tất cả allow áp dụng cho Pod đó ở chiều đang xét. Một Pod được phép nếu bất kỳ policy nào cho phép; nó bị chặn chỉ khi bị ít nhất một policy chọn ở chiều đó và không policy nào cho phép kết nối cụ thể này.
Hệ quả thực chiến: bạn không thể viết một policy “cho phép tất cả trừ X” bằng NetworkPolicy chuẩn (không có deny). Muốn vậy phải đảo logic: default-deny rồi chỉ allow những gì hợp lệ. Đây là khác biệt tư duy quan trọng với người quen firewall có deny rule và priority. (Trên một số nền tảng, các CRD mở rộng như CiliumNetworkPolicy bổ sung biểu đạt phong phú hơn, nhưng nguyên tắc cộng dồn của NetworkPolicy chuẩn thì không đổi.)
Kiểm thử và xác minh policy trước khi enforce
Một policy sai có thể âm thầm chặn traffic hợp lệ. Quy trình xác minh tối thiểu:
- Kiểm tra bằng kết nối thực: từ một Pod nguồn,
kubectl execthử kết nối tới đích kỳ vọng (cho phép và bị chặn) để xác nhận policy hành xử đúng cả hai chiều mong muốn. - Đọc log allow/deny (Dataplane V2): bật logging, tạo traffic mẫu, xác nhận quyết định khớp ý định.
- Hubble (file 6): quan sát verdict theo policy để thấy chính xác policy nào tác động flow nào.
- Rà phụ thuộc ẩn trước khi áp default-deny (xem phần phụ thuộc ẩn bên dưới).
Đừng coi “áp policy xong” là “policy đúng”. Chỉ khi đã xác minh bằng traffic thực và log, policy mới đáng tin ở production.
Phụ lục: thiết kế isolation model chịu được thay đổi
Bốn tầng isolation thường gặp
- Namespace boundary: tách team/tenant theo namespace, dùng
namespaceSelectorđể kiểm soát cross-namespace. - Tier boundary: web → api → db, chỉ cho phép chiều hợp lệ giữa các tầng.
- Egress control: mặc định chặn egress, chỉ mở DNS + các FQDN/đích cần thiết.
- Sensitive workload lockdown: payments/identity/data có default-deny chặt nhất.
Thiết kế theo tầng giúp policy dễ suy luận và audit, thay vì một mớ rule rời rạc khó kiểm chứng.
Vì sao label governance là vấn đề bảo mật
Trên mô hình identity (Dataplane V2), quyền truy cập mạng gắn với label. Nghĩa là ai có quyền gắn label cho Pod thì gián tiếp có quyền thay đổi “workload đó được nói chuyện với ai”. Vì vậy:
- Kiểm soát ai được sửa label/template Deployment.
- Dùng admission policy (Chương 10) để chặn label nhạy cảm bị lạm dụng.
- Coi label như một phần của attack surface, không chỉ là metadata tổ chức.
Quy trình rollout policy an toàn
- Viết policy ở chế độ quan sát trước: bật logging, chưa enforce chặt (hoặc enforce ở namespace staging).
- Phân tích log allow/deny để phát hiện đường hợp lệ bị bỏ sót (đặc biệt DNS, health check, sidecar).
- Mở các đường hợp lệ còn thiếu.
- Áp default-deny và enforce ở production.
- Giữ
deny.logbật để phát hiện regression khi thêm workload mới.
Bỏ qua bước quan sát là nguyên nhân số một khiến rollout NetworkPolicy gây sự cố: bạn chặn nhầm một đường phụ thuộc ẩn mà không biết.
Những phụ thuộc ẩn hay bị chặn nhầm
- DNS (CoreDNS, cổng 53).
- Health check/probe từ kubelet (thường là traffic nội node).
- Health check từ load balancer/NEG (dải health check của Google).
- Metrics scraping (Prometheus/Managed collection).
- Sidecar (service mesh, logging agent) cần egress riêng.
Lập danh sách các phụ thuộc này trước khi áp default-deny giúp tránh phần lớn sự cố.
Ghi nhớ cốt lõi
- NetworkPolicy chỉ allow, không deny; trạng thái deny tạo bằng cách “chọn mà không cho phép”.
- Default-allow tới khi Pod bị chọn → phải có default-deny chủ động ở nơi nhạy cảm.
- Calico thực thi theo IP (nhạy churn); Dataplane V2 theo identity (ổn định) + FQDN + logging.
- Luôn mở DNS khi default-deny egress; luôn rà phụ thuộc ẩn trước khi enforce.
- Label là bề mặt bảo mật trong mô hình identity — quản trị label nghiêm túc.
Một lưu ý cuối về phạm vi của NetworkPolicy
NetworkPolicy kiểm soát traffic ở L3/L4 (và FQDN/L7 hạn chế với CRD mở rộng), nhưng nó không thay thế các lớp bảo mật khác: nó không xác thực danh tính ứng dụng (đó là việc của mTLS/service mesh), không chống được kẻ tấn công đã có quyền trên chính Pod được phép kết nối, và không bảo vệ dữ liệu trên đường truyền (cần TLS). Hãy coi NetworkPolicy là một lớp trong mô hình phòng thủ nhiều tầng (defense-in-depth): nó giới hạn ai nói chuyện được với ai ở tầng mạng, còn xác thực, mã hóa và kiểm soát quyền ở tầng ứng dụng vẫn là trách nhiệm riêng. Thiết kế bảo mật mạnh nhất kết hợp NetworkPolicy default-deny với mTLS và admission control (Chương 10, 12) để mỗi tầng bù đắp giới hạn của tầng kia.