← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202619 phút đọc17 min read

SecuritySecurity

Part of the Kubernetes Roadmap knowledge base.

Tổng quan

Kubernetes mạnh mẽ chính vì nó là một API duy nhất có thể tạo, hủy và kết nối mọi thứ trong cluster — và đó cũng chính là lý do nó là mục tiêu giá trị cao. Một ServiceAccount token bị lộ, một RBAC binding quá rộng, hay một Pod privileged có thể leo thang từ một workload lên toàn bộ cluster và thường là vào cả cloud account bên dưới. Security không phải là một tính năng bạn bật lên; nó là một tập các mặc định bạn phải thay đổi, bởi Kubernetes ra mắt với trạng thái mở theo mặc định: mạng Pod phẳng và default-allow, Secret chỉ được base64-encode, và một Pod với đúng quyền có thể mount filesystem của host.

Một khung tư duy hữu ích là 4Cs of Cloud Native Security, một mô hình defense-in-depth trong đó mỗi tầng được bảo mật bên trong tầng bao ngoài nó:

Bạn không thể bảo mật một tầng trong để bù cho một tầng ngoài bị hỏng — một container được hardening hoàn hảo trên một cluster có cluster-admin bind vào system:anonymous thì vẫn mở toang. Trang này tập trung vào tầng Cluster và Container, nơi Kubernetes cho bạn nhiều control trực tiếp nhất: authentication (bạn là ai), authorization (bạn được làm gì), admission control (request cụ thể này có được phép không), và các control runtime cùng supply-chain ràng buộc những gì workload có thể làm khi đã chạy.

Kiến thức nền tảng

Vòng đời của một request

Mọi lời gọi tới API server đi qua ba cổng, theo thứ tự. Hiểu pipeline này giải thích mỗi control nằm ở đâu:

Một request đi qua API server
  1. 01kubectl / client
  2. 02Authenticationai?
  3. 03Authorizationđược phép?
  4. 04Admission Controlmutate + validate
  5. 05etcd
  1. Authentication — xác lập danh tính. Nếu không authenticator nào nhận ra credential, request bị từ chối như anonymous (hoặc fail). Kubernetes không có user object; danh tính đến từ certificate, token, hoặc một nhà cung cấp bên ngoài.
  2. Authorization — quyết định danh tính đó có được thực hiện verb này trên resource này không. Gần như luôn là RBAC. Deny by default: nếu không rule nào cấp quyền, nó bị từ chối.
  3. Admission control — một chuỗi plugin và webhook có thể mutate (chèn default, sidecar) rồi validate (chấp nhận/từ chối) object. Đây là nơi policy engine và Pod Security enforcement chạy, sau khi authz đã qua nhưng trước khi object được lưu.

Authentication — bạn là ai?

Kubernetes phân biệt hai loại danh tính:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-api
  namespace: prod
automountServiceAccountToken: false   # đừng mount token trừ khi Pod gọi API

Hai bước hardening quan trọng: đặt automountServiceAccountToken: false trên các ServiceAccount (hoặc Pod) không bao giờ nói chuyện với API server, và không bao giờ cấp quyền cho ServiceAccount default. Trên cloud, hãy bind một ServiceAccount vào một cloud IAM role — IRSA / EKS Pod Identity (AWS), Workload Identity (GKE/AKS) — để Pod nhận credential cloud có phạm vi hẹp qua SA token thay vì access key dài hạn.

Authorization — RBAC

Role-Based Access Control (RBAC) là authorizer chuẩn. Nó có đúng bốn loại object, chia theo phạm vi:

KindPhạm viCấp
RoleMột namespaceMột tập quyền (rule) trong namespace đó
ClusterRoleToàn clusterQuyền toàn cluster, hoặc trên resource cấp cluster (node, PV), hoặc một tập rule tái sử dụng
RoleBindingMột namespaceBind một Role hoặc một ClusterRole vào subject, có hiệu lực trong namespace đó
ClusterRoleBindingToàn clusterBind một ClusterRole vào subject trên mọi namespace

