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ó:
- Cloud — hạ tầng: cloud account, endpoint của control plane, hardening OS của node, IAM, ranh giới mạng. Tầng nền tảng và nhiều đặc quyền nhất; một điểm yếu ở đây làm sụp đổ mọi thứ bên trên.
- Cluster — bản thân Kubernetes: authentication, RBAC, admission control, Pod Security, network policy, mã hóa Secret at rest, bảo mật etcd và API server.
- Container — image và runtime: quét lỗ hổng, ký và xác minh nguồn gốc, drop capability, chạy non-root.
- Code — ứng dụng của bạn: quản lý dependency, validate input, TLS, xử lý secret. Tầng trong cùng và là tầng bạn kiểm soát hoàn toà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:
- 01kubectl / client
- 02Authenticationai?
- 03Authorizationđược phép?
- 04Admission Controlmutate + validate
- 05etcd
- 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.
- 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.
- 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:
- Normal user (con người) — quản lý bên ngoài Kubernetes. Không có object
User. Một user là bất cứ thứ gì một authenticator đảm bảo:- X.509 client certificate — CN trở thành username, các trường Organization trở thành group. Đơn giản nhưng khó revoke (không có CRL sẵn) — tránh dùng cho con người ở quy mô lớn.
- OIDC token — cách được khuyến nghị cho con người. Kubernetes tin một identity provider bên ngoài (Okta, Entra ID, Google, Dex, Keycloak); user đăng nhập ở đó và xuất trình một JWT đã ký. Group và danh tính đến từ các claim trong token, nên bạn quản lý con người trong IdP và revoke tập trung.
- Authenticating proxy / cloud IAM (EKS
aws-iam-authenticator/ access entries, GKE/AKS IAM) — map danh tính cloud sang user Kubernetes.
- ServiceAccount (workload) — object Kubernetes hạng nhất, có namespace, dùng bởi Pod để authenticate với API. Mỗi Pod có một ServiceAccount (SA
defaultcủa namespace nếu không chỉ định). Cluster hiện đại cấp projected token ngắn hạn, gắn audience (quaTokenRequest) tự động mount tại/var/run/secrets/kubernetes.io/serviceaccount/token, thay cho token dựa trên Secret không bao giờ hết hạn kiểu cũ.
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:
| Kind | Phạm vi | Cấp |
|---|---|---|
| Role | Một namespace | Một tập quyền (rule) trong namespace đó |
| ClusterRole | Toàn cluster | Quyề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 |
| RoleBinding | Một namespace | Bind một Role hoặc một ClusterRole vào subject, có hiệu lực trong namespace đó |
| ClusterRoleBinding | Toàn cluster | Bind 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:
- 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. - 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ĩa | Chặn |
|---|---|---|
| Privileged | Không hạn chế — mở hoàn toàn | Không chặn gì (cho workload infra/system tin cậy) |
| Baseline | Hạn chế tối thiểu, ngăn các leo thang đã biết | Container privileged, host namespace, hostPath, hầu hết capability thêm, hostPort |
| Restricted | Được hardening, theo best practice hiện tại | Mọ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+audit ở restricted, 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 Gatekeeper | Kyverno | |
|---|---|---|
| Ngôn ngữ policy | Rego (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ăng | Validate, audit | Validate, mutate, generate, verify image, cleanup |
| Tái sử dụng | ConstraintTemplate + Constraint theo template | ClusterPolicy tự chứa |
| Hợp nhất | Logic 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ì:
runAsNonRoot/runAsUser— một process root thoát khỏi container là root trên node; chạy dưới UID khác 0 kiềm chế phạm vi thiệt hại.allowPrivilegeEscalation: false— ngăn một process giành thêm quyền so với process cha (chặn binary setuid), đóng một đường leo thang phổ biến.readOnlyRootFilesystem: true— kẻ tấn công không thể thả công cụ hay sửa binary trong container; mountemptyDircho vài đường dẫn cần ghi.capabilities.drop: ["ALL"]— Linux capability là các quyền root chi tiết; drop hết rồi thêm lại đúng cái cần (ví dụNET_BIND_SERVICEđể bind port <1024).seccompProfile: RuntimeDefault— hạn chế các syscall container được gọi về tập mặc định đã kiểm định của runtime, thu nhỏ bề mặt tấn công vào kernel.privileged: true— ngược lại với tất cả những điều trên; nó cấp quyền gần như của host. Hãy coi nó là cờ đỏ chỉ dành cho một số rất ít DaemonSet system/infra.
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ệ:
- Mã hóa at rest — cấu hình một
EncryptionConfigurationtrê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/secretboxlà các lựa chọn thay cho KMS. - RBAC — coi
get/listtrênsecretslà cực kỳ privileged; thu hẹp thật chặt. Đọc một Secret là đọc một credential. - 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:
- Quét image tìm CVE đã biết trước và sau khi deploy — Trivy, Grype, hoặc scanner của registry — và fail build CI khi có phát hiện high/critical. Sinh một SBOM (Syft) để biết bên trong có gì.
- Dùng base image tối giản (distroless, Alpine, scratch) để thu nhỏ bề mặt tấn công — ít package, ít CVE, không có shell cho kẻ tấn công dùng.
- Pin theo digest (
image@sha256:...), không dùng tag khả biến;:latestkhông có version và không tái lập được. - Ký image và verify tại admission. Ký bằng cosign (Sigstore) lúc build, rồi chặn image không ký hoặc không tin cậy tại admission — Kyverno (
verifyImages), Connaisseur, hoặc cổng có sẵn của cloud (GKE Binary Authorization, AWS Signer + ECR). Điều này đảm bảo chỉ image do pipeline của bạn build và ký mới được chạy, đánh bại image bị chỉnh sửa hay typosquat. - Dùng registry riêng với access control và pull-through cache; tránh pull image công khai tùy tiện lúc runtime.
Best Practices
-
Thực thi RBAC least-privilege và không bao giờ bind
cluster-adminrộ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ằngkubectl auth can-i --listvà công cụ nhưrbac-lookup; coiverbs: ["*"],resources: ["*"], và quyền đọcsecretsrộng là các phát hiện cần xử lý. -
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. ĐặtautomountServiceAccountToken: falsetrên Pod không gọi API, để một Pod bị chiếm không có cluster credential để trộm. -
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.
-
Thực thi Pod Security Admission ở
restricted(hoặc tối thiểubaseline) trên mọi namespace workload. Rollout vớiwarn+audittrước, sửa vi phạm, rồienforce. Chỉ giữprivilegedcho các namespace system đã kiểm định. -
Á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 standardrestricted. -
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àhostPathlà các vector thoát container — chặn chúng bằng Pod Security và một policy engine. -
Á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.
-
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. -
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.
-
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ỏ.
-
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. -
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ó. -
Ư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ớifailurePolicyhợp lý. -
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.
-
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.
-
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
- Overview of Cloud Native Security (the 4Cs)
- Kubernetes Security Concepts
- Authenticating
- Managing Service Accounts
- Using RBAC Authorization
- Admission Controllers Reference
- Dynamic Admission Control (webhooks)
- Validating Admission Policy (CEL)
- Pod Security Standards · Pod Security Admission
- Set the security context for a Pod/Container
- Network Policies
- Encrypting Secret Data at Rest
- Kyverno · OPA Gatekeeper
- Sigstore cosign · External Secrets Operator · Falco
- roadmap.sh — Kubernetes
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:
- Cloud — the infrastructure: the cloud account, the control-plane endpoint, node OS hardening, IAM, network boundaries. The most privileged and foundational layer; a weakness here undermines everything above.
- Cluster — Kubernetes itself: authentication, RBAC, admission control, Pod Security, network policy, encrypting Secrets at rest, securing etcd and the API server.
- Container — the images and runtime: scanning for vulnerabilities, signing and verifying provenance, dropping capabilities, running as non-root.
- Code — your application: dependency management, input validation, TLS, secret handling. The innermost layer and the one you fully control.
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:
- 01kubectl / client
- 02Authenticationwho?
- 03Authorizationallowed?
- 04Admission Controlmutate + validate
- 05etcd
- 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.
- 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.
- 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:
- Normal users (humans) — managed outside Kubernetes. There is no
Userobject. A user is whatever an authenticator vouches for:- X.509 client certificates — the CN becomes the username, the Organization fields become groups. Simple but hard to revoke (no built-in CRL) — avoid for humans at scale.
- OIDC tokens — the recommended approach for humans. Kubernetes trusts an external identity provider (Okta, Entra ID, Google, Dex, Keycloak); users log in there and present a signed JWT. Groups and identity come from the token’s claims, so you manage people in your IdP and revoke centrally.
- Authenticating proxy / cloud IAM (EKS
aws-iam-authenticator/ access entries, GKE/AKS IAM) — map cloud identities to Kubernetes users.
- ServiceAccounts (workloads) — first-class Kubernetes objects, namespaced, used by Pods to authenticate to the API. Each Pod gets a ServiceAccount (the namespace
defaultSA if unspecified). Modern clusters issue short-lived, audience-bound projected tokens (viaTokenRequest) automatically mounted at/var/run/secrets/kubernetes.io/serviceaccount/token, replacing the old never-expiring Secret-based tokens.
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:
| Kind | Scope | Grants |
|---|---|---|
| Role | One namespace | A set of permissions (rules) within that namespace |
| ClusterRole | Cluster-wide | Permissions cluster-wide, or on cluster-scoped resources (nodes, PVs), or a reusable rule set |
| RoleBinding | One namespace | Binds a Role or a ClusterRole to subjects, effective in that namespace |
| ClusterRoleBinding | Cluster-wide | Binds 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:
- 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. - 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:
| Level | Meaning | Blocks |
|---|---|---|
| Privileged | Unrestricted — no restrictions | Nothing (for trusted infra/system workloads) |
| Baseline | Minimally restrictive, prevents known privilege escalations | privileged containers, host namespaces, hostPath, most added capabilities, hostPorts |
| Restricted | Hardened, follows current best practice | Everything 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 Gatekeeper | Kyverno | |
|---|---|---|
| Policy language | Rego (Open Policy Agent) | YAML (Kubernetes-native) |
| Learning curve | Steeper (learn Rego) | Gentle (looks like K8s manifests) |
| Capabilities | Validate, audit | Validate, mutate, generate, verify images, cleanup |
| Reuse | Templated ConstraintTemplate + Constraint | Self-contained ClusterPolicy |
| Best for | Complex 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:
runAsNonRoot/runAsUser— a root process that escapes the container is root on the node; running as a non-zero UID contains the blast radius.allowPrivilegeEscalation: false— prevents a process from gaining more privileges than its parent (blocks setuid binaries), closing a common escalation route.readOnlyRootFilesystem: true— an attacker can’t drop tools or modify binaries in the container; mountemptyDirfor the few paths that need to be writable.capabilities.drop: ["ALL"]— Linux capabilities are fine-grained root powers; drop them all and add back only the specific ones needed (e.g.NET_BIND_SERVICEto bind port <1024).seccompProfile: RuntimeDefault— restricts the syscalls the container can make to the container runtime’s vetted default set, shrinking the kernel attack surface.privileged: true— the opposite of all of the above; it grants near-host access. Treat it as a red flag reserved for a tiny set of system/infra DaemonSets.
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:
- Encryption at rest — configure an
EncryptionConfigurationon 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/secretboxare alternatives to KMS. - RBAC — treat
get/listonsecretsas highly privileged; scope it tightly. Reading a Secret is reading a credential. - 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:
- Scan images for known CVEs before and after deployment — Trivy, Grype, or the registry’s scanner — and fail the CI build on high/critical findings. Generate an SBOM (Syft) so you know what’s inside.
- Use minimal base images (distroless, Alpine, scratch) to shrink the attack surface — fewer packages, fewer CVEs, no shell for an attacker to use.
- Pin by digest (
image@sha256:...), not mutable tags;:latestis unversioned and irreproducible. - Sign images and verify at admission. Sign with cosign (Sigstore) at build time, then block unsigned or untrusted images at admission — Kyverno (
verifyImages), Connaisseur, or the cloud’s built-in gate (GKE Binary Authorization, AWS Signer + ECR). This ensures only images your pipeline built and signed can run, defeating tampered or typosquatted images. - Use a private registry with access controls and pull-through caching; avoid pulling arbitrary public images at runtime.
Best Practices
-
Enforce least-privilege RBAC and never bind
cluster-adminbroadly. Grant the minimum verbs on the minimum resources in the minimum scope. Audit withkubectl auth can-i --listand tools likerbac-lookup; treatverbs: ["*"],resources: ["*"], and broadsecretsread as findings. -
Give every workload a dedicated ServiceAccount and disable token automount when unused. Never grant permissions to the
defaultSA. SetautomountServiceAccountToken: falseon Pods that don’t call the API, so a compromised Pod has no cluster credential to steal. -
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.
-
Enforce Pod Security Admission at
restricted(orbaselineminimum) on every workload namespace. Roll out withwarn+auditfirst, fix violations, thenenforce. Keepprivilegedonly for vetted system namespaces. -
Apply the hardened
securityContextto every workload.runAsNonRoot,allowPrivilegeEscalation: false,readOnlyRootFilesystem: true,capabilities.drop: ["ALL"], andseccompProfile: RuntimeDefault. These are the concrete controls behind therestrictedstandard. -
Never run privileged containers or mount the host filesystem/namespaces except for vetted infrastructure.
privileged: true,hostPID,hostNetwork, andhostPathare container-escape vectors — block them with Pod Security and a policy engine. -
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.
-
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 secretsRBAC. -
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.
-
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.
-
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. -
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. -
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 sanefailurePolicy. -
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.
-
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.
-
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
- Overview of Cloud Native Security (the 4Cs)
- Kubernetes Security Concepts
- Authenticating
- Managing Service Accounts
- Using RBAC Authorization
- Admission Controllers Reference
- Dynamic Admission Control (webhooks)
- Validating Admission Policy (CEL)
- Pod Security Standards · Pod Security Admission
- Set the security context for a Pod/Container
- Network Policies
- Encrypting Secret Data at Rest
- Kyverno · OPA Gatekeeper
- Sigstore cosign · External Secrets Operator · Falco
- roadmap.sh — Kubernetes