</>Học Dev
Bài học

Tuần 4 - Ngày 21: Kubernetes Security

Tuần 4 – Ngày 21

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

KubernetesClusterControlPlanekube-apiserverfrontdoor,RBACenforcement(kubectl/CIgivào)etcdkey-valuestorechosecret+configencryptatrestSchedulerchnnodechopodControllerMgrreconcileloopDataPlane(Nodes1..N)kubeletworkeragent,giapiservercerts+authcontainerruntime(containerd/CRI-O)Podsworkload(appcontainers)
ComponentLỗ hổng phổ biếnMitigation
kube-apiserverAnonymous auth, unbounded RBAC, exposed port 6443Disable anonymous, audit log, NetworkPolicy/SG hạn chế IP
etcdPlain-text secret on disk, exposed port 2379Encryption-at-rest (EncryptionConfiguration), mTLS, không expose internet
kubeletPort 10250 read/write API, anonymous request--anonymous-auth=false, --authorization-mode=Webhook
Container runtimerunc CVE, image với CVEPatch runc, image scanning
WorkloadExcessive permission, container escapePod 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

PrivilegedBaselineRestricted(norestriction)(noknownescalation)(hardenedbestpractice)allowed:disallowed:thêmdisallowed:-micapability-hostNetwork-non-rootuser-hostNetwork-hostPID/IPC-allowPrivilegeEscalation=false-privileged=true-hostPath-capabilitiesdropALL-hostPath-privileged=true-seccomp=RuntimeDefault-CAP_SYS_ADMIN-readOnlyRootFilesystem(recommended)
LevelUse case
PrivilegedSystem daemon (CNI, CSI, monitoring agent) — chỉ namespace cụ thể
BaselineWorkload thường, không cần host access
RestrictedProduction 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

ScopecaResourceNamespacedvsCluster-scopedRoleClusterRoleRoleBindingClusterRoleBindingServiceAccountNode,PV,Pod,Service,ConfigMapNamespace,CRD
ObjectPhạm viDùng cho
Role1 namespaceQuyền trong namespace đó (Pod, Service, ConfigMap...)
ClusterRoleToàn clusterQuyền cluster-wide (Node, PV) hoặc reusable across NS
RoleBinding1 namespaceGán Role hoặc ClusterRole cho subject trong NS
ClusterRoleBindingToàn clusterGán ClusterRole cho subject áp cho mọi NS
ServiceAccount1 namespaceIdentity 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ự

  1. Encryption-at-rest cho etcd: cấu hình EncryptionConfiguration với provider KMS (AWS KMS / GCP KMS / Vault). EKS/GKE managed mặc định bật.
  2. RBAC trên Secret object: hạn chế get secrets chỉ ServiceAccount cần thiết.
  3. 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 GatekeeperKyverno
LanguageRego (steep curve)YAML (familiar)
MutationCó (newer)Có (mature)
Use beyond K8sCó (OPA generic)K8s-only
CommunityCNCF graduatedCNCF incubating
Image verifyQua extensionBuilt-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

  1. 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.

  2. 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 secrets trong 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.

  3. 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 -zv xem có bị deny không.

  4. Vì sao gắn cluster-admin ClusterRole 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 SA defaultcluster-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-system với hostPath / → chiếm node → chiếm cluster.

    Best practice: (1) không cấp cluster-admin cho SA workload, (2) automountServiceAccountToken: false nếu pod không gọi K8s API, (3) tạo SA riêng với Role minimal cho mỗi app.

  5. 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


Bài tiếp theo →