SecurityContext, capabilities và Pod Security Admission
Chào bạn, mình đỗ CKA và phần security là phần mình từng làm sai nhiều nhất khi luyện. Lý do rất đơn giản: đề không hỏi "khái niệm", đề đưa một YAML hoặc một namespace và yêu cầu bạn làm cho pod chạy được trong điều kiện bảo mật mới. Bài này mình sẽ dạy đúng theo hướng đó: viết YAML cho non-root, bỏ capability, và gắn label PSA cho namespace, kèm những lỗi mình đã trả giá.
Mục tiêu bài học
- Phân biệt được
securityContextở cấp pod và cấp container, biết trường nào đặt ở đâu. - Cấu hình pod chạy non-root với
runAsUser,runAsNonRoot,fsGroupvàreadOnlyRootFilesystem. - Drop toàn bộ capability và add lại đúng capability cần thiết.
- Gắn label Pod Security Admission cho namespace (enforce/audit/warn) và sửa pod vi phạm cho qua mức
restricted. - Nhận diện các lỗi hay gặp trong phòng thi liên quan đến ba phần trên.
Ôn tập nhanh (5 phút)
Bài trước chúng ta đã nói: ServiceAccount là identity theo namespace, và pod không khai báo gì sẽ chạy bằng SA default. Điều đó chỉ giải quyết pod là ai (authentication/authorization với API server).
Bài này là chuyện khác: pod chạy với đặc quyền gì bên trong container. Một pod có thể dùng ServiceAccount quyền rất hẹp nhưng process bên trong vẫn chạy uid 0 và giữ đủ Linux capabilities. Đề CKA hay tách hai ý này ra thành hai câu riêng.
[!NOTE] Nhớ nhanh: ServiceAccount = quyền gọi API server. SecurityContext = quyền của process trong container. PSA = luật của cả namespace, áp lúc tạo pod.
Lý thuyết
1. SecurityContext: hai cấp, một mục tiêu
securityContext xuất hiện ở hai nơi:
spec.securityContext(cấp pod): áp cho mọi container, dùng cho các trường chung như user/group/fsGroup.spec.containers[].securityContext(cấp container): ghi đè cấp pod, và là nơi duy nhất khai báo đượccapabilities,privileged,allowPrivilegeEscalation,readOnlyRootFilesystem.
spec:
securityContext: # cấp pod
runAsUser: 1000
runAsGroup: 3000
runAsNonRoot: true
fsGroup: 2000
containers:
- name: app
image: busybox:1.36
securityContext: # cấp container
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
Nếu bạn đặt capabilities ở cấp pod, API server trả lỗi kiểu unknown field "capabilities" trong spec.securityContext. Đây là lỗi mình gặp đầu tiên khi luyện, và nó tốn của mình gần 5 phút trong một buổi mock.
| Trường | Cấp pod | Cấp container |
|---|---|---|
runAsUser, runAsGroup, runAsNonRoot |
Có | Có (ghi đè) |
fsGroup |
Có | Không |
supplementalGroups |
Có | Không |
seccompProfile |
Có | Có (ghi đè) |
capabilities |
Không | Có |
privileged |
Không | Có |
allowPrivilegeEscalation |
Không | Có |
readOnlyRootFilesystem |
Không | Có |
Trường quan trọng nhất khi thi là runAsNonRoot: true. Nó không set uid, nó chỉ bắt buộc runtime phải chứng minh được process không chạy uid 0. Nếu image có USER root và bạn không set runAsUser, pod sẽ vào CreateContainerConfigError với message container has runAsNonRoot and image will run as root. Cách sửa luôn là thêm runAsUser: 1000.
Còn fsGroup thì liên quan đến volume: các volume kiểu emptyDir, configMap, secret, PV đều được chown cho group này, nhờ vậy process non-root ghi được vào volume. Đây là lý do vì sao đề hay đi kèm "pod phải ghi được vào volume" với yêu cầu non-root.
2. Capabilities: drop trước, add sau
Linux capabilities là các mảnh quyền root bị chia nhỏ (NET_ADMIN, SYS_TIME, CHOWN, ...). Container mặc định giữ một bộ capabilities của runtime. Cách hardening chuẩn là drop hết rồi add lại đúng cái cần:
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Hai điều cần nhớ:
drop: ["ALL"]phải viết đúng chữALLin hoa, trong một list.allowPrivilegeEscalation: falselà bạn đồng minh củadrop. Thiếu nó, process vẫn có thể leo thang quasetuid. Mứcrestrictedcủa PSA yêu cầu cả hai.
Với mức restricted, chỉ được add lại duy nhất NET_BIND_SERVICE. Nếu đề yêu cầu NET_ADMIN, bạn buộc phải nới namespace xuống privileged. Đây là cái bẫy rất hay gặp: đề cho namespace đang restricted, yêu cầu pod có NET_ADMIN, và người thi loay hoay sửa YAML mãi mà không hiểu vì sao bị từ chối.
3. Pod Security Admission: luật của namespace
PSA là admission controller có sẵn trong Kubernetes (GA từ 1.25), hoạt động bằng label trên namespace, không cần cài gì thêm. Có ba mức:
| Mức | Ý nghĩa | Dùng khi |
|---|---|---|
privileged |
Không hạn chế gì | workload hệ thống thật sự cần đặc quyền |
baseline |
Chặn các thiết lập nguy hiểm: privileged, hostNetwork, hostPID, hostPath, thêm capability ngoài danh sách cho phép |
workload thông thường |
restricted |
baseline cộng thêm: non-root, drop: ALL, allowPrivilegeEscalation: false, seccompProfile là RuntimeDefault hoặc Localhost, chỉ cho volume type an toàn |
đề hardening, chuẩn cao nhất |
Ba label tương ứng ba chế độ:
pod-security.kubernetes.io/enforce: chặn thật, pod vi phạm bị từ chối.pod-security.kubernetes.io/audit: chỉ ghi annotation vào audit log.pod-security.kubernetes.io/warn: chỉ in cảnh báo rakubectl, vẫn tạo pod.
Mặc định khi namespace không có label nào, mức hiệu lực là {{c5::privileged}}, tức là không chặn gì. Vì vậy đề bài luôn phải yêu cầu bạn tự gắn label.
kubectl label namespace sec-lab \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted \
--overwrite
Muốn ghim phiên bản luật (tránh cluster nâng cấp làm đổi hành vi) thì thêm pod-security.kubernetes.io/enforce-version=v1.29.
[!IMPORTANT] PSA chỉ kiểm tra lúc tạo pod. Đổi label không xoá hay sửa pod đang chạy. Nếu đề yêu cầu "namespace phải chặn pod privileged mà các pod cũ vẫn còn", bạn phải tự xoá hoặc sửa chúng.
Luồng kiểm tra diễn ra như sau:
flowchart TD
Req["📝 kubectl apply pod"] --> API["API Server"]
API --> PSA{"Namespace có label enforce?"}
PSA -->|"privileged"| OK1["✅ Gần như cho qua tất cả"]
PSA -->|"baseline"| Check2["Kiểm tra privileged, hostNetwork, hostPath"]
PSA -->|"restricted"| Check3["Kiểm tra non-root, drop ALL, seccomp"]
Check2 -->|"hợp lệ"| OK2["✅ Nhận pod"]
Check3 -->|"hợp lệ"| OK3["✅ Nhận pod"]
Check2 -->|"vi phạm"| Rej["❌ Từ chối pod"]
Check3 -->|"vi phạm"| Rej
Khi bị từ chối, message có dạng: pods "x" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "app" must set securityContext.allowPrivilegeEscalation=false). Đọc kỹ message này, nó liệt kê đủ mọi vi phạm, sửa đúng từng dòng là xong.
Ví dụ có hướng dẫn
Ví dụ 1: Pod non-root, filesystem read-only, vẫn ghi được vào volume
Yêu cầu: pod chạy uid 1000, group 3000, không cho ghi vào root filesystem, nhưng process cần ghi vào /tmp.
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: sec-lab
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
runAsNonRoot: true
fsGroup: 2000
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do date > /tmp/out; sleep 30; done"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Các bước kiểm tra:
kubectl apply -f secure-app.yaml
kubectl get pod secure-app -n sec-lab
kubectl exec -n sec-lab secure-app -- id
kubectl exec -n sec-lab secure-app -- cat /tmp/out
kubectl exec -n sec-lab secure-app -- sh -c 'touch /root/test' # phải bị Permission denied
Kết quả id phải là uid=1000 gid=3000 groups=2000. Nếu lệnh touch /root/test thành công thì readOnlyRootFilesystem chưa ăn.
Ví dụ 2: Drop ALL rồi add lại một capability
apiVersion: v1
kind: Pod
metadata:
name: cap-pod
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Kiểm chứng bằng cách xem capability hiệu lực của process:
kubectl exec cap-pod -- cat /proc/1/status | grep -i cap
Bạn sẽ thấy CapPrm và CapEff khác 0000003fffffffff (bộ đầy đủ mặc định). Đây là cách chứng minh nhanh khi đề yêu cầu "verify".
Ví dụ 3: Bật restricted cho namespace rồi sửa pod vi phạm
Bước 1, gắn label:
kubectl create namespace sec-lab
kubectl label ns sec-lab pod-security.kubernetes.io/enforce=restricted --overwrite
Bước 2, apply một pod "xấu":
apiVersion: v1
kind: Pod
metadata:
name: bad-pod
namespace: sec-lab
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
privileged: true
Kết quả: bị từ chối với vài dòng vi phạm. Bước 3, sửa thành bản hợp lệ tối thiểu cho restricted:
apiVersion: v1
kind: Pod
metadata:
name: good-pod
namespace: sec-lab
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
Bốn thứ bắt buộc để qua restricted: runAsNonRoot: true (kèm uid non-root), allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], và seccompProfile.type là RuntimeDefault. Thiếu bất kỳ cái nào là bị chặn.
Mẹo mình dùng nhiều nhất: chạy --dry-run=server trước khi apply thật. Server-side dry run vẫn đi qua admission nên bạn thấy cảnh báo và lỗi từ chối ngay mà không tạo rác trong cluster.
kubectl apply -f good-pod.yaml --dry-run=server
Luyện tập có hướng dẫn
Làm theo thứ tự, mỗi bài tự bấm giờ. Đáp án ở phần thu gọn bên dưới, cố gắng làm trước khi mở.
Bài 1. Tạo namespace hardening và gắn cả ba label enforce, warn, audit ở mức restricted, ghim version v1.29.
Bài 2. Viết pod probe trong namespace đó: image busybox:1.36, chạy sleep 3600, uid 1000, drop toàn bộ capability, allowPrivilegeEscalation: false, seccomp RuntimeDefault. Pod phải ở trạng thái Running, và kubectl exec vào xác nhận được uid.
Bài 3. Pod sau bị từ chối. Chỉ ra ba vi phạm và sửa lại cho qua restricted:
spec:
containers:
- name: web
image: nginx:1.25
securityContext:
runAsNonRoot: true
capabilities:
add: ["NET_ADMIN", "SYS_TIME"]
Bài 4. Đề yêu cầu pod trong namespace hardening phải có capability NET_ADMIN. Trình bày hai hướng xử lý và nói rõ hướng nào đúng về mặt bảo mật.
Bài 5. Namespace sec-lab đang có 3 pod chạy bằng privileged: true. Bạn gắn enforce=restricted lên namespace. Ba pod đó có bị xoá không? Làm sao để đưa namespace về trạng thái sạch?
Đáp án bài 1
kubectl create namespace hardening
kubectl label ns hardening \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.29 \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted --overwrite
kubectl get ns hardening --show-labels
Đáp án bài 2
apiVersion: v1
kind: Pod
metadata:
name: probe
namespace: hardening
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: probe
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
kubectl apply -f probe.yaml --dry-run=server # xem trước
kubectl apply -f probe.yaml
kubectl get pod probe -n hardening -w
kubectl exec -n hardening probe -- id
Đáp án bài 3
Ba vi phạm:
capabilities.addcóNET_ADMINvàSYS_TIME:restrictedchỉ cho add lạiNET_BIND_SERVICE.- Thiếu
allowPrivilegeEscalation: false. - Thiếu
seccompProfile.typelàRuntimeDefaulthoặcLocalhost.
Bản sửa:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101 # nginx-unprivileged dùng uid 101
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.25
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
Lưu ý: image nginx thường cần root để bind port 80, nên đổi sang bản unprivileged. Trong đề thi, nếu image cố định là nginx và yêu cầu non-root, hãy kiểm tra lại xem đề có cho phép giữ port 80 hay không.
Đáp án bài 4
Hướng 1 (sai về bảo mật): hạ namespace xuống privileged hoặc baseline. NET_ADMIN không nằm trong danh sách capability được add ở baseline, nên thực tế chỉ privileged mới chạy được.
Hướng 2 (đúng): giữ namespace restricted và giữ pod không cần NET_ADMIN. Nếu thật sự cần, tách namespace riêng cho workload đặc quyền với enforce=privileged, giới hạn phạm vi RBAC, và ghi rõ lý do. Trong phòng thi, hãy đọc đề xem họ yêu cầu "giữ restricted" hay "cho pod chạy" trước khi chọn.
Đáp án bài 5
Không. PSA chỉ áp lúc tạo pod, label không có tác dụng hồi tố. Ba pod cũ vẫn chạy. Để sạch, phải xoá hoặc rollout lại chúng:
kubectl get pods -n sec-lab -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'
kubectl delete pod <tên> -n sec-lab
Với workload do Deployment quản lý, dùng kubectl rollout restart deployment/<tên> -n sec-lab để ReplicaSet tạo pod mới, và pod mới sẽ bị PSA kiểm tra, nên phải sửa template trước.
Góc chuyên gia
Ba lỗi mình từng mắc, theo thứ tự tần suất:
- Đặt
capabilitieshoặcallowPrivilegeEscalationởspec.securityContext(cấp pod). Không hợp lệ, YAML bị từ chối ngay. Luôn kiểm tra bằngkubectl explain pod.spec.securityContextvàkubectl explain pod.spec.containers.securityContexttrước khi viết. runAsNonRoot: truemà quênrunAsUser, dẫn tớiCreateContainerConfigError. Đọckubectl describe pod, message nằm ở phầnEvents, không nằm trong log.readOnlyRootFilesystem: truerồi thắc mắc vì sao app crash. Rất nhiều app cần ghi vào/tmp,/var/run,/var/cache. Cách sửa nhanh: mountemptyDirvào đúng thư mục đó.
Quy tắc ngón tay:
- Pod non-root:
runAsNonRoot: true+runAsUser: <uid>+fsGroupnếu có volume. Viết cả ba ngay từ đầu, đừng chờ lỗi. - Namespace restricted: dùng đúng bốn dòng vàng là
runAsNonRoot,runAsUser,allowPrivilegeEscalation: false,capabilities.drop: ["ALL"], cộngseccompProfile: RuntimeDefault. Thuộc lòng bộ này là xong 90% câu hỏi hardening. - Trước khi apply vào namespace có PSA, chạy
--dry-run=server. Mất 3 giây, tiết kiệm 3 phút. - Khi bị từ chối, đọc nguyên message lỗi. PSA liệt kê từng vi phạm kèm tên container, không cần đoán.
- Đừng giả định
kube-systemđược miễn trừ PSA. Kiểm tra bằngkubectl get ns kube-system --show-labelstrước khi kết luận.
Mẹo tốc độ trong phòng thi: phần security thường chỉ 2 đến 3 câu, mỗi câu nên xong trong 6 đến 8 phút. Cứ generate bằng kubectl run rồi patch, hoặc viết YAML tay, nhưng nhớ set context và namespace trước:
kubectl config set-context --current --namespace=hardening
Sau khi làm xong, luôn verify bằng một lệnh kubectl get pod cộng một lệnh kubectl exec hoặc kubectl describe. Điểm CKA chấm trên trạng thái thực tế của cluster, không chấm trên YAML bạn viết.
Vài câu cloze để tự kiểm tra nhanh: capabilities chỉ khai báo được ở cấp {{c1::container}}; mức PSA chặt nhất trong ba mức là {{c2::restricted}}; label bắt buộc chặn thật là pod-security.kubernetes.io/{{c3::enforce}}; để bắt process không chạy uid 0 ta đặt runAsNonRoot: {{c4::true}}; và {{c6::allowPrivilegeEscalation: false}} là trường bắt buộc phải có ở mức restricted.
Tóm tắt và bước tiếp theo
Tóm gọn bài này trong bốn dòng:
securityContextở cấp container ghi đè cấp pod;capabilities,privileged,allowPrivilegeEscalation,readOnlyRootFilesystemchỉ có ở cấp container.- Non-root cần
runAsNonRoot: truecộngrunAsUser, thêmfsGroupkhi có volume. - Hardening chuẩn là
drop: ["ALL"]cộngallowPrivilegeEscalation: false. - PSA là label trên namespace với ba mức
privileged/baseline/restrictedvà ba chế độenforce/audit/warn; chỉ áp lúc tạo pod.
Flashcard dưới đây để bạn ôn lại trước khi chuyển bài. Mình khuyên chạy hết một lượt, đánh dấu những thẻ sai rồi làm lại sau 24 giờ.
[
{ "front": "securityContext đặt ở cấp nào?", "back": "Hai cấp: spec.securityContext (pod) và spec.containers[].securityContext (container), cấp container ghi đè cấp pod." },
{ "front": "Trường nào CHỈ khai báo được ở cấp container?", "back": "capabilities, privileged, allowPrivilegeEscalation, readOnlyRootFilesystem." },
{ "front": "Trường nào chỉ có ở cấp pod?", "back": "fsGroup, supplementalGroups, sysctls." },
{ "front": "runAsNonRoot: true khác runAsUser thế nào?", "back": "runAsNonRoot chỉ bắt buộc process không chạy uid 0; runAsUser mới là thứ set uid cụ thể." },
{ "front": "Lỗi gì xảy ra khi runAsNonRoot: true mà image chạy root?", "back": "CreateContainerConfigError với message 'container has runAsNonRoot and image will run as root'." },
{ "front": "fsGroup dùng để làm gì?", "back": "Set group sở hữu cho volume (emptyDir, configMap, secret, PV) để process non-root ghi được." },
{ "front": "Cách drop toàn bộ Linux capabilities trong YAML?", "back": "securityContext.capabilities.drop: [\"ALL\"], đặt ở cấp container." },
{ "front": "Ba mức của Pod Security Admission?", "back": "privileged (không hạn chế), baseline (chặn đặc quyền nguy hiểm), restricted (non-root, drop ALL, seccomp)." },
{ "front": "Label nào chặn thật pod vi phạm?", "back": "pod-security.kubernetes.io/enforce, các label audit và warn chỉ ghi log hoặc in cảnh báo." },
{ "front": "Namespace không có label PSA thì mức hiệu lực là gì?", "back": "privileged, tức là không chặn gì." },
{ "front": "Bốn yêu cầu tối thiểu để pod qua mức restricted?", "back": "runAsNonRoot: true (kèm uid non-root), allowPrivilegeEscalation: false, capabilities.drop: [\"ALL\"], seccompProfile RuntimeDefault hoặc Localhost." },
{ "front": "Ở mức restricted được add lại capability nào?", "back": "Chỉ NET_BIND_SERVICE. NET_ADMIN hay SYS_TIME đều bị từ chối." },
{ "front": "Đổi label PSA có xoá pod đang chạy không?", "back": "Không. PSA chỉ kiểm tra lúc tạo pod, phải tự xoá hoặc rollout lại workload." },
{ "front": "Lệnh nhanh để xem trước vi phạm PSA mà không tạo pod?", "back": "kubectl apply -f pod.yaml --dry-run=server" }
]
Bài tiếp theo: CertificateSigningRequest và kubeconfig. Chúng ta sẽ đi từ việc "process trong container chạy bằng quyền gì" sang "client nào được phép gọi API server": tạo CSR, approve, ký certificate và dựng kubeconfig riêng cho từng user. Đây là dạng bài chắc chắn xuất hiện trong domain Security, và cũng là phần dễ mất điểm nhất nếu bạn không thuộc thứ tự lệnh.