Hai điều hay khiến người ta vấp. Thứ nhất, RBAC là thuần cộng dồn và deny-by-default — không có deny rule; bạn chỉ cấp quyền, và bất cứ gì không được cấp đều bị cấm. Thứ hai, một RoleBinding có thể tham chiếu một ClusterRole: đây là cách chuẩn tắc để tái sử dụng một tập quyền (chẳng hạn “view”) xuyên nhiều namespace mà không định nghĩa lại — ClusterRole cung cấp rule, RoleBinding thu hẹp chúng vào namespace của nó.

Một rule là apiGroups × resources × verbs. Verb gồm get, list, watch, create, update, patch, delete, deletecollection. Wildcard (*) tồn tại nhưng gần như luôn là sai lầm ngoài các role admin thực sự.

# Một Role: quyền chỉ đọc Pod và log của chúng trong namespace "prod"
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: prod
  name: pod-reader
rules:
  - apiGroups: [""]                 # "" = core API group
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
# Bind Role đó vào một group (từ OIDC claim) và một ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: prod
  name: pod-reader-binding
subjects:
  - kind: Group
    name: "oncall-engineers"        # đến từ claim groups của token IdP
    apiGroup: rbac.authorization.k8s.io
  - kind: ServiceAccount
    name: log-collector
    namespace: prod
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Least privilege trong thực tế: bắt đầu từ số không và chỉ cấp những gì subject cần. Audit quyền truy cập bằng kubectl auth can-i --list --as=system:serviceaccount:prod:log-collector, và dùng công cụ (rbac-lookup, kubectl-who-can, rakkess) để tìm các cấp quyền thừa. Những tội lớn là bind cluster-admin vào một con người hay SA, dùng verbs: ["*"] trên resources: ["*"], cấp secrets rộng rãi (đọc được Secret = đọc được credential), và cho phép create trên pods/pods/exec trong một namespace có ServiceAccount privileged (một đường lateral-movement dẫn tới trộm token và leo thang).

Khái niệm chính

Admission control

Sau authentication và authorization, request tới chuỗi admission. Hai pha chạy nối tiếp:

  1. Mutating admission — webhook và plugin có thể thay đổi object: chèn một sidecar (service mesh), thêm label mặc định, đặt một securityContext, áp một defaultRequest. Mutation chạy trước để validation thấy object cuối cùng.
  2. Validating admission — webhook và plugin chấp nhận hoặc từ chối: bắt buộc image phải được ký, không Pod nào privileged, resource limit phải được đặt. Chúng không thể thay đổi object, chỉ phán xét.

Các plugin có sẵn (ví dụ ResourceQuota, PodSecurity, LimitRanger) được biên dịch vào. Ngoài chúng, dynamic admission webhook (MutatingWebhookConfiguration / ValidatingWebhookConfiguration) gọi ra các HTTPS service của riêng bạn, và đó là cách policy engine cắm vào. Một lựa chọn mới hơn, chạy in-process, là ValidatingAdmissionPolicy — các rule validation viết bằng CEL chạy ngay trong API server mà không cần webhook bên ngoài, tránh rủi ro availability và latency của một webhook có thể chặn mọi write nếu nó chết.

Pod Security Standards & Pod Security Admission

Pod Security Standards (PSS) định nghĩa ba mức policy cộng dồn:

MứcÝ nghĩaChặn
PrivilegedKhông hạn chế — mở hoàn toànKhông chặn gì (cho workload infra/system tin cậy)
BaselineHạn chế tối thiểu, ngăn các leo thang đã biếtContainer privileged, host namespace, hostPath, hầu hết capability thêm, hostPort
RestrictedĐược hardening, theo best practice hiện tạiMọi thứ Baseline chặn cộng thêm yêu cầu runAsNonRoot, seccomp RuntimeDefault, drop ALL capability, không cho privilege escalation, loại volume hạn chế

Pod Security Admission (PSA) là admission controller có sẵn (GA từ v1.25) thực thi các standard này theo từng namespace qua label. Nó thay thế PodSecurityPolicy đã deprecated. Mỗi mức có thể chạy ở ba mode độc lập: enforce (từ chối Pod vi phạm), audit (cho phép nhưng log), warn (cho phép nhưng trả cảnh báo cho người dùng). Một cách rollout an toàn là trước tiên warn+auditrestricted, quan sát cái gì vỡ, rồi chuyển sang enforce.

apiVersion: v1
kind: Namespace
metadata:
  name: prod
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted        # cảnh báo cùng mức cho người dùng kubectl
    pod-security.kubernetes.io/audit: restricted

