Skip to main content

7.3. Security - Image Security, Security Contexts, Network Policies and Custom Resources

Mục lục


1. Image Security

Ứng dụng đang chạy ổn với image nginx:alpine kéo thẳng từ Docker Hub. Rồi một hôm, đội bảo mật ra yêu cầu: mọi image chạy production phải nằm trong registry nội bộ, không được public ra ngoài. Bạn sửa Deployment, trỏ image sang myprivateregistry.com:5000/nginx:alpine, apply — và pod mới lập tức rơi vào ImagePullBackOff. Không phải sai tên image, cũng không phải registry sập. Vấn đề là Kubernetes chưa biết cách "đăng nhập" vào registry riêng đó để lấy image về.

Part 7.2 đã trả lời câu hỏi "ai được phép làm gì" bằng RBAC. Phần này đi tiếp sang một lớp phòng thủ khác: image nào được phép chạy, container chạy với quyền gì, và pod nào được phép nói chuyện với pod nào. Bắt đầu từ image.

Tên image nói lên điều gì?

Nhìn kỹ một cái tên image quen thuộc: nginx. Đây là image gì, và nó được pull từ đâu? Docker Hub và mọi registry tương thích đều dùng chung một quy ước đặt tên: <registry>/<account>/<repository>:<tag>. Phần nào cũng có giá trị mặc định nếu bạn không ghi ra.

nginx thực chất Docker hiểu thành docker.io/library/nginx:latest. library là account mặc định — nơi lưu các Docker Official Images, được một đội ngũ chuyên trách review và duy trì. docker.io là DNS name của registry mặc định, tức Docker Hub. Nếu bạn tự tạo account riêng và push image lên, tên sẽ có dạng <tên-của-bạn>/<repository> thay vì library/<repository>.

💡 Hình dung: Gọi tên image nginx giống gọi món "phở" ở quán quen — không cần nói rõ "phở của quán nào" vì mặc định ai cũng hiểu là quán quen thuộc (docker.io/library). Muốn ăn phở quán khác, phải nói rõ tên quán: <account>/nginx. Registry giống một khu ẩm thực chứa nhiều quán — Docker Hub là khu mặc định, nhưng hoàn toàn có thể mở "quán" ở một khu ẩm thực khác, ví dụ gcr.io (registry của Google, nơi lưu nhiều image dùng để chạy end-to-end test cho chính Kubernetes) hoặc một registry nội bộ tự host.

Mọi image công khai trên các registry này ai cũng pull được. Nhưng nếu ứng dụng nội bộ không nên public, giải pháp là host một private registry — tự triển khai, hoặc dùng registry riêng có sẵn từ AWS, Azure, GCP. Trên bất kỳ giải pháp nào, một repository có thể được đặt ở chế độ private, chỉ truy cập được bằng credential hợp lệ.

Kubernetes lấy credential để pull private image ở đâu?

Ở góc nhìn thuần Docker, muốn chạy container từ private image, bạn docker login vào registry, nhập credential, rồi mới docker run được. Nhưng trong Kubernetes, ai gõ docker login hộ bạn mỗi lần một pod mới cần pull image? Không ai cả — và đó chính là vấn đề cần giải quyết.

Image thực sự được pull bởi container runtime trên worker node, dưới sự điều phối của kubelet — không phải bởi kubectl hay api-server. Vậy kubelet cần credential, và Kubernetes cấp credential đó qua một Secret. Tạo một Secret loại docker-registry — một secret type dựng sẵn chuyên để lưu registry credential (server, username, password, email). Đặt tên Secret, ví dụ regcred, rồi tham chiếu tên đó trong pod spec qua field imagePullSecrets. Khi pod được tạo, kubelet đọc imagePullSecrets, lấy đúng Secret đó, và dùng nó để xác thực với registry trước khi pull.

Image Pull Flow qua Private RegistryPod specimagePullSecrets:regcredkubeletworker nodePrivate Registrymyregistry.com:5000Containerimage pulled, Running134Secret: regcreddockerconfigjson21) Pod cần image → 2) kubelet đọc Secret regcred → 3) đăng nhập registry → 4) pull thành công, chạy container.

💡 Hình dung: Secret loại docker-registry giống một chiếc thẻ ra vào kho hàng riêng. Pod không tự cầm thẻ — nó chỉ ghi tên thẻ cần dùng (imagePullSecrets: regcred). Đến lúc cần lấy hàng, kubelet mới đi lấy đúng thẻ đó và trình cho bảo vệ kho (registry) để được vào.

Lab: Cấu hình Pod pull image từ private registry

Môi trường có sẵn Deployment nginx-web đang chạy image công khai. Xuyên suốt demo dùng đúng tên Deployment và Secret này.

Xem image Deployment đang dùng.

kubectl describe deployment nginx-web | grep Image:

# Output minh họa
Image: nginx:alpine

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Không chỉ định registry, Kubernetes ngầm hiểu là pull từ docker.io/library/nginx. Giờ đổi sang registry riêng.

Sửa image sang private registry.

kubectl set image deployment/nginx-web nginx-web=myprivateregistry.com:5000/nginx:alpine

# Output
deployment.apps/nginx-web image updated

Rolling update kích hoạt ngay — pod mới được tạo, nhưng pod cũ vẫn giữ nguyên cho tới khi pod mới chuyển sang Ready.

kubectl get pods

# Output
NAME READY STATUS RESTARTS AGE
nginx-web-5f6d8c9b7d-abcde 1/1 Running 0 10m
nginx-web-7c9f4d8b6f-xyz12 0/1 ImagePullBackOff 0 20s

Pod mới kẹt ở ImagePullBackOff. kubectl describe pod cho biết chính xác vì sao.

