🛡️ 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, fsGroup và 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 được capabilities, 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ữ ALL in hoa, trong một list.
  • allowPrivilegeEscalation: false là bạn đồng minh của drop. Thiếu nó, process vẫn có thể leo thang qua setuid. Mức restricted củ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 ra kubectl, 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:

  1. capabilities.add có NET_ADMIN và SYS_TIME: restricted chỉ cho add lại NET_BIND_SERVICE.
  2. Thiếu allowPrivilegeEscalation: false.
  3. Thiếu seccompProfile.type là RuntimeDefault hoặc Localhost.

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:

  1. Đặt capabilities hoặc allowPrivilegeEscalation ở spec.securityContext (cấp pod). Không hợp lệ, YAML bị từ chối ngay. Luôn kiểm tra bằng kubectl explain pod.spec.securityContext và kubectl explain pod.spec.containers.securityContext trước khi viết.
  2. runAsNonRoot: true mà quên runAsUser, dẫn tới CreateContainerConfigError. Đọc kubectl describe pod, message nằm ở phần Events, không nằm trong log.
  3. readOnlyRootFilesystem: true rồ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: mount emptyDir vào đúng thư mục đó.

Quy tắc ngón tay:

  • Pod non-root: runAsNonRoot: true + runAsUser: <uid> + fsGroup nế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ộng seccompProfile: 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ằng kubectl get ns kube-system --show-labels trướ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, readOnlyRootFilesystem chỉ có ở cấp container.
  • Non-root cần runAsNonRoot: true cộng runAsUser, thêm fsGroup khi có volume.
  • Hardening chuẩn là drop: ["ALL"] cộng allowPrivilegeEscalation: false.
  • PSA là label trên namespace với ba mức privileged/baseline/restricted và 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.