PSA thô (ba mức cố định, cả namespace) — để có rule tùy biến mịn hơn (“không tag :latest”, “chỉ image từ registry của chúng ta”, “mỗi Pod cần một label team”) bạn cần một policy engine.

Policy engine — OPA Gatekeeper & Kyverno

Policy engine là các validating (và mutating) admission webhook thực thi các rule tổ chức tùy ý, và cũng có thể audit resource hiện có để tìm vi phạm.

OPA GatekeeperKyverno
Ngôn ngữ policyRego (Open Policy Agent)YAML (thuần Kubernetes)
Độ khóDốc hơn (phải học Rego)Dễ (trông như manifest K8s)
Khả năngValidate, auditValidate, mutate, generate, verify image, cleanup
Tái sử dụngConstraintTemplate + Constraint theo templateClusterPolicy tự chứa
Hợp nhấtLogic phức tạp, đa cloud (OPA ở khắp nơi)Nơi chỉ dùng Kubernetes muốn sự đơn giản

Một Kyverno policy yêu cầu securityContext runAsNonRoot (validate qua một pattern khai báo):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-run-as-non-root
spec:
  validationFailureAction: Enforce      # Enforce = chặn; Audit = chỉ báo cáo
  background: true
  rules:
    - name: check-run-as-non-root
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Container phải đặt securityContext.runAsNonRoot=true."
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true

Cách tương đương của Gatekeeper là một ConstraintTemplate tái sử dụng viết bằng Rego cộng một Constraint áp dụng nó:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequirenonroot
spec:
  crd:
    spec:
      names: { kind: K8sRequireNonRoot }
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequirenonroot
        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          not c.securityContext.runAsNonRoot == true
          msg := sprintf("container %v phải đặt runAsNonRoot=true", [c.name])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
  name: require-non-root
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]

securityContext — ràng buộc container

securityContext là bộ nút cấp Pod/cấp container để hardening runtime. Nó là cơ chế cụ thể mà mức PSS restricted và policy ở trên kiểm tra. Block chuẩn vàng:

spec:
  securityContext:                 # cấp Pod
    runAsNonRoot: true             # từ chối chạy dưới UID 0
    runAsUser: 10001
    fsGroup: 10001                 # quyền sở hữu group cho volume được mount
    seccompProfile:
      type: RuntimeDefault         # áp filter syscall mặc định của runtime
  containers:
    - name: app
      image: registry.example.com/app@sha256:abc123...   # pin theo digest
      securityContext:             # cấp container (ghi đè cấp Pod)
        allowPrivilegeEscalation: false   # chặn leo thang setuid (không giành thêm quyền)
        readOnlyRootFilesystem: true      # FS container bất biến; chỉ ghi vào volume được mount
        privileged: false
        capabilities:
          drop: ["ALL"]                   # drop mọi Linux capability...
          add: ["NET_BIND_SERVICE"]       # ...rồi thêm lại đúng cái thực sự cần

Mỗi control mang lại gì:

NetworkPolicy — cô lập mạng

Theo mặc định mọi Pod đều có thể tới mọi Pod khác trên mọi namespace — mạng phẳng và default-allow. Một NetworkPolicy đổi điều này thành default-deny cho các Pod được chọn, rồi allow-list lưu lượng cụ thể. NetworkPolicy được thực thi bởi CNI plugin (Calico, Cilium, v.v.) — áp một cái trên một CNI không hỗ trợ chúng thì âm thầm không làm gì.

Bước nền tảng là một policy default-deny cho mỗi namespace, rồi allow tường minh:

# 1) Deny toàn bộ ingress + egress trong namespace (selector rỗng = mọi Pod)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
---
# 2) Cho phép Pod api chỉ nhận lưu lượng từ Pod web, trên cổng 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-api
  namespace: prod
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: web }
      ports:
        - protocol: TCP
          port: 8080

Khi bạn bật một policy default-deny egress, nhớ cho phép DNS tường minh (UDP/TCP 53 tới kube-dns) nếu không mọi lookup tên sẽ vỡ — một cạm bẫy kinh điển lần đầu. NetworkPolicy chọn theo label Pod/namespace và CIDR, không theo IP; để có rule L7 phong phú hơn (method/path HTTP), hãy dùng Cilium NetworkPolicy hoặc một service mesh.