kubectl describe pod nginx-web-7c9f4d8b6f-xyz12 | tail -4

# Output minh họa (rút gọn)
Warning Failed 10s kubelet Failed to pull image
"myprivateregistry.com:5000/nginx:alpine":
unauthorized: authentication required

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Đúng như dự đoán: registry riêng từ chối vì chưa có credential nào được gửi kèm. Không có cách nào pull được từ private registry mà thiếu credential — phải tạo Secret trước.

Tạo Secret chứa credential.

kubectl create secret docker-registry regcred \
--docker-server=myprivateregistry.com:5000 \
--docker-username=registry-user \
--docker-password='S3cretPass!' \
--docker-email=[email protected]

# Output
secret/regcred created

docker-registry là secret type chuyên cho registry credential — khác generic (dữ liệu tùy ý) và tls (certificate/key). Bốn giá trị trên được Kubernetes gói thành một file .dockerconfigjson, đúng định dạng file ~/.docker/config.json mà chính Docker CLI dùng sau khi docker login.

Gắn Secret vào Deployment qua imagePullSecrets.

kubectl edit deployment nginx-web

Thêm vào spec.template.spec, ngang hàng với containers:

spec:
template:
spec:
imagePullSecrets:
- name: regcred
containers:
- name: nginx-web
image: myprivateregistry.com:5000/nginx:alpine
kubectl get pods

# Output
NAME READY STATUS RESTARTS AGE
nginx-web-7c9f4d8b6f-xyz12 1/1 Running 0 8s

Lần này kubelet đọc imagePullSecrets, lấy credential từ regcred, xác thực thành công với registry, và pull image — đúng luồng đã minh họa ở diagram trên.

Image đã pull được. Nhưng thứ đang chạy bên trong container đó có quyền gì trên chính worker node? Để hiểu security context ở phần sau, cần lùi lại một tầng thấp hơn: cách Docker cô lập và giới hạn quyền của process bên trong container ngay từ đầu.


2. Kiến thức nền về Security trong Docker

Trước khi bàn security context trong Kubernetes, cần nắm một số kiến thức nền về security trong Docker. Nếu đã quen thuộc, có thể bỏ qua phần này.

Namespace cô lập process, không phải máy ảo riêng

Khác với máy ảo, container không tách biệt hoàn toàn khỏi host — container và host chia sẻ chung một kernel. Cái tạo ra cảm giác "cô lập" là Linux namespace: host có namespace riêng, mỗi container cũng có namespace riêng. Process bên trong container thực chất vẫn chạy trên chính host, chỉ là bên trong một namespace khác.

Ví dụ, chạy một container Ubuntu thực thi lệnh sleep 1h. Từ bên trong container, liệt kê process chỉ thấy đúng process sleep đó, với PID là 1 — vì trong namespace riêng của nó, đây là process đầu tiên. Nhưng từ chính host, liệt kê process sẽ thấy cùng process sleep đó xuất hiện với một PID khác hẳn (ví dụ 6879) — vì trên host, nó chỉ là một process bình thường trong hàng dài process khác. Cùng một process, hai PID khác nhau ở hai namespace khác nhau — đó là cách Docker cô lập container mà không cần ảo hóa toàn bộ hệ điều hành.

Root trong container không phải root trên host

Mặc định, Docker chạy process bên trong container dưới quyền root user. Điều này dẫn tới một câu hỏi đáng lo: root bên trong container có giống hệt root trên host không? Nếu có, một process trong container liệu có thể làm bất cứ điều gì root làm được trên toàn hệ thống — kể cả reboot host?

Không hẳn vậy. Docker giới hạn quyền của root-trong-container bằng Linux capabilities — một cơ chế chia nhỏ "siêu quyền" của root thành từng quyền lẻ, thay vì cấp trọn gói.

💡 Hình dung: Root trên host giống người giữ trọn bộ chìa khóa tòa nhà — mở được mọi cửa, mọi phòng, kể cả phòng máy chủ trung tâm. Root bên trong container giống một nhân viên bảo vệ chỉ được phát MỘT SỐ chìa khóa lẻ trong chùm đó (CHOWN, NET_BIND_SERVICE, SETUID...) — đủ để làm việc thường ngày, nhưng không có chìa khóa phòng máy chủ (SYS_ADMIN, SYS_TIME, reboot host). Muốn phát thêm chìa, phải xin rõ ràng bằng --cap-add; muốn tịch thu bớt, dùng --cap-drop.

Root user trên Linux mặc định có quyền gần như không giới hạn: sửa file và permission bất kỳ, tạo/kill process tùy ý, set UID/GID, bind vào network port, thực hiện thao tác ở tầng hệ thống như reboot hay chỉnh system clock. Mỗi khả năng này là một capability riêng biệt.

Docker mặc định chỉ cấp một tập capability giới hạn cho container — khoảng 14 quyền, trong đó có CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, SETUID, SETGID, SETPCAP, SETFCAP, NET_BIND_SERVICE, NET_RAW, SYS_CHROOT, MKNOD, AUDIT_WRITE. Đủ để hầu hết ứng dụng thông thường chạy trơn tru, nhưng không có các quyền nguy hiểm như SYS_ADMIN hay SYS_TIME.

Có ba cách thao tác với tập quyền này khi chạy container:

Thao tácÝ nghĩa
docker run --cap-add=<CAP>Cấp thêm một capability nằm ngoài tập mặc định
docker run --cap-drop=<CAP>Tước bớt một capability khỏi tập mặc định
docker run --privilegedBật toàn bộ capability, coi như tương đương root thật trên host — chỉ dùng khi thực sự cần, vì gần như xóa bỏ lớp cô lập vừa nói ở trên

