Mục tiêu học tập
- Nắm threat surface của Kubernetes: API server, kubelet, etcd, workload
- Áp dụng Pod Security Standards (Privileged / Baseline / Restricted) thay thế PSP cũ
- Thiết kế RBAC đúng: Role/ClusterRole, RoleBinding/ClusterRoleBinding, ServiceAccount
- Cấu hình NetworkPolicy: default-deny, ingress/egress rules
- Quản lý secret an toàn: hiểu bẫy base64, dùng External Secrets Operator + AWS Secrets Manager/Vault
- Triển khai policy engine: OPA Gatekeeper hoặc Kyverno
- Audit cluster với kube-bench (CIS benchmark) và kubescape
1. Threat surface của Kubernetes
| Component | Lỗ hổng phổ biến | Mitigation |
|---|---|---|
| kube-apiserver | Anonymous auth, unbounded RBAC, exposed port 6443 | Disable anonymous, audit log, NetworkPolicy/SG hạn chế IP |
| etcd | Plain-text secret on disk, exposed port 2379 | Encryption-at-rest (EncryptionConfiguration), mTLS, không expose internet |
| kubelet | Port 10250 read/write API, anonymous request | --anonymous-auth=false, --authorization-mode=Webhook |
| Container runtime | runc CVE, image với CVE | Patch runc, image scanning |
| Workload | Excessive permission, container escape | Pod Security Standard, RBAC, NetworkPolicy |
Managed K8s (EKS, GKE, AKS) lo control plane (apiserver, etcd, scheduler). Bạn vẫn chịu trách nhiệm workload, RBAC, network policy, secret management — đây là Shared Responsibility cho K8s.
2. Pod Security Standards (PSS)
PSP (Pod Security Policy) đã deprecated trong K8s 1.21 và remove trong 1.25. Thay bằng Pod Security Standards + Pod Security Admission controller (built-in từ 1.25).
Ba level
| Level | Use case |
|---|---|
| Privileged | System daemon (CNI, CSI, monitoring agent) — chỉ namespace cụ thể |
| Baseline | Workload thường, không cần host access |
| Restricted | Production workload, multi-tenant — yêu cầu |
Áp dụng PSS qua namespace label
apiVersion: v1
kind: Namespace
metadata:
name: prod
labels:
# enforce = reject pod vi phạm; audit = log; warn = stderr warning
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.28
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Pod đáp ứng Restricted
apiVersion: v1
kind: Pod
metadata:
name: app
namespace: prod
spec:
automountServiceAccountToken: false # đừng auto-mount nếu app không gọi K8s API
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: app:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- { name: tmp, emptyDir: { medium: Memory, sizeLimit: 64Mi } }
3. RBAC: Role, ClusterRole, Binding, ServiceAccount
Phân biệt
| Object | Phạm vi | Dùng cho |
|---|---|---|
| Role | 1 namespace | Quyền trong namespace đó (Pod, Service, ConfigMap...) |
| ClusterRole | Toàn cluster | Quyền cluster-wide (Node, PV) hoặc reusable across NS |
| RoleBinding | 1 namespace | Gán Role hoặc ClusterRole cho subject trong NS |
| ClusterRoleBinding | Toàn cluster | Gán ClusterRole cho subject áp cho mọi NS |
| ServiceAccount | 1 namespace | Identity của pod (mặc định là default) |
Ví dụ: ServiceAccount + Role + Binding
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: prod
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: configmap-reader
namespace: prod
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"] # chỉ ConfigMap cụ thể
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-config-read
namespace: prod
subjects:
- kind: ServiceAccount
name: app-sa
namespace: prod
roleRef:
kind: Role
name: configmap-reader
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Pod
metadata:
name: app
namespace: prod
spec:
serviceAccountName: app-sa # dùng SA mới, không phải default
containers:
- { name: app, image: app:1.0 }
Anti-pattern: cluster-admin cho ServiceAccount
# ❌ ĐỪNG làm
- kind: ServiceAccount
name: default
roleRef:
kind: ClusterRole
name: cluster-admin
Pod có SA cluster-admin = RCE pod = chiếm cluster. Phổ biến nhất là Helm chart cẩu thả gắn cluster-admin "cho tiện cài đặt".
Audit RBAC
# Liệt kê mọi binding tới một subject
kubectl get rolebindings,clusterrolebindings -A -o json | \
jq '.items[] | select(.subjects[]?.name=="app-sa") | .metadata.name'
# Tool gọn: rbac-lookup, rbac-tool
rbac-lookup app-sa --kind serviceaccount
# Check identity hiện tại có làm action không
kubectl auth can-i delete pods --as=system:serviceaccount:prod:app-sa
4. NetworkPolicy
Mặc định K8s cho phép mọi pod giao tiếp với mọi pod. NetworkPolicy là stateless firewall ở L3/L4 để áp default-deny + chỉ allow traffic cần thiết.
⚠ NetworkPolicy chỉ hiệu lực nếu CNI plugin hỗ trợ (Calico, Cilium, AWS VPC CNI với policy enforcement). Flannel default không enforce.
Default deny mọi traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: prod
spec:
podSelector: {} # match mọi pod trong NS
policyTypes:
- Ingress
- Egress
Allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow
namespace: prod
spec:
podSelector:
matchLabels: { app: api }
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector:
matchLabels: { app: web } # chỉ web pod được vào api
ports:
- { protocol: TCP, port: 8080 }
egress:
- to: # api được gọi DNS
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- { protocol: UDP, port: 53 }
- to: # api được gọi RDS qua IP
- ipBlock:
cidr: 10.0.20.0/24
ports:
- { protocol: TCP, port: 5432 }
Best practice
1. Namespace-level default-deny (1 NetworkPolicy block all)
2. Per-app NetworkPolicy explicit allow ingress + egress
3. Đừng quên egress DNS (port 53) — thiếu sẽ break service discovery
4. Test bằng "kubectl exec ... -- nc -zv target port" trước rollout
Cilium còn hỗ trợ L7 policy (block HTTP method/path), eBPF observability — overlay trên NetworkPolicy chuẩn.
5. Secret management
Bẫy K8s Secret base64
apiVersion: v1
kind: Secret
metadata:
name: db
type: Opaque
data:
password: aHVudGVyMg== # ← KHÔNG phải mã hoá, chỉ là base64 "hunter2"
data.password chỉ là base64-encode (decode bằng echo aHVudGVyMg== | base64 -d). Bất kỳ ai có kubectl get secret đều xem được.
# Mặc định lưu plain trong etcd (chưa mã hoá)
kubectl exec -n kube-system etcd-master -- \
etcdctl get /registry/secrets/default/db --print-value-only
# → JSON với password plain
Layer phòng thủ thực sự
- Encryption-at-rest cho etcd: cấu hình
EncryptionConfigurationvới provider KMS (AWS KMS / GCP KMS / Vault). EKS/GKE managed mặc định bật. - RBAC trên Secret object: hạn chế
get secretschỉ ServiceAccount cần thiết. - External secret store: secret thật nằm ngoài K8s (AWS Secrets Manager, HashiCorp Vault), K8s chỉ giữ reference.
External Secrets Operator (ESO)
---
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-sm
namespace: prod
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt: # IRSA: pod assumes IAM role qua ServiceAccount
serviceAccountRef:
name: app-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-secret
namespace: prod
spec:
refreshInterval: 1h # auto-rotate
secretStoreRef:
name: aws-sm
kind: SecretStore
target:
name: db # K8s Secret name được sync
data:
- secretKey: password # key trong K8s Secret
remoteRef:
key: prod/app/db # path trong AWS Secrets Manager
property: password
ESO + IRSA (IAM Role for ServiceAccount): pod identity = IAM role, ESO pull secret từ AWS Secrets Manager, materialize thành K8s Secret để mount như bình thường. Secret thật sống ở Secrets Manager, có rotation, audit, KMS encryption.
Vault Agent Injector
Annotation pod → Vault Agent sidecar mount secret vào /vault/secrets/. Secret không tồn tại trong K8s Secret object.
6. Admission Controller: OPA Gatekeeper vs Kyverno
Admission controller chặn request trước khi API server lưu vào etcd. Dùng để enforce policy cluster-wide.
OPA Gatekeeper
Dùng Rego (DSL của OPA). Mạnh, generic — nhưng học cong cao.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names: { kind: K8sRequiredLabels }
validation:
openAPIV3Schema:
type: object
properties:
labels: { type: array, items: { type: string } }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
required := input.parameters.labels
provided := input.review.object.metadata.labels
missing := required[_]
not provided[missing]
msg := sprintf("missing label %v", [missing])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: ns-must-have-owner
spec:
match:
kinds: [{ apiGroups: [""], kinds: ["Namespace"] }]
parameters:
labels: ["owner", "cost-center"]
Kyverno
YAML-based — dễ đọc, dễ viết hơn Rego. Phổ biến cho team K8s-native.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-runAsNonRoot
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Container phải chạy non-root"
pattern:
spec:
securityContext:
runAsNonRoot: true
Kyverno hỗ trợ: validate, mutate (auto-add label), generate (tạo NetworkPolicy mặc định khi tạo NS), verify image signature (cosign).
| Tiêu chí | OPA Gatekeeper | Kyverno |
|---|---|---|
| Language | Rego (steep curve) | YAML (familiar) |
| Mutation | Có (newer) | Có (mature) |
| Use beyond K8s | Có (OPA generic) | K8s-only |
| Community | CNCF graduated | CNCF incubating |
| Image verify | Qua extension | Built-in cosign |
7. Audit cluster: kube-bench, kubescape
kube-bench — CIS Kubernetes Benchmark
# Chạy trên node (giả định kubeadm cluster)
docker run --rm --pid=host \
-v /etc:/etc:ro -v /var:/var:ro \
aquasec/kube-bench:latest run --targets node
# Output ví dụ
# [PASS] 4.2.1 Ensure that the --anonymous-auth argument is set to false
# [FAIL] 4.2.6 Ensure that the --make-iptables-util-chains argument is set to true
# [WARN] 4.2.9 Ensure that the --event-qps argument is set to 0 or a level which ensures appropriate event capture
CIS Benchmark gồm hàng trăm check cho apiserver flags, kubelet config, etcd permission. Quan trọng nhất với EKS: chạy với target --benchmark eks-1.5.0 (managed control plane bypass nhiều check, focus vào node + workload).
kubescape
# Scan theo NSA/CISA framework
kubescape scan framework nsa --submit=false
# Scan riêng workload trong namespace
kubescape scan --include-namespaces prod
Tích hợp được vào CI để fail PR nếu manifest vi phạm best practice (no-resource-limits, allow-privilege-escalation...).
8. Câu hỏi ôn tập
-
Tại sao PSP (Pod Security Policy) bị thay thế bằng Pod Security Standards?
Xem đáp án
PSP có nhiều vấn đề: (1) policy là K8s object riêng → khó áp dụng nhất quán, (2) RBAC giữa PSP và SA phức tạp, dễ mismatch im lặng, (3) ordering không xác định khi nhiều PSP match, (4) UX kém — admin phải tự viết policy mỗi cluster.
PSS chuẩn hoá 3 level (Privileged/Baseline/Restricted) — như "preset" — và apply bằng namespace label đơn giản. Pod Security Admission là built-in từ K8s 1.25, không cần cài thêm controller. PSP đã remove trong 1.25.
-
K8s Secret object có "an toàn" không? Phải làm gì để bảo vệ thật?
Xem đáp án
K8s Secret chỉ encode base64, không phải encrypt. Mặc định lưu plain trong etcd. Ai có quyền
get secretstrong NS, hoặc đọc được etcd, đều xem được giá trị.Layer phòng thủ thật: (1) Encryption-at-rest etcd với KMS provider, (2) RBAC hạn chế
get secrets, (3) dùng external secret store (Secrets Manager, Vault) — secret thật ở ngoài K8s, K8s chỉ giữ reference qua ESO hoặc Vault Agent Injector. Quan trọng hơn cả: secret có rotation và audit log riêng. -
NetworkPolicy có thực sự enforce trong mọi cluster không?
Xem đáp án
Không. NetworkPolicy là spec, enforcement phụ thuộc CNI plugin. Calico, Cilium, AWS VPC CNI (với enforcement bật), Azure CNI enforce đầy đủ. Flannel thuần thì không enforce. Nhiều team apply NetworkPolicy nhưng cluster chạy CNI không enforce → tưởng có firewall nhưng thực tế traffic vẫn open.
Phải verify: tạo policy block, exec vào pod test
nc -zvxem có bị deny không. -
Vì sao gắn
cluster-adminClusterRole cho ServiceAccount mặc định nguy hiểm?Xem đáp án
Mọi pod trong namespace mặc định dùng SA
default. Nếu SAdefaultcócluster-admin, bất kỳ pod nào trong NS đó cũng có toàn quyền cluster qua API server. Attacker RCE 1 pod → exec API call qua mounted token (/var/run/secrets/kubernetes.io/serviceaccount/token) → tạo Pod mới ởkube-systemvới hostPath/→ chiếm node → chiếm cluster.Best practice: (1) không cấp
cluster-admincho SA workload, (2)automountServiceAccountToken: falsenếu pod không gọi K8s API, (3) tạo SA riêng với Role minimal cho mỗi app. -
OPA Gatekeeper và Kyverno khác nhau ở điểm nào quan trọng nhất với team K8s?
Xem đáp án
Ngôn ngữ policy. Gatekeeper dùng Rego (OPA DSL) — mạnh, generic (dùng cho microservice authorization, Terraform validation...), nhưng steep learning curve, debug khó. Kyverno dùng YAML quen thuộc với K8s admin — viết policy như viết manifest, không cần học DSL mới, nhưng kém linh hoạt cho logic phức tạp.
Lựa chọn: team đã dùng OPA ở chỗ khác (API gateway, Terraform) → Gatekeeper consistent. Team thuần K8s, ưu tiên adoption nhanh → Kyverno. Kyverno còn built-in image signature verify (cosign), generate policy (auto-tạo NetworkPolicy/ResourceQuota khi tạo NS) — Gatekeeper cần extension.
Bài tập thực hành
# Cần một cluster (minikube, kind, hoặc EKS dev)
kind create cluster --name sec-lab
# 1. Áp Pod Security Standard restricted cho namespace
kubectl create namespace lab
kubectl label namespace lab \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted
# 2. Thử deploy pod vi phạm (chạy root) → bị reject
kubectl run nginx -n lab --image=nginx:1.27
# → Error: violates PodSecurity "restricted:v1.28": ...
# 3. Deploy pod đúng PSS Restricted
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: { name: ok, namespace: lab }
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: { type: RuntimeDefault }
containers:
- name: app
image: nginxinc/nginx-unprivileged:1.27
ports: [{ containerPort: 8080 }]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
EOF
# 4. Tạo NetworkPolicy default-deny + allow DNS
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: lab }
spec: { podSelector: {}, policyTypes: [Ingress, Egress] }
EOF
# 5. Cài Kyverno và viết policy
kubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.12.0/install.yaml
# Policy mẫu: bắt buộc label "owner" cho mọi pod
# 6. Chạy kube-bench
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench
# 7. Cleanup
kind delete cluster --name sec-lab
Tài liệu tham khảo chính thức
- Pod Security Standards
- Using RBAC Authorization
- Network Policies
- External Secrets Operator
- OPA Gatekeeper
- Kyverno docs
- CIS Kubernetes Benchmark