Secret — mã hóa at rest

Secret của Kubernetes không được mã hóa mặc định — chúng được lưu base64-encode trong etcd, thứ dễ dàng giải ngược. Bất cứ ai có quyền truy cập etcd, một bản backup etcd, hoặc RBAC get secrets rộng đều đọc được. Ba lớp bảo vệ:

  1. Mã hóa at rest — cấu hình một EncryptionConfiguration trên API server để Secret được mã hóa trước khi vào etcd. Hãy ưu tiên một KMS provider (cloud KMS hoặc Vault qua KMS plugin) hơn key lưu cục bộ, để key mã hóa không nằm trên node control-plane. Sắp xếp provider với cái mã hóa đứng đầu; aescbc/secretbox là các lựa chọn thay cho KMS.
  2. RBAC — coi get/list trên secrets là cực kỳ privileged; thu hẹp thật chặt. Đọc một Secret là đọc một credential.
  3. External secret manager — giữ nguồn sự thật trong AWS Secrets Manager, Vault, hoặc GCP Secret Manager và đồng bộ vào cluster bằng External Secrets Operator, hoặc inject lúc runtime bằng Secrets Store CSI driver — để secret không bị commit vào Git và rotation được tập trung. Không bao giờ commit Secret plaintext vào Git; nếu buộc phải lưu trong Git, hãy seal chúng (Sealed Secrets) hoặc mã hóa bằng SOPS.

Bảo mật image & supply-chain

Container chỉ đáng tin bằng image và nguồn gốc của nó. Một phòng thủ supply-chain nhiều lớp:

Best Practices

  1. Thực thi RBAC least-privilege và không bao giờ bind cluster-admin rộng rãi. Cấp verb tối thiểu trên resource tối thiểu trong phạm vi tối thiểu. Audit bằng kubectl auth can-i --list và công cụ như rbac-lookup; coi verbs: ["*"], resources: ["*"], và quyền đọc secrets rộng là các phát hiện cần xử lý.

  2. Cấp cho mỗi workload một ServiceAccount riêng và tắt automount token khi không dùng. Không bao giờ cấp quyền cho SA default. Đặt automountServiceAccountToken: false trên Pod không gọi API, để một Pod bị chiếm không có cluster credential để trộm.

  3. Dùng OIDC (IdP của bạn) cho authentication con người, không dùng client cert hay token dùng chung. Cert không revoke sạch được và token dùng chung không quy trách nhiệm được. OIDC cho danh tính tập trung, map group và revocation.

  4. Thực thi Pod Security Admission ở restricted (hoặc tối thiểu baseline) trên mọi namespace workload. Rollout với warn+audit trước, sửa vi phạm, rồi enforce. Chỉ giữ privileged cho các namespace system đã kiểm định.

  5. Áp securityContext được hardening cho mọi workload. runAsNonRoot, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities.drop: ["ALL"], và seccompProfile: RuntimeDefault. Đây là các control cụ thể đằng sau standard restricted.

  6. Không bao giờ chạy container privileged hay mount host filesystem/namespace trừ hạ tầng đã kiểm định. privileged: true, hostPID, hostNetwork, và hostPath là các vector thoát container — chặn chúng bằng Pod Security và một policy engine.

  7. Áp dụng NetworkPolicy default-deny và allow-list các luồng cần thiết. Mạng phẳng, mở theo mặc định là một xa lộ lateral-movement. Bắt đầu với default-deny ingress+egress cho mỗi namespace, rồi allow tường minh lưu lượng cần — và nhớ cho phép DNS.

  8. Bật mã hóa Secret at rest với một KMS provider. base64 trong etcd không phải mã hóa. Mã hóa trước khi lưu, giữ key trong cloud KMS/Vault, và thu hẹp chặt RBAC get secrets.

  9. Giữ secret ngoài Git; dùng external manager hoặc sealed/encrypted secret. Đồng bộ từ Vault/Secrets Manager qua External Secrets Operator hoặc Secrets Store CSI driver, hoặc chỉ commit giá trị Sealed Secrets/SOPS-encrypted. Rotate tập trung.

  10. Quét image tìm CVE trong CI và liên tục, fail build khi có phát hiện critical. Dùng Trivy/Grype, sinh SBOM, và dùng base image tối giản (distroless/scratch) để giữ bề mặt tấn công nhỏ.

  11. Ký image bằng cosign và verify chữ ký tại admission. Thực thi rằng chỉ image do pipeline của bạn build và ký mới được chạy, qua Kyverno verifyImages, Binary Authorization, hoặc Connaisseur — đánh bại image bị chỉnh sửa và typosquat.

  12. Triển khai một policy engine (Kyverno hoặc Gatekeeper) làm lan can toàn tổ chức. Thực thi các rule Pod Security không diễn đạt được: cấm :latest, yêu cầu resource limit và probe, hạn chế registry, bắt buộc label. Chạy ở audit mode trước để tìm vi phạm hiện có.

  13. Ưu tiên CEL ValidatingAdmissionPolicy trong cluster hoặc webhook highly-available. Một validating webhook một replica là rủi ro outage toàn cluster — nếu nó chết, mọi write fail. Dùng ValidatingAdmissionPolicy (không service ngoài) hoặc chạy webhook HA với failurePolicy hợp lý.

  14. Bind ServiceAccount vào cloud IAM role có phạm vi (IRSA / Workload Identity). Không bao giờ nướng access key cloud dài hạn vào Pod. Federate SA token sang một cloud role least-privilege để credential ngắn hạn và theo từng workload.

  15. Hardening tầng cloud và cluster, không chỉ workload. Hạn chế truy cập mạng vào API server, giữ control plane và node được vá, bảo mật và backup etcd, bật audit logging, và thực thi upgrade — 4Cs nghĩa là một điểm yếu tầng ngoài đánh bại hardening tầng trong.

  16. Bật audit logging và runtime threat detection. Bật audit log của API server để biết ai làm gì, và chạy một runtime sensor (Falco, hoặc tương đương cloud/thương mại) để bắt thoát container, exec bất thường, và crypto-mining mà control lúc admission không thấy được.