Một cách khác để áp quyền user ngay từ khi build image là dùng chỉ thị USER trong Dockerfile — image build ra sẽ luôn chạy với UID đó, không cần ai nhớ truyền --user mỗi lần docker run.

Theo kinh nghiệm vận hành thực tế, phần lớn ứng dụng web hay backend thông thường chạy tốt với đúng tập capability mặc định này — chỉ cần đụng tới --cap-add khi ứng dụng thực sự cần thao tác hệ thống thấp, ví dụ một NTP daemon cần SYS_TIME để chỉnh đồng hồ. Nếu thấy mình phải --cap-add nhiều capability cho một ứng dụng "bình thường", đó thường là dấu hiệu nên xem lại kiến trúc hơn là xem lại danh sách capability.

Những khái niệm này — user chạy container, và tập capability được cấp — ánh xạ thẳng vào Kubernetes qua một field duy nhất: securityContext.


3. Security Contexts trong Kubernetes

Mọi thứ vừa bàn ở Docker — UID chạy container, capability nào được thêm/bớt — đều cấu hình được trong Kubernetes qua field securityContext. Điểm khác biệt: vì container luôn nằm trong Pod, securityContext có thể khai báo ở hai cấp — cấp Pod (áp dụng cho mọi container bên trong) và cấp container (chỉ áp dụng cho đúng container đó).

Khi nào dùng cấp nào? Quy tắc chỉ có một câu: cấu hình cụ thể hơn luôn thắng. Set runAsUser ở Pod nghĩa là "mặc định mọi container trong Pod này chạy với UID đó". Container nào tự khai báo securityContext riêng sẽ override giá trị đó, chỉ cho chính nó.

💡 Hình dung: Giống quy định trong một công ty — ban giám đốc ban hành chính sách chung áp dụng cho toàn công ty (Pod-level securityContext), nhưng trưởng phòng bảo mật có thể ra thêm quy định riêng chặt hơn cho phòng mình (container-level securityContext). Phòng nào không có quy định riêng thì mặc nhiên theo chính sách chung của công ty.

apiVersion: v1
kind: Pod
metadata:
name: multi-pod
spec:
securityContext:
runAsUser: 1001
containers:
- name: web
image: nginx
securityContext:
runAsUser: 1002
capabilities:
add: ["SYS_TIME"]
- name: sidecar
image: busybox
command: ["sleep", "3600"]
securityContext: cụ thể hơn luôn thắngPod: multi-podspec.securityContextrunAsUser: 1001 (mặc định)container: webcó riêng securityContextrunAsUser: 1002→ chạy với UID 1002container: sidecarkhông set securityContextriêng→ kế thừa UID 1001overrideinheritCấu hình ở container luôn override cấu hình ở Pod — không có container-level nào thì mới dùng lại giá trị Pod.

Lab: Cấu hình Security Context cho Pod và container

Xác định user đang chạy sleep process trong Pod ubuntu-sleeper. Muốn biết đang chạy dưới user nào trên Linux, dùng whoami:

kubectl exec ubuntu-sleeper -- whoami

# Output
root

Mặc định, không set gì, container chạy dưới root user.

Sửa Pod ubuntu-sleeper để chạy với UID 1010. Lấy cấu hình hiện tại ra file trước, vì chỉ cần đổi đúng một chỗ:

kubectl get pod ubuntu-sleeper -o yaml > ubuntu-sleeper.yaml

Thêm runAsUser vào securityContext:

spec:
securityContext:
runAsUser: 1010

Pod đang chạy không cho phép sửa securityContext trực tiếp bằng kubectl edit, nên phải xóa và tạo lại:

kubectl delete pod ubuntu-sleeper --force
kubectl apply -f ubuntu-sleeper.yaml
kubectl exec ubuntu-sleeper -- whoami

# Output minh họa
I have no name!

(Output minh họa — với UID không tồn tại trong /etc/passwd của image, whoami không resolve được tên user và trả về thông báo này; đây là hành vi thật của whoami/getent, không phải lỗi.)

Xác định user chạy container web trong multi-pod.yaml. File này định nghĩa securityContext ở cả hai cấp — đúng tình huống ở diagram trên. Pod-level set runAsUser: 1001, nhưng container web override riêng thành runAsUser: 1002. Container-level cụ thể hơn nên thắng. Đáp án: UID 1002.

Container sidecar chạy với user nào? Container này không khai báo securityContext riêng, nên nó rơi về dùng giá trị Pod-level. Đáp án: UID 1001.

Cập nhật ubuntu-sleeper để chạy lại dưới root, kèm capability SYS_TIME. Mở file cấu hình, xóa hai dòng securityContext cấp Pod (để quay về mặc định root), rồi thêm capability ở cấp container:

spec:
containers:
- name: ubuntu-sleeper
image: ubuntu
command: ["sleep", "3600"]
securityContext:
capabilities:
add: ["SYS_TIME"]
kubectl delete pod ubuntu-sleeper --force
kubectl apply -f ubuntu-sleeper.yaml
kubectl get pod ubuntu-sleeper

# Output
NAME READY STATUS RESTARTS AGE
ubuntu-sleeper 1/1 Running 0 6s

Thêm capability NET_ADMIN. Chỉ cần thêm một phần tử vào danh sách add:

capabilities:
add: ["SYS_TIME", "NET_ADMIN"]

Xóa pod, apply lại, kiểm tra Running — xong bài lab.

Pod Security Admission — kiểm soát baseline toàn namespace

securityContext kiểm soát từng Pod một, do người viết YAML tự quyết định. Nhưng nếu bạn là cluster admin và muốn bắt buộc cả một namespace không ai được chạy container --privileged, dù họ có nhớ set securityContext hay không, thì sao?

