Ở cửa hải quan, câu hỏi không phải "hành lý của anh có nguy hiểm không" — nhân viên đâu mở được hết ra mà biết. Câu hỏi là "anh có ký vào tờ khai rằng nó không nguy hiểm không". Không ký, không qua. Nghe hơi phiền, nhưng đó là cách duy nhất một hệ thống không đọc được ý định bên trong vẫn phân loại được an toàn: bắt bạn tuyên bố, và mặc định coi im lặng là đáng ngờ. Mức restricted của Kubernetes làm đúng như vậy, và nó từ chối cả một pod nginx chẳng làm gì sai. RBAC quyết định ai được tạo pod; Pod Security Standards quyết định pod đó được phép làm gì với node. Hai lớp khác nhau, và lớp thứ hai bật bằng một dòng nhãn.
Bật bằng nhãn namespace
kubectl create ns pss-restricted
kubectl label ns pss-restricted \
pod-security.kubernetes.io/enforce=restricted
Không cài gì thêm. Từ Kubernetes 1.25, bộ kiểm này nằm sẵn trong API server, thay cho PodSecurityPolicy đã bị gỡ. Ba mức: privileged (không chặn gì), baseline (chặn leo thang rõ ràng), restricted (siết chặt).
Đo thật: cùng một pod, ba namespace
Tôi tạo ba namespace gắn ba mức, rồi thả cùng một bộ pod vào.
| pod | privileged | baseline | restricted |
|---|---|---|---|
| nginx bình thường | tạo được | tạo được | từ chối |
privileged: true |
tạo được | từ chối | từ chối |
hostNetwork: true |
tạo được | từ chối | từ chối |
Ba dòng sau đúng như tên gọi. Dòng đầu mới là điều đáng dừng lại.
Pod nginx bình thường bị từ chối
Đây là toàn văn lời từ chối:
violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false
(container "c" must set securityContext.allowPrivilegeEscalation=false)
unrestricted capabilities
(container "c" must set securityContext.capabilities.drop=["ALL"])
runAsNonRoot != true
(pod or container "c" must set securityContext.runAsNonRoot=true)
seccompProfile
(pod or container "c" must set securityContext.seccompProfile.type
to "RuntimeDefault" or "Localhost")
Pod này không làm gì đặc biệt cả. Nó chỉ chạy nginx. Điểm cần hiểu: restricted không hỏi "pod này có nguy hiểm không" mà hỏi "pod này có khai báo là nó không nguy hiểm không". Không khai là từ chối.
Cách đó nghe khó chịu nhưng đúng. Kubernetes không biết container bên trong làm gì; nó chỉ đọc được phần khai báo. Mặc định của securityContext là "chạy root, giữ đủ capability, cho leo thang quyền" — nên im lặng nghĩa là mở, và restricted không chấp nhận im lặng.
Pod hợp lệ trông thế nào
apiVersion: v1
kind: Pod
metadata: {name: pok}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile: {type: RuntimeDefault}
containers:
- name: c
image: nginx:alpine
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: ["ALL"]}
Kết quả: pod/pok created trong pss-restricted. Năm dòng. Không dòng nào khó, nhưng dòng nào cũng phải có — và runAsUser: 1000 không thừa: runAsNonRoot: true chỉ kiểm tra image không chạy root, còn nếu image không khai USER thì pod sẽ dừng ở CreateContainerConfigError sau khi đã tạo xong. Bộ kiểm cho qua, kubelet mới chặn — hai chỗ khác nhau.
Với nginx thật, image chính thức chạy root nên phải dùng bản nginxinc/nginx-unprivileged hoặc tự dựng. Đó mới là chi phí thật của restricted: không phải năm dòng YAML, mà là việc phải kiểm lại từng image trong hệ thống.
Ba nhãn, ba hành vi
| Nhãn | Làm gì |
|---|---|
enforce |
Từ chối tạo pod |
warn |
Cảnh báo cho người gõ lệnh, pod vẫn được tạo |
audit |
Ghi vào audit log |
Dùng chung được, và đó là cách siết chặt an toàn:
kubectl label ns san-xuat \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
Namespace này đang chặn ở mức baseline, nhưng mọi pod chưa đạt restricted sẽ hiện cảnh báo và vào audit log. Bạn đo được siết chặt sẽ gây vỡ bao nhiêu, mà chưa vỡ gì cả. Đặt enforce=restricted thẳng vào một namespace đang chạy là cách nhanh nhất để dừng mọi lần triển khai tiếp theo — và nó không dừng pod đang chạy, nên bạn chỉ biết khi lần deploy sau thất bại.
enforce chỉ xét lúc tạo pod
Gắn nhãn enforce không đụng tới pod đang chạy. Pod vi phạm vẫn sống cho tới khi có ai xoá nó. Hệ quả thực tế: một Deployment vi phạm vẫn phục vụ bình thường, nhưng nếu node chết hoặc bạn scale lên, ReplicaSet không tạo được pod thay thế. Sự cố nằm im chờ đúng lúc tệ nhất mới nổ.
Nên sau khi gắn nhãn, đừng dừng ở "không có lỗi". Đá thử một pod:
kubectl delete pod -n san-xuat <mot-pod> && \
kubectl get pods -n san-xuat -w
Mẫu số chung: an toàn mặc định đòi khai báo tường minh, và luật ở cổng không kiểm cái đã vào
Bài này gói hai nguyên tắc thiết kế đúng ở khắp nơi. Thứ nhất: khi một hệ thống không đọc được ý định bên trong, nó phải bắt bạn tuyên bố sự an toàn, và mặc định coi im lặng là nguy hiểm chứ không phải an toàn. restricted từ chối nginx không vì nginx xấu mà vì nó chưa ký tờ khai; mặc định của securityContext mở toang, nên "quên cấu hình" phải quy về "bị chặn", không phải "mở toang". Đây là tư thế fail-closed / tường-minh-hơn-ngầm-định mà mọi hệ an toàn đều chọn: firewall mặc định chặn, cổng nào không khai là đóng; CSP đặt default-src 'none' rồi mới cho phép từng nguồn; TypeScript bật noImplicitAny để một kiểu không được khai trở thành lỗi chứ không im lặng thành any; Rust bắt bạn viết hẳn khối unsafe để đánh dấu chỗ bạn tự chịu trách nhiệm. Ở đâu cũng cùng ý: làm cho đường an toàn đòi một câu khẳng định, và mặc định cái chưa nói thành đóng.
Thứ hai: một luật kiểm lúc vào cổng chỉ canh những gì đi qua từ lúc nó bật — nó không tự rà lại cái đã ở trong. enforce chỉ chạy lúc tạo pod, nên pod vi phạm cũ vẫn sống và sự cố chỉ nổ ở lần scale hay deploy sau. Cùng cái bẫy với một ràng buộc CHECK thêm vào CSDL bằng NOT VALID (dòng cũ không bị kiểm lại), với một linter mới thêm vào CI (bắt code mới, code cũ vẫn vi phạm nằm đó), với một validation mới ở API (chặn ghi mới nhưng hàng cũ trong bảng vẫn sai). Bài học: thêm một luật là bảo vệ tương lai, không phải quá khứ — muốn sạch cả quá khứ thì phải quét lại toàn bộ hiện trạng một cách riêng.
Mẫu số chung: thiết kế để cái chưa-khai-báo rơi về phía an toàn (đóng), và nhớ rằng một luật gác cổng không đồng nghĩa mọi thứ bên trong đã hợp lệ — hãy chủ động tái kiểm hiện trạng, đừng đợi lần tạo mới tiếp theo phơi ra chỗ hỏng vào đúng lúc tệ nhất.
Muốn tự soi, xem namespace nào đang không có lớp bảo vệ nào:
kubectl get ns -o json | python3 -c '
import sys,json
for n in json.load(sys.stdin)["items"]:
l=n["metadata"].get("labels",{})
m=l.get("pod-security.kubernetes.io/enforce","(khong co)")
print(f"{m:>12} " + n["metadata"]["name"])
'
Rồi thử warn trước khi thử enforce — không mất gì mà biết ngay có bao nhiêu thứ sẽ vỡ:
kubectl label --overwrite ns <ten> \
pod-security.kubernetes.io/warn=restricted
# tao lai chinh nhung pod dang chay, nhung chi chay thu
kubectl get pods -n <ten> -o yaml | kubectl create -f - --dry-run=server
Mỗi dòng Warning: would violate PodSecurity là một pod sẽ không tạo lại được sau khi siết. Lỗi AlreadyExists ở cuối là bình thường — pod đang chạy thật, cảnh báo đã in ra trước đó rồi. Lệnh đầu tôi viết là kubectl delete pod --all --dry-run=server, và nó không in cảnh báo nào. Suýt kết luận là namespace sạch. Lý do rõ ràng khi nghĩ lại: bộ kiểm chỉ chạy lúc tạo pod, mà xoá thì không tạo gì. Muốn thấy cảnh báo thì phải làm động tác tạo.
Phần sau đo kubectl từ phía người dùng: cache ở đâu, và vì sao lệnh đầu tiên sau khi đổi cụm luôn chậm hơn hẳn.