Tài liệu tham khảo

Part of the Kubernetes Roadmap knowledge base.

Overview

Kubernetes is powerful precisely because it is a single API that can create, destroy, and connect anything in your cluster — which is exactly why it is a high-value target. A compromised ServiceAccount token, an over-permissive RBAC binding, or a privileged Pod can escalate from one workload to the entire cluster and often into the underlying cloud account. Security is not a feature you switch on; it is a set of defaults you must change, because Kubernetes ships open by default: the Pod network is flat and default-allow, Secrets are only base64-encoded, and a Pod with the right permissions can mount the host filesystem.

A useful frame is the 4Cs of Cloud Native Security, a defense-in-depth model where each layer is secured within the one outside it:

You cannot secure an inner layer to compensate for a broken outer one — a perfectly hardened container on a cluster with cluster-admin bound to system:anonymous is still wide open. This page focuses on the Cluster and Container layers, where Kubernetes gives you the most direct controls: authentication (who are you), authorization (what may you do), admission control (should this specific request be allowed), and the runtime and supply-chain controls that constrain what workloads can do once running.

Fundamentals

The request lifecycle

Every call to the API server passes through three gates, in order. Understanding this pipeline explains where each control lives:

A request through the API server
  1. 01kubectl / client
  2. 02Authenticationwho?
  3. 03Authorizationallowed?
  4. 04Admission Controlmutate + validate
  5. 05etcd
  1. Authentication — establishes identity. If no authenticator recognizes the credential, the request is rejected as anonymous (or fails). Kubernetes has no user objects; identity comes from certificates, tokens, or an external provider.
  2. Authorization — decides whether that identity may perform this verb on this resource. Almost always RBAC. Deny by default: if no rule grants access, it is denied.
  3. Admission control — a chain of plugins and webhooks that can mutate (inject defaults, sidecars) and then validate (accept/reject) the object. This is where policy engines and Pod Security enforcement run, after authz has passed but before the object is persisted.

Authentication — who are you?

Kubernetes distinguishes two identity types:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-api
  namespace: prod
automountServiceAccountToken: false   # don't mount a token unless the Pod calls the API