Đó là bài toán của Pod Security Admission (PSA) — cơ chế enforce theo namespace, dùng để thay thế PodSecurityPolicy (PSP) đã bị loại bỏ hoàn toàn khỏi Kubernetes từ bản 1.25. PSA không cần tạo object riêng như PSP từng làm — chỉ cần gắn label lên Namespace, chọn một trong ba Pod Security Standards: privileged (không giới hạn), baseline (chặn các lỗ hổng leo thang quyền phổ biến, ví dụ cấm hostNetwork/hostPID, cấm container privileged), hoặc restricted (khắt khe nhất, bắt buộc runAsNonRoot, drop toàn bộ capability mặc định).

apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted

Với label này, mọi Pod tạo trong namespace production vi phạm chuẩn restricted (ví dụ chạy root, không drop capability) sẽ bị api-server từ chối thẳng ngay từ bước admission — không cần chờ ai review YAML bằng mắt.

securityContext kiểm soát container chạy với quyền gì trên chính node. Nhưng còn ai được phép nói chuyện với ai qua mạng? Đó là lãnh địa của NetworkPolicy.


4. Kiến thức nền về Networking và Security cho Network Policies

Trước khi tạo NetworkPolicy, cần nắm rõ Kubernetes networking mặc định hoạt động ra sao — nếu không, rất dễ tạo một policy tưởng đâu chặn đúng traffic cần chặn, nhưng thực ra chặn nhầm hoặc không chặn gì cả.

Xét một ví dụ đơn giản: web server phục vụ front-end, một app server phục vụ back-end API, và một database server. User gửi request tới web server ở port 80. Web server gọi tiếp app server ở port 5000. App server fetch dữ liệu từ database ở port 3306, rồi trả kết quả ngược lại theo đúng đường đã đi.

Có hai hướng traffic cần phân biệt: ingress (đi vào) và egress (đi ra) — luôn nhìn từ phía component đang xét, và chỉ quan tâm hướng traffic bắt nguồn, không quan tâm response đi ngược lại. Với web server: request từ user là ingress, request nó gửi tới app server là egress. Response trả về không tính, vì đó không phải traffic mới bắt nguồn — nó chỉ là phần trả lời của một kết nối ingress/egress đã được cho phép.

Vậy hệ thống 3 tầng này cần đúng 5 rule để hoạt động: ingress port 80 tại web server, egress từ web tới port 5000 trên app server, ingress port 5000 tại app server, egress từ app tới port 3306 trên database, và ingress port 3306 tại database server.

Mặc định: mọi Pod nói chuyện được với mọi Pod

Trong một Kubernetes cluster, mỗi node, mỗi Pod, mỗi Service đều có địa chỉ IP riêng. Một trong các yêu cầu bắt buộc của mọi giải pháp networking Kubernetes là: Pod phải giao tiếp được với nhau mà không cần cấu hình route thủ công — mọi Pod đều nằm trên cùng một mạng ảo trải khắp cluster.

Hệ quả trực tiếp: Kubernetes mặc định áp dụng một all-allow rule — bất kỳ Pod nào cũng reach được bất kỳ Pod hay Service nào khác trong cluster, không cần khai báo gì thêm.

💡 Hình dung: Mạng Kubernetes mặc định giống một văn phòng open-space không khóa cửa phòng nào — nhân viên phòng nào cũng đi lại tự do sang phòng khác, không ai chặn ai.

Áp use case web-API-database vào Kubernetes: mỗi tầng là một Pod, có Service đứng trước để giao tiếp ổn định. Mặc định, cả ba Pod đều nói chuyện được với nhau — kể cả web Pod với database Pod, dù chẳng có lý do nghiệp vụ nào để hai bên đó nói chuyện trực tiếp.

Giả sử đội bảo mật yêu cầu: web server không được phép kết nối thẳng tới database. Đây chính là lúc cần NetworkPolicy — một object Kubernetes khác, giống Pod hay Service, dùng để giới hạn traffic được phép tới/từ một nhóm Pod cụ thể. Liên kết NetworkPolicy với Pod bằng đúng kỹ thuật đã quen: label và selector. Một khi policy được tạo và áp lên một Pod, mọi traffic không khớp rule sẽ bị chặn — và điều này chỉ ảnh hưởng đúng Pod mà policy áp lên, các Pod khác không bị đụng tới.

Cách viết một NetworkPolicy cụ thể, và những selector mạnh hơn label thường (namespace, dải IP), sẽ đi sâu ở phần tiếp theo.


5. Network Policies chi tiết

Web → API → DB: trước và sau khi áp NetworkPolicyMặc định: allow-allPod: webPod: apiPod: dbweb→db OKSau khi áp db-policyPod: webPod: apiPod: dbport 3306 OKweb→dbBLOCKEDNetworkPolicy chỉ khoá đúng Pod nó áp lên (db) — web và api chưa bị policy nào áp thì vẫn allow-all như cũ.

Yêu cầu cụ thể: bảo vệ database Pod, chỉ cho phép truy cập từ API Pod trên đúng port 3306 — không đụng tới web Pod hay API Pod, hai Pod đó vẫn allow-all như cũ. Đúng như diagram trên: NetworkPolicy không phải một "tường lửa toàn cluster" — nó chỉ khóa cửa đúng căn phòng nó được gắn vào.

Xây dựng rule: chỉ cần Ingress, không cần Egress

Bước đầu, liên kết NetworkPolicy với database Pod bằng podSelector + matchLabels, ví dụ khớp label role: db. Ngay khi liên kết, mọi traffic ra/vào Pod này bị chặn — kể cả traffic hợp lệ từ API Pod. Cần thêm rule để mở lại đúng phần cần thiết.

