Chia văn phòng thành các phòng ban có biển tên khác nhau không tạo ra ổ khoá giữa chúng — ai cũng đi lại tự do, cái biển chỉ để gọi tên. Muốn có ranh giới thật thì phải lắp cửa. Rất nhiều người dựng namespace trong Kubernetes rồi tin mình đã có bức tường, trong khi thứ họ có mới chỉ là tấm biển tên. Bài này đo xem ranh giới đó có tồn tại không, và cách thật sự lắp cửa.
Mặc định: mọi pod gọi được mọi pod
Pod pr ở namespace default:
gọi a.default -> 200
gọi web.kh -> 200 (namespace KHÁC)
Không có ranh giới mạng nào giữa namespace. Namespace chia không gian tên, không chia không gian mạng. Một pod bị chiếm quyền trong namespace dev gọi thẳng được cơ sở dữ liệu trong namespace prod, chỉ cần biết tên dịch vụ — và tên đó đoán được. Đây là mặc định có chủ ý (mọi thứ chạy được ngay), nhưng nó khiến NetworkPolicy là thứ nên thêm sớm chứ không phải để sau.
Luật 1: chặn hết
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: chan-het, namespace: kh}
spec:
podSelector: {}
policyTypes: [Ingress]
gọi a.default -> 200 namespace khác không ảnh hưởng
gọi web.kh -> 000 chặn sạch
podSelector: {} chọn mọi pod trong namespace. Không có mục ingress nào nghĩa là không cho phép gì cả. Đây là chính sách nền đáng có ở mọi namespace: chặn hết trước, rồi mở dần. Nó áp cho namespace kh mà không đụng gì tới default.
Luật 2: mở cho đúng một nhãn
spec:
podSelector: {matchLabels: {app: web}}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: default}}
podSelector: {matchLabels: {duoc: "co"}}
ports: [{protocol: TCP, port: 80}]
pod CHƯA có nhãn duoc=co -> 000
sau khi gán nhãn duoc=co -> 200
Gán một nhãn là đủ để mở đường — và gỡ nhãn là đủ để đóng lại. Không cần khởi động lại gì.
Một chi tiết cú pháp quan trọng: namespaceSelector và podSelector nằm trong cùng một mục của danh sách from nghĩa là VÀ — namespace phải khớp và pod phải khớp. Tách thành hai mục riêng thì thành HOẶC, và ý nghĩa khác hẳn:
# VÀ — pod có nhãn duoc=co, trong namespace default
- from:
- namespaceSelector: {...}
podSelector: {...}
# HOẶC — mọi pod trong namespace default, HOẶC mọi pod có nhãn duoc=co ở bất kỳ đâu
- from:
- namespaceSelector: {...}
- podSelector: {...}
Một dấu gạch đầu dòng. Đây là lỗi hay gặp nhất khi viết NetworkPolicy, và nó mở rộng quyền chứ không thu hẹp — nên nó không bao giờ làm gì hỏng và bạn không phát hiện ra.
Luật 3: giới hạn cổng
ports: [{protocol: TCP, port: 80}]
gọi thẳng podIP:80 -> 200
gọi thẳng podIP:8080 -> 000
Chú ý: tôi gọi thẳng podIP, không qua Service. Chính sách vẫn áp. Đó là vì NetworkPolicy được thực thi ở tầng mạng của node (bởi CNI), không ở tầng Service. Nó chặn gói tin thật, nên không có đường vòng nào. Hệ quả ngược lại cũng đúng: NetworkPolicy không biết gì về Service. Bạn khai cổng của pod (targetPort), không phải cổng của Service.
Ba điều dễ hiểu sai
Chính sách cộng dồn, không ghi đè. Hai chính sách cùng chọn một pod thì pod đó được phép làm hợp của hai tập luật. Không có thứ tự ưu tiên, không có "từ chối" — NetworkPolicy chỉ có cho phép. Nghĩa là bạn không thể viết một chính sách để chặn thứ mà chính sách khác đã cho phép. Muốn chặn thì phải sửa hoặc xoá chính sách kia.
Có ít nhất một chính sách chọn tới pod → mọi thứ không được nêu bị chặn. Không chính sách nào chọn tới → mở hoàn toàn. Đây là chỗ hay gây bất ngờ: thêm một chính sách rất hẹp cho một pod sẽ chặn mọi thứ khác của pod đó, kể cả những luồng đang chạy.
policyTypes quyết định chiều. Chặn Ingress không chặn Egress — pod vẫn gọi ra ngoài bình thường. Muốn chặn cả hai thì phải khai cả hai. Và chặn Egress khó hơn nhiều vì phải nhớ mở cho DNS:
egress:
- to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}}]
ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]
Quên dòng này là pod không phân giải được tên nào, và triệu chứng trông giống hệt sự cố DNS chứ không giống sự cố chính sách.
CNI phải hỗ trợ
NetworkPolicy là một object của Kubernetes, nhưng việc thực thi là của CNI. Với plugin không hỗ trợ, bạn tạo chính sách và nó không có tác dụng gì — không lỗi, không cảnh báo. Đây là kiểu hỏng im lặng nguy hiểm: bạn nghĩ đã chặn, thực ra chưa. Kiểm bằng cách thử thật: tạo chính sách chặn hết rồi gọi thử. Nếu vẫn thông, CNI của bạn không thực thi nó.
Toàn bộ phép đo trong bài này chạy trên kindnet — CNI mặc định của kind — và nó có thực thi. Tôi kiểm lại ba lần để chắc chắn:
có chính sách, pod chưa có nhãn -> 000
gán nhãn duoc=co -> 200
xoá hết chính sách, gỡ nhãn -> 200
Điều này đáng nói vì kindnet từng không hỗ trợ NetworkPolicy, và rất nhiều tài liệu vẫn viết như vậy. Bản trong kindest/node:v1.34.3 thì có. Đừng tin tài liệu — hãy chạy phép thử ở cuối bài trên chính cụm của bạn.
Bộ chính sách nền đáng có
Cho mỗi namespace:
# 1. chặn hết vào
kind: NetworkPolicy
metadata: {name: 00-chan-vao}
spec: {podSelector: {}, policyTypes: [Ingress]}
---
# 2. cho phép trong cùng namespace gọi nhau
kind: NetworkPolicy
metadata: {name: 01-cung-namespace}
spec:
podSelector: {}
policyTypes: [Ingress]
ingress: [{from: [{podSelector: {}}]}]
Hai chính sách này cho bạn ranh giới namespace thật, với chi phí vận hành gần như bằng không. Mọi luồng đi qua namespace sau đó phải được khai rõ — và đó chính là điều bạn muốn.
Mẫu số chung: gom nhóm không phải cách ly, và lỗi nới-lỏng thì câm lặng
Bài này gói ba bài học về ranh giới, đúng ở mọi hệ. Thứ nhất, và cốt lõi: một cấu trúc để gom nhóm không phải một cấu trúc để cách ly — nó cho bạn cái tên, không cho bạn bức tường. Namespace nhóm tài nguyên lại nhưng không dựng hàng rào tin cậy nào giữa chúng; muốn hàng rào thì phải thêm cơ chế thực thi riêng. Cùng nhầm lẫn khắp nơi: một package trong Java tổ chức mã chứ không phải kiểm soát truy cập (package-private là hàng rào rất yếu); một thư mục không phải một ranh giới quyền; một schema CSDL nhóm bảng nhưng GRANT mới là cái phân quyền; tên một VLAN không phải một ACL. Nguyên tắc: một cái nhãn để sắp xếp không bao giờ tự nó là một cái khoá — đừng nhầm ranh giới tổ chức với ranh giới bảo mật.
Thứ hai: một thay đổi nới lỏng một hạn chế gần như không bao giờ báo lỗi, nên nó ẩn mình. Đổi VÀ thành HOẶC bằng một dấu gạch đầu dòng mở rộng quyền — không gì hỏng, không ai nhận ra. Đây là bất đối xứng cơ bản trong gỡ lỗi: lỗi siết chặt lộ ra ngay (có thứ ngừng chạy), còn lỗi nới lỏng chỉ tìm thấy bằng cách rà soát chủ động — chmod 777 "chạy được", một CORS quá rộng, một mẫu regex quá rộng, một WHERE bị nới trả về nhiều dòng hơn. Với mọi thứ liên quan tới quyền và an toàn, đừng đợi nó vỡ mới sửa — phải đi kiểm chỗ thừa quyền, vì thừa quyền không tự vỡ.
Thứ ba, ngắn: một object khai báo không làm gì nếu không có bộ thực thi đọc nó. NetworkPolicy mà CNI không hỗ trợ chỉ là văn bản — cùng họ với một cấu hình chẳng ai đọc, một luật lint chưa nối vào CI. Cách duy nhất chắc chắn là thử thật: tạo luật chặn rồi gọi xem có bị chặn không.
Mẫu số chung: đừng nhầm một cấu trúc gom-nhóm với một hàng rào cách-ly; chủ động rà soát các lỗi nới-lỏng vì chúng không tự báo; và luôn kiểm bằng thử-thật rằng luật của bạn có một bộ thực thi thật sự chạy.
Muốn tự kiểm CNI của bạn có thực thi NetworkPolicy không:
kubectl create ns thu
kubectl -n thu create deployment web --image=nginx:alpine
kubectl -n thu expose deployment web --port=80
kubectl -n thu wait --for=condition=available deployment/web
kubectl run t --image=curlimages/curl --restart=Never --rm -it -- \
curl -s -m 4 -o /dev/null -w "truoc: %{http_code}\n" http://web.thu/
kubectl -n thu apply -f - <<'Y'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: chan}
spec: {podSelector: {}, policyTypes: [Ingress]}
Y
kubectl run t2 --image=curlimages/curl --restart=Never --rm -it -- \
curl -s -m 4 -o /dev/null -w "sau: %{http_code}\n" http://web.thu/
Nếu cả hai đều 200, CNI của bạn đang bỏ qua NetworkPolicy — và mọi chính sách bạn đã viết chỉ là văn bản.
Phần sau đo CNI: cùng phép thử, kindnet và Calico chênh nhau gần hai lần thông lượng giữa node.