Two important hardening moves: set automountServiceAccountToken: false on ServiceAccounts (or Pods) that never talk to the API server, and never grant permissions to the default ServiceAccount. In the cloud, bind a ServiceAccount to a cloud IAM role — IRSA / EKS Pod Identity (AWS), Workload Identity (GKE/AKS) — so Pods get scoped cloud credentials via their SA token instead of long-lived access keys.

Authorization — RBAC

Role-Based Access Control (RBAC) is the standard authorizer. It has exactly four object kinds, split by scope:

KindScopeGrants
RoleOne namespaceA set of permissions (rules) within that namespace
ClusterRoleCluster-widePermissions cluster-wide, or on cluster-scoped resources (nodes, PVs), or a reusable rule set
RoleBindingOne namespaceBinds a Role or a ClusterRole to subjects, effective in that namespace
ClusterRoleBindingCluster-wideBinds a ClusterRole to subjects across all namespaces

Two things trip people up. First, RBAC is purely additive and deny-by-default — there are no deny rules; you only grant, and anything not granted is forbidden. Second, a RoleBinding can reference a ClusterRole: this is the idiomatic way to reuse one permission set (say, “view”) across many namespaces without redefining it — the ClusterRole supplies the rules, the RoleBinding scopes them to its namespace.

A rule is apiGroups × resources × verbs. Verbs are get, list, watch, create, update, patch, delete, deletecollection. Wildcards (*) exist but are almost always a mistake outside genuine admin roles.

# A Role: read-only access to Pods and their logs in the "prod" namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: prod
  name: pod-reader
rules:
  - apiGroups: [""]                 # "" = core API group
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
# Bind that Role to a group (from OIDC claims) and a ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: prod
  name: pod-reader-binding
subjects:
  - kind: Group
    name: "oncall-engineers"        # comes from the IdP token's groups claim
    apiGroup: rbac.authorization.k8s.io
  - kind: ServiceAccount
    name: log-collector
    namespace: prod
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Least privilege in practice: start from zero and grant only what a subject needs. Audit access with kubectl auth can-i --list --as=system:serviceaccount:prod:log-collector, and use tooling (rbac-lookup, kubectl-who-can, rakkess) to find over-grants. The cardinal sins are binding cluster-admin to a human or SA, using verbs: ["*"] on resources: ["*"], granting secrets broadly (readable Secrets = readable credentials), and permitting create on pods/pods/exec in a namespace with privileged ServiceAccounts (a lateral-movement path to token theft and escalation).

Key Concepts

Admission control

After authentication and authorization, the request hits the admission chain. Two phases run in sequence:

  1. Mutating admission — webhooks and plugins can change the object: inject a sidecar (service mesh), add default labels, set a securityContext, apply a defaultRequest. Mutations run first so validation sees the final object.
  2. Validating admission — webhooks and plugins accept or reject: enforce that images are signed, that no Pod is privileged, that resource limits are set. They cannot change the object, only pass judgment.

Built-in plugins (e.g. ResourceQuota, PodSecurity, LimitRanger) are compiled in. Beyond them, dynamic admission webhooks (MutatingWebhookConfiguration / ValidatingWebhookConfiguration) call out to your own HTTPS services, which is how policy engines plug in. A newer, in-process option is ValidatingAdmissionPolicy — validation rules written in CEL that run inside the API server without an external webhook, avoiding the availability and latency risk of a webhook that could block all writes if it goes down.

Pod Security Standards & Pod Security Admission

The Pod Security Standards (PSS) define three cumulative policy levels:

LevelMeaningBlocks
PrivilegedUnrestricted — no restrictionsNothing (for trusted infra/system workloads)
BaselineMinimally restrictive, prevents known privilege escalationsprivileged containers, host namespaces, hostPath, most added capabilities, hostPorts
RestrictedHardened, follows current best practiceEverything Baseline blocks plus requires runAsNonRoot, seccomp RuntimeDefault, drop ALL capabilities, no privilege escalation, restricted volume types

Pod Security Admission (PSA) is the built-in (GA since v1.25) admission controller that enforces these standards per namespace via labels. It replaced the deprecated PodSecurityPolicy. Each level can run in three modes independently: enforce (reject violating Pods), audit (allow but log), warn (allow but return a user-facing warning). A safe rollout is to first warn+audit at restricted, watch what breaks, then flip to enforce.

apiVersion: v1
kind: Namespace
metadata:
  name: prod
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted        # warn on the same level for kubectl users
    pod-security.kubernetes.io/audit: restricted