Câu hỏi tiếp theo: cần khai báo Ingress, Egress, hay cả hai? Luôn nhìn từ góc của Pod đang được bảo vệ. Từ góc DB Pod, traffic từ API Pod gửi tới là ingress — traffic đi vào. Còn response DB trả về cho API thì sao, có cần một rule egress riêng không? Không. Một khi ingress traffic đã được allow, response của chính traffic đó tự động được phép quay lại — không cần khai báo thêm. Vậy trường hợp này chỉ cần đúng một Ingress rule.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: api
ports:
- protocol: TCP
port: 3306

from định nghĩa traffic được phép đến từ đâu — ở đây là Pod có label role: api. ports định nghĩa traffic được phép đến cổng nào trên DB Pod — 3306/TCP. Policy này chặn mọi traffic khác tới DB Pod, chỉ chừa đúng một lối đi.

Ba loại selector dưới from/to, và cái bẫy khi kết hợp chúng

Thực tế, from (trong ingress) và to (trong egress) hỗ trợ ba loại selector:

  • podSelector — chọn theo label của Pod, như ví dụ trên.
  • namespaceSelector — chọn theo label gắn trên Namespace. Hữu ích khi có nhiều Pod cùng label ở các namespace khác nhau (dev/test/prod), và bạn chỉ muốn allow đúng namespace prod.
  • ipBlock — chọn theo dải địa chỉ IP (CIDR), dùng khi nguồn traffic không phải một Pod trong cluster — ví dụ một backup server chạy bên ngoài, có IP 192.168.5.10.

Ba selector này kết hợp với nhau theo hai cách cho ra hai kết quả hoàn toàn khác nhau — và đây chính là chỗ dễ mắc lỗi nhất khi viết NetworkPolicy trong thực tế.

Giả sử bạn viết:

ingress:
- from:
- podSelector:
matchLabels:
app: api
namespaceSelector:
matchLabels:
env: prod

Hai selector nằm chung một phần tử trong list from (không có dấu - riêng trước namespaceSelector) — đây là phép AND: nguồn traffic phải vừa có label app: api, vừa nằm trong namespace có label env: prod, thiếu một trong hai là bị chặn.

Giờ chỉ cần thêm một dấu gạch ngang:

ingress:
- from:
- podSelector:
matchLabels:
app: api
- namespaceSelector:
matchLabels:
env: prod

Cùng những dòng đó, nhưng giờ là hai phần tử riêng trong list from — thành phép OR: traffic khớp label app: api (ở BẤT KỲ namespace nào) ĐƯỢC allow, HOẶC traffic từ BẤT KỲ Pod nào trong namespace env: prod cũng được allow. Phạm vi allow rộng ra rất nhiều so với ý định ban đầu, chỉ vì lệch một dấu gạch ngang.

Nếu bạn đang review một NetworkPolicy production và thấy nhiều selector xếp trong cùng một rule from, việc đầu tiên nên làm là đếm xem chúng nằm chung một phần tử list (AND) hay tách thành nhiều phần tử (OR) — đây là lỗi cấu hình âm thầm, không có warning nào từ Kubernetes, và hậu quả là mở toang đúng thứ bạn định khóa.

Egress: khi chính Pod bên trong là bên khởi tạo kết nối ra ngoài

Đảo tình huống lại: thay vì backup server chủ động kéo dữ liệu, giờ chính DB Pod chủ động đẩy backup ra một server bên ngoài. Traffic bắt nguồn từ DB Pod, hướng ra ngoài — đây là egress, và selector to dùng thay cho from, còn lại cấu trúc tương tự:

policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 192.168.5.10/32
ports:
- protocol: TCP
port: 80

Lab: Xây dựng NetworkPolicy bảo vệ payroll và database

Môi trường lab có 4 Pod: external, internal, payroll, và db (MySQL), cùng Service tương ứng cho mỗi Pod — payroll và Service external/internal đều lắng nghe port 8080, db lắng nghe 3306.

Đếm số NetworkPolicy đang có.

kubectl get netpol

# Output
NAME POD-SELECTOR AGE
payroll-policy name=payroll 10m

Có đúng một policy, tên payroll-policy, áp lên Pod có label name=payroll.

Xem policy này đang kiểm soát loại traffic nào.

kubectl describe netpol payroll-policy

# Output minh họa (rút gọn)
PodSelector: name=payroll
Allowing ingress traffic:
To Port: 8080/TCP
From:
PodSelector: name=internal
Not affecting egress traffic
Policy Types: Ingress

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Dòng Not affecting egress traffic nghĩa là không có rule egress nào — mọi traffic từ payroll Pod đi ra ngoài vẫn allow-all bình thường. Chỉ ingress bị kiểm soát, và chỉ cho phép đúng Pod có label name=internal gọi vào port 8080.

Kiểm tra bằng connectivity test thật. Từ ứng dụng internal-facing, gọi payroll service ở port 8080:

kubectl exec internal -- curl -s -o /dev/null -w "%{http_code}" payroll-service:8080

# Output minh họa
200

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Từ external Pod, cùng lệnh đó:

kubectl exec external -- curl -s -o /dev/null -w "%{http_code}" --max-time 5 payroll-service:8080

# Output minh họa
000
command terminated with exit code 28

(Output minh họa — exit code 28 của curl là timeout; đúng hành vi khi packet bị policy âm thầm drop, không có response nào trả về để nhận mã lỗi HTTP.)

Đúng như payroll-policy quy định: internal truy cập được, external bị chặn hoàn toàn — kể cả ping (ICMP) cũng không qua được, vì rule chỉ mở đúng TCP/8080, không mở toàn bộ giao thức.

Giới hạn ngược lại: chỉ cho internal Pod egress tới payroll và DB, chặn phần còn lại. Tạo policy mới internal-policy, áp lên internal, chỉ khai báo Egress, gồm hai rule tới hai đích:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: internal-policy
namespace: default
spec:
podSelector:
matchLabels:
name: internal
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
name: payroll
ports:
- protocol: TCP
port: 8080
- to:
- podSelector:
matchLabels:
name: mysql
ports:
- protocol: TCP
port: 3306
kubectl apply -f internal-policy.yaml
kubectl describe netpol internal-policy

# Output minh họa (rút gọn)
PodSelector: name=internal
Allowing egress traffic:
To Port: 8080/TCP To: PodSelector: name=payroll
To Port: 3306/TCP To: PodSelector: name=mysql
Policy Types: Egress

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Hai rule độc lập trong egress hoạt động theo OR: internal Pod gọi được payroll (8080) HOẶC mysql (3306) — mọi egress khác, kể cả tới external Pod, giờ bị chặn.

Chọn CNI plugin nào để NetworkPolicy thực sự có tác dụng?

NetworkPolicy không tự thực thi — nó cần một network plugin (CNI) hỗ trợ đọc và áp policy đó. Tạo NetworkPolicy trên một cluster dùng plugin không hỗ trợ vẫn thành công về mặt API — object vẫn lưu vào etcd — nhưng không hề có tác dụng gì, và Kubernetes không báo lỗi hay cảnh báo nào về việc đó.

Danh sách plugin hỗ trợ NetworkPolicy thay đổi theo thời gian, nên luôn kiểm tra tài liệu chính thức của Kubernetes trước khi chọn. Tính đến hiện tại, các lựa chọn được liệt kê chính thức và duy trì tích cực gồm Calico (phổ biến nhất, hỗ trợ đầy đủ NetworkPolicy cùng policy mở rộng riêng), Cilium (dựa trên eBPF, thêm policy tầng L7 và observability qua Hubble), kube-router, và Antrea.

Flannel vẫn là ngoại lệ đáng nhớ: bản thân nó chỉ lo định tuyến Layer 3 overlay, không có NetworkPolicy controller đi kèm — tạo NetworkPolicy trên cluster chỉ dùng Flannel thuần sẽ không có tác dụng gì. Giải pháp phổ biến là chạy Flannel song song với Calico, dùng Calico riêng cho việc enforce policy — tổ hợp này thường được gọi là Canal.

Weave Net, từng là lựa chọn phổ biến hỗ trợ NetworkPolicy, đã ngừng phát triển và repository chính thức đã được archive. Romana cũng ở tình trạng tương tự, không còn hoạt động phát triển đáng kể từ nhiều năm. Cả hai không còn nên được cân nhắc cho cluster mới.

Theo kinh nghiệm triển khai thực tế, Calico vẫn là lựa chọn an toàn nhất cho một cluster mới không có yêu cầu đặc biệt — tài liệu phong phú, cộng đồng lớn, và ít bất ngờ khi nâng cấp version. Cilium đáng cân nhắc hơn khi thực sự cần observability sâu (Hubble) hoặc policy tầng L7, nhưng đòi hỏi hiểu biết eBPF nhất định khi debug sự cố.

Đã xong hai mảng lớn nhất của phần Security: image và quyền chạy container (mục 1–3), và ai được nói chuyện với ai qua mạng (mục 4–5). Trước khi sang phần mở rộng Kubernetes bằng CRD, có một cặp command-line tool nhỏ giúp việc thao tác hằng ngày với namespace/cluster đỡ vất vả hơn nhiều.


6. Kubectx và Kubens - Command Line Utilities

Xuyên suốt tài liệu này, bạn đã phải làm việc với nhiều namespace khác nhau trong môi trường lab. Trong một cluster "sống" thực tế, tình huống còn khắc nghiệt hơn nhiều: thường xuyên chuyển đổi giữa hàng chục namespace, có khi giữa nhiều cluster khác nhau. Gõ kubectl config use-context ... hay kubectl config set-context --current --namespace=... mỗi lần rất dễ nhầm và mất thời gian. Đó là lý do kubectxkubens tồn tại.

Tham khảo: ahmetb/kubectx.

Kubectx giúp chuyển đổi giữa các context mà không cần gõ lệnh kubectl config dài dòng — đặc biệt hữu ích khi làm việc với nhiều cluster cùng lúc.

sudo git clone https://github.com/ahmetb/kubectx /opt/kubectx
sudo ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx

Kubens làm việc tương tự nhưng cho namespace, cài từ cùng repo:

sudo git clone https://github.com/ahmetb/kubectx /opt/kubectx
sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens
LệnhCông dụng
kubectxLiệt kê tất cả context hiện có
kubectx <context-name>Chuyển sang context khác
kubectx -Quay lại context vừa dùng trước đó
kubectx -cXem context hiện tại
kubens <namespace-name>Chuyển namespace mặc định của context hiện tại
kubens -Quay lại namespace vừa dùng trước đó

Đến đây đã đủ công cụ để vận hành ngày qua ngày với các resource sẵn có của Kubernetes. Phần còn lại của bài chuyển sang một câu hỏi khác hẳn: nếu Kubernetes không có sẵn loại resource bạn cần thì sao?


7. Custom Resource Definitions (CRD)