PSA is coarse (three fixed levels, whole-namespace) — for finer, custom rules (“no :latest tag”, “images only from our registry”, “every Pod needs a team label”) you need a policy engine.

Policy engines — OPA Gatekeeper & Kyverno

Policy engines are validating (and mutating) admission webhooks that enforce arbitrary organizational rules, and can also audit existing resources for violations.

OPA GatekeeperKyverno
Policy languageRego (Open Policy Agent)YAML (Kubernetes-native)
Learning curveSteeper (learn Rego)Gentle (looks like K8s manifests)
CapabilitiesValidate, auditValidate, mutate, generate, verify images, cleanup
ReuseTemplated ConstraintTemplate + ConstraintSelf-contained ClusterPolicy
Best forComplex logic, multi-cloud (OPA everywhere)Kubernetes-only shops wanting simplicity

A Kyverno policy that requires the runAsNonRoot securityContext (validation via a declarative pattern):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-run-as-non-root
spec:
  validationFailureAction: Enforce      # Enforce = block; Audit = report only
  background: true
  rules:
    - name: check-run-as-non-root
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Containers must set securityContext.runAsNonRoot=true."
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true

The equivalent Gatekeeper approach is a reusable ConstraintTemplate written in Rego plus a Constraint that applies it:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequirenonroot
spec:
  crd:
    spec:
      names: { kind: K8sRequireNonRoot }
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequirenonroot
        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          not c.securityContext.runAsNonRoot == true
          msg := sprintf("container %v must set runAsNonRoot=true", [c.name])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
  name: require-non-root
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]

securityContext — constraining the container

The securityContext is the per-Pod/per-container knob set that hardens the runtime. It is the concrete mechanism the restricted PSS level and the policy above check for. The gold-standard block:

spec:
  securityContext:                 # Pod-level
    runAsNonRoot: true             # refuse to start as UID 0
    runAsUser: 10001
    fsGroup: 10001                 # group ownership for mounted volumes
    seccompProfile:
      type: RuntimeDefault         # apply the runtime's default syscall filter
  containers:
    - name: app
      image: registry.example.com/app@sha256:abc123...   # pin by digest
      securityContext:             # container-level (overrides Pod-level)
        allowPrivilegeEscalation: false   # block setuid escalation (no gaining new privs)
        readOnlyRootFilesystem: true      # immutable container FS; write only to mounted volumes
        privileged: false
        capabilities:
          drop: ["ALL"]                   # drop every Linux capability...
          add: ["NET_BIND_SERVICE"]       # ...then add back only what's truly needed

What each control buys you:

NetworkPolicy — network isolation

By default every Pod can reach every other Pod across all namespaces — the network is flat and default-allow. A NetworkPolicy changes this to default-deny for selected Pods, then allow-lists specific traffic. NetworkPolicies are enforced by the CNI plugin (Calico, Cilium, etc.) — applying one on a CNI that doesn’t support them silently does nothing.

The foundational move is a default-deny policy per namespace, then explicit allows:

# 1) Deny all ingress + egress in the namespace (empty selector = all Pods)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
---
# 2) Allow the api Pods to receive traffic only from the web Pods, on 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-api
  namespace: prod
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: web }
      ports:
        - protocol: TCP
          port: 8080

Once you enable a default-deny egress policy, remember to explicitly allow DNS (UDP/TCP 53 to kube-dns) or every name lookup breaks — a classic first-time pitfall. NetworkPolicy selects by Pod/namespace labels and CIDR, not IPs; for richer L7 (HTTP method/path) rules, use Cilium NetworkPolicy or a service mesh.

Secrets — encryption at rest

Kubernetes Secrets are not encrypted by default — they are stored base64-encoded in etcd, which is trivially reversible. Anyone with etcd access, an etcd backup, or broad get secrets RBAC can read them. Three layers of protection:

  1. Encryption at rest — configure an EncryptionConfiguration on the API server so Secrets are encrypted before hitting etcd. Prefer a KMS provider (cloud KMS or Vault via the KMS plugin) over a locally-stored key, so the encryption key isn’t sitting on the control-plane node. Order providers with the encrypting one first; aescbc/secretbox are alternatives to KMS.
  2. RBAC — treat get/list on secrets as highly privileged; scope it tightly. Reading a Secret is reading a credential.
  3. External secret managers — keep the source of truth in AWS Secrets Manager, Vault, or GCP Secret Manager and sync into the cluster with the External Secrets Operator, or inject at runtime with the Secrets Store CSI driver — so secrets aren’t committed to Git and rotation is centralized. Never commit plaintext Secrets to Git; if you must store them in Git, seal them (Sealed Secrets) or encrypt with SOPS.