Trước khi bàn custom resource, nhắc lại resource có sẵn hoạt động ra sao. Tạo một Deployment, Kubernetes lưu nó vào etcd, rồi một controller — cụ thể là Deployment controller, built-in sẵn — liên tục theo dõi resource đó và thực hiện thay đổi cần thiết trên cluster: tạo ReplicaSet, ReplicaSet lại tạo đúng số Pod đã khai báo. Toàn bộ resource quen thuộc — ReplicaSet, Deployment, Job, CronJob, StatefulSet, Namespace — đều có controller riêng vận hành theo đúng cơ chế đó.

Giờ thử một ý tưởng khác hẳn: muốn Kubernetes quản lý một object hoàn toàn không có sẵn, ví dụ FlightTicket để đặt vé máy bay — chỉ để minh họa rằng đây có thể là bất cứ thứ gì, không nhất thiết liên quan tới hạ tầng. Muốn kubectl create ra một FlightTicket, kubectl get liệt kê được nó, kubectl delete xóa được booking.

Thử tạo thẳng object đó ngay bây giờ sẽ nhận lỗi: there are no matches for kind "FlightTicket" in version "flights.com/v1". Kubernetes API không tự nhận diện được resource type do bạn tự nghĩ ra — phải khai báo nó trước bằng một Custom Resource Definition (CRD).

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: flighttickets.flights.com
spec:
scope: Namespaced
group: flights.com
names:
kind: FlightTicket
singular: flightticket
plural: flighttickets
shortNames: ["ft"]
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
fromAirport:
type: string
toAirport:
type: string
number:
type: integer
minimum: 1
maximum: 10

apiextensions.k8s.io/v1 là apiVersion duy nhất còn được hỗ trợ cho CRD — phiên bản v1beta1 đã bị gỡ bỏ hoàn toàn từ Kubernetes 1.22, nên không cần lo về chuyện chọn nhầm version cũ. scope xác định resource namespaced hay cluster-scoped (như Pod thì namespaced, còn Node hay PersistentVolume thì không). names khai báo cách gọi resource ở các ngữ cảnh khác nhau: kind dùng trong YAML, plural dùng khi gọi API và hiển thị trong kubectl get, shortNames cho phép gõ tắt.

kubectl create -f flighttickets-crd.yaml

# Output
customresourcedefinition.apiextensions.k8s.io/flighttickets.flights.com created

CRD tạo xong, giờ mới tạo được instance:

apiVersion: flights.com/v1
kind: FlightTicket
metadata:
name: my-flight-ticket
spec:
fromAirport: Mumbai
toAirport: London
number: 2
kubectl create -f my-flight-ticket.yaml
kubectl get ft

# Output
NAME AGE
my-flight-ticket 4s

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

FlightTicket này là một custom resource thật — được lưu, liệt kê, xóa đúng như mọi resource khác. Nhưng nó không hề đặt vé máy bay thật nào cả.

💡 Hình dung: CRD + Custom Resource giống việc bạn thêm một từ mới vào từ điển tiếng Việt — Kubernetes API giờ "biết" FlightTicket là gì, chấp nhận cho kubectl get/create/delete như mọi resource khác. Nhưng đó chỉ là một danh từ mới. Chưa có gì hành động theo danh từ đó — tạo một FlightTicket không khiến vé máy bay thật được đặt. Cần dạy Kubernetes một động từ đi kèm.

Dạy động từ đó là việc của Custom Controller.


8. Custom Controllers

Custom Controller là bất kỳ process hay đoạn code nào chạy trong một vòng lặp, liên tục theo dõi Kubernetes API để phát hiện khi object loại FlightTicket được tạo, sửa, hoặc xóa — rồi thực hiện hành động tương ứng, ví dụ gọi một API đặt vé thật bên ngoài.

Về mặt kỹ thuật, có thể viết controller bằng bất kỳ ngôn ngữ nào biết gọi Kubernetes API — kể cả Python. Nhưng viết bằng Go, dùng Kubernetes Go client, có lợi thế rõ rệt: thư viện Shared Informers đi kèm đã lo sẵn cơ chế caching và queuing hiệu quả, thứ mà tự viết bằng ngôn ngữ khác sẽ phải tự tay xây dựng lại, và rất dễ làm sai (gọi API quá nhiều lần, miss event, race condition khi có nhiều controller cùng xử lý).

Quy trình xây dựng thường bắt đầu từ repo mẫu sample-controller trên GitHub: clone về, sửa controller.go theo logic riêng — ví dụ ở đâu đó gọi Flight Booking API thật khi phát hiện FlightTicket mới — build, rồi chạy, chỉ định file kubeconfig để controller xác thực với API server. Sau khi hoàn thiện, đóng gói controller vào một Docker image và chạy nó như một Pod hoặc Deployment ngay trong cluster, thay vì phải build-và-chạy thủ công mỗi lần.

Trong kỳ thi CKA, không kỳ vọng câu hỏi yêu cầu tự viết custom controller — phần đó thiên về coding hơn là vận hành cluster. Nhưng câu hỏi về CRD, và cách làm việc với các controller có sẵn, là nội dung nên nắm chắc.


9. Operator Framework

Tới đây, CRD và Custom Controller vẫn là hai thực thể tách rời — bạn tự tạo CRD, tự deploy controller như một Deployment riêng, tự quản lý cả hai. Operator Framework đóng gói cả hai lại thành một đơn vị triển khai duy nhất.

💡 Hình dung: Tự viết CRD + Custom Controller giống việc bạn tự thuê nhân sự, tự soạn quy trình vận hành cho một hệ thống con — linh hoạt nhưng tốn công. Cài một Operator có sẵn giống mua nguyên một gói dịch vụ vận hành trọn gói: cài một lần, có sẵn cả "nhân sự" (controller) lẫn quy trình xử lý sự cố (backup, restore, upgrade) đi kèm, không phải tự lắp ráp từng mảnh.

Ví dụ thực tế: etcd operator — cài vào là có ngay CRD etcdCluster cùng một custom controller theo dõi resource đó để tự deploy etcd cluster bên trong Kubernetes. Nhưng nó còn làm được nhiều hơn deploy: chỉ cần tạo thêm một custom resource khác, etcd operator có thể tự backup cluster hoặc restore từ một bản backup có sẵn — nhờ có thêm logic backup operatorrestore operator đóng gói cùng.

Nói cách khác, một Kubernetes operator làm đúng những việc một human operator vẫn làm thủ công để vận hành một ứng dụng: cài đặt, backup/restore định kỳ, tự sửa các sự cố thường gặp — chỉ khác là toàn bộ được code hóa và chạy tự động.

CRD → Custom Resource → Controller → OperatorOperator bundleCRD: FlightTicketđịnh nghĩa kind + schemaCustom Resourcemy-flight-ticket (etcd)Custom Controllerwatch loop (Go + client-go)Flight Booking API(hệ thống ngoài)watchgọi API đặt vé thậtCRD + Custom Resource là "danh từ" mới trong etcd — Custom Controller mới là "động từ" biến nó thành hành động thật.

Tìm operator có sẵn tại OperatorHub.io — nơi liệt kê hàng trăm operator cho các ứng dụng phổ biến như etcd, MySQL, Prometheus, Grafana, Argo CD, hay Istio. Cài đặt thường chỉ gồm hai bước: cài Operator Lifecycle Manager (OLM) trước, rồi cài chính operator cần dùng qua OLM. Cần lưu ý: OLM bản v0 (bản đang được dùng rộng rãi hiện nay) đã bước vào giai đoạn maintenance-only — không nhận thêm tính năng mới, chỉ vá lỗi — trong khi bản kế nhiệm OLM v1 (dự án operator-controller) đã đạt GA trong một số bản phân phối như OpenShift từ đầu 2025 và đang dần thay thế v0, dù bản thân dự án thượng nguồn vẫn tiếp tục hoàn thiện API. OperatorHub.io vẫn là nơi tra cứu operator tốt, nhưng nên kiểm tra tình trạng bảo trì của từng operator cụ thể trước khi đưa vào production, không mặc định mọi operator trên đó đều được cập nhật đều đặn. Theo kinh nghiệm cá nhân, việc đầu tiên nên làm trước khi cài bất kỳ operator nào lên production là xem ngày release gần nhất và số issue còn mở trên repo gốc của nó — một operator "phổ biến trên OperatorHub" không đồng nghĩa "đang được đội ngũ gốc duy trì tích cực".

Đi sâu vào việc viết operator riêng cần cả một khóa học khác. Trong phạm vi kỳ thi CKA, phần này chỉ mang tính bổ sung — trọng tâm vẫn là CRD và cách làm việc với custom resource.

Phần Security khép lại tại đây, từ authentication/RBAC ở Part 7.1–7.2 cho tới image, security context, và network policy ở phần này. Bước tiếp theo chuyển sang Part 8. Storage, nơi bàn cách Pod lưu trữ dữ liệu bền vững qua Volumes, PersistentVolumes, và PersistentVolumeClaims.


Nguồn tham khảo

Nguồn gốc: Khóa "Certified Kubernetes Administrator (CKA)" — phần "Security" (Image Security; kiến thức nền Docker security và Linux capabilities; Security Contexts; Network Policies — kiến thức nền, chi tiết, lab thực hành; Kubectx và Kubens; Custom Resource Definitions, Custom Controllers, và Operator Framework), nền tảng KodeKloud. Giảng viên: Mumshad Mannambeth.

Fact-check:

  • PodSecurityPolicy đã bị gỡ bỏ hoàn toàn từ Kubernetes 1.25, thay thế bởi Pod Security Admission và Pod Security Standards (privileged/baseline/restricted) — 2026-09-23, Kubernetes: Pod Security Standards.
  • apiextensions.k8s.io/v1beta1 cho CustomResourceDefinition đã ngừng phục vụ từ Kubernetes 1.22; apiextensions.k8s.io/v1 là phiên bản duy nhất còn hỗ trợ — 2026-09-23, Kubernetes: Deprecated API Migration Guide.
  • Network Policy Providers được liệt kê chính thức: Antrea, Calico, Cilium, kube-router — 2026-09-23, Kubernetes: Network Policies.
  • Flannel không tự có NetworkPolicy controller; tổ hợp Flannel + Calico ("Canal") cung cấp policy enforcement trên nền Flannel — 2026-09-23, Calico: Install for Flannel.
  • Repository weaveworks/weave đã được chủ sở hữu archive từ 20/06/2024, không còn nhận thay đổi mới — 2026-09-23, weaveworks/weave trên GitHub.
  • Cú pháp kubectl create secret docker-registry và field imagePullSecrets vẫn là cách được khuyến nghị để pull image từ private registry — 2026-09-23, Kubernetes: Pull an Image from a Private Registry.
  • Operator Lifecycle Manager (OLM) v0 đang ở giai đoạn maintenance-only, không nhận thêm tính năng mới; OLM v1 (operator-controller) đã đạt GA trong một số bản phân phối như OpenShift (từ đầu 2025) dù bản thân dự án thượng nguồn vẫn đang hoàn thiện — 2026-09-23, Operator Framework: OLM, Red Hat: Announcing OLM v1.

Kubectx/Kubens: ahmetb/kubectx.

Repo ghi chú, link tài liệu, và đáp án các practice question của toàn bộ khóa học: kodekloudhub/certified-kubernetes-administrator-course.