Image & supply-chain security

The container is only as trustworthy as the image and its provenance. A layered supply-chain defense:

Best Practices

  1. Enforce least-privilege RBAC and never bind cluster-admin broadly. Grant the minimum verbs on the minimum resources in the minimum scope. Audit with kubectl auth can-i --list and tools like rbac-lookup; treat verbs: ["*"], resources: ["*"], and broad secrets read as findings.

  2. Give every workload a dedicated ServiceAccount and disable token automount when unused. Never grant permissions to the default SA. Set automountServiceAccountToken: false on Pods that don’t call the API, so a compromised Pod has no cluster credential to steal.

  3. Use OIDC (your IdP) for human authentication, not client certs or shared tokens. Certs can’t be revoked cleanly and shared tokens can’t be attributed. OIDC gives central identity, group mapping, and revocation.

  4. Enforce Pod Security Admission at restricted (or baseline minimum) on every workload namespace. Roll out with warn+audit first, fix violations, then enforce. Keep privileged only for vetted system namespaces.

  5. Apply the hardened securityContext to every workload. runAsNonRoot, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities.drop: ["ALL"], and seccompProfile: RuntimeDefault. These are the concrete controls behind the restricted standard.

  6. Never run privileged containers or mount the host filesystem/namespaces except for vetted infrastructure. privileged: true, hostPID, hostNetwork, and hostPath are container-escape vectors — block them with Pod Security and a policy engine.

  7. Adopt default-deny NetworkPolicies and allow-list required flows. The flat, open-by-default network is a lateral-movement highway. Start with default-deny ingress+egress per namespace, then explicitly allow needed traffic — and remember to allow DNS.

  8. Enable Secret encryption at rest with a KMS provider. base64 in etcd is not encryption. Encrypt before persistence, keep the key in cloud KMS/Vault, and tightly scope get secrets RBAC.

  9. Keep secrets out of Git; use an external manager or sealed/encrypted secrets. Sync from Vault/Secrets Manager via the External Secrets Operator or Secrets Store CSI driver, or commit only Sealed Secrets/SOPS-encrypted values. Rotate centrally.

  10. Scan images for CVEs in CI and continuously, and fail builds on critical findings. Use Trivy/Grype, generate an SBOM, and use minimal (distroless/scratch) base images to keep the attack surface small.

  11. Sign images with cosign and verify signatures at admission. Enforce that only images built and signed by your pipeline can run, via Kyverno verifyImages, Binary Authorization, or Connaisseur — defeating tampered and typosquatted images.

  12. Deploy a policy engine (Kyverno or Gatekeeper) for org-wide guardrails. Enforce rules Pod Security can’t express: forbid :latest, require resource limits and probes, restrict registries, mandate labels. Run in audit mode first to find existing violations.

  13. Prefer in-cluster CEL ValidatingAdmissionPolicies or highly-available webhooks. A single-replica validating webhook is a cluster-wide outage risk — if it’s down, writes fail. Use ValidatingAdmissionPolicy (no external service) or run webhooks HA with a sane failurePolicy.

  14. Bind ServiceAccounts to scoped cloud IAM roles (IRSA / Workload Identity). Never bake long-lived cloud access keys into Pods. Federate the SA token to a least-privilege cloud role so credentials are short-lived and per-workload.

  15. Harden the cloud and cluster layers, not just workloads. Restrict API-server network access, keep the control plane and nodes patched, secure and back up etcd, enable audit logging, and enforce upgrades — the 4Cs mean an outer-layer weakness defeats inner-layer hardening.

  16. Enable audit logging and runtime threat detection. Turn on the API-server audit log to know who did what, and run a runtime sensor (Falco, or a cloud/commercial equivalent) to catch container escapes, unexpected exec, and crypto-mining that admission-time controls can’t see.

References