The Minimize Microservice Vulnerabilities domain is the largest slice of the CKS at 20% of the exam, and it’s the one that ties every other domain together. Where System Hardening confines a single container to the host and Cluster Setup and Hardening locks down the control plane, this domain is about the workloads themselves — how you stop a compromised or misconfigured microservice from becoming a foothold, and how you isolate one tenant’s pods from another’s.
The CNCF curriculum breaks this domain into four objectives, and this guide walks through each the way the exam tests them — hands-on, on a real cluster, with commands and manifests you can run today:
- Enforce OS-level security domains with Pod Security Standards and Pod Security Admission
- Manage Kubernetes Secrets safely, including encryption at rest
- Isolate workloads with container runtime sandboxes (gVisor, Kata Containers)
- Encrypt pod-to-pod traffic with mTLS
If you want the full blueprint and exam logistics first, start with the CKS exam guide for 2026 and the CKS exam topics breakdown.
Objective 1: Pod Security Standards and Pod Security Admission
Pod Security Admission (PSA) is the built-in admission controller that replaced the deprecated and now-removed PodSecurityPolicy. It enforces the three Pod Security Standards — a set of policy profiles defined by the community — at the namespace level. Knowing these cold is non-negotiable for the exam.
The three profiles
| Profile | What it allows | Use for |
|---|---|---|
| privileged | Unrestricted — no restrictions at all | Trusted, infrastructure workloads (CNI, storage) |
| baseline | Blocks known privilege escalations; permissive otherwise | The common default for most apps |
| restricted | Heavily hardened, current pod-hardening best practices | Security-critical workloads |
The restricted profile requires (among other things): runAsNonRoot: true, allowPrivilegeEscalation: false, seccompProfile.type of RuntimeDefault or Localhost, and all capabilities dropped except NET_BIND_SERVICE.
The three enforcement modes
PSA applies a standard through one of three modes, set as namespace labels:
| Mode | Effect |
|---|---|
| enforce | Rejects pods that violate the policy |
| audit | Allows the pod but records a violation in the audit log |
| warn | Allows the pod but returns a user-facing warning |
You can run all three at once — a common real-world pattern is to enforce baseline while you audit and warn on restricted, so you see what would break before tightening.
Apply it with labels
# Enforce the 'restricted' standard on a namespace, and warn/audit at the same level
kubectl label namespace payments \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
You can also pin the version so a cluster upgrade doesn’t silently change what “restricted” means:
kubectl label namespace payments \
pod-security.kubernetes.io/enforce-version=v1.31 --overwrite
Prove it works
Try to run a privileged pod in the hardened namespace:
kubectl run bad --image=nginx --namespace=payments \
--privileged
# Error from server (Forbidden): pods "bad" is forbidden:
# violates PodSecurity "restricted:v1.31": privileged (...), allowPrivilegeEscalation != false (...)
A pod that satisfies restricted looks like this:
apiVersion: v1
kind: Pod
metadata:
name: good
namespace: payments
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.27
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
Exam tip: PSA operates on namespaces, not individual pods. If a task says “ensure no pod in namespace
xcan run as privileged,” the answer is a namespace label, not a change to each pod. For policy needs PSA can’t express (e.g. “block a specific registry” or “require a label”), the exam expects a validating admission webhook such as OPA Gatekeeper or Kyverno — covered below.
When PSA isn’t enough: OPA Gatekeeper
Pod Security Standards are fixed, coarse-grained profiles. When you need custom policy — enforce that images come only from an approved registry, require every pod to carry a team label, or ban the latest tag — you reach for a policy engine. OPA Gatekeeper is the CNCF option, using ConstraintTemplate (the Rego logic) plus Constraint (where it applies):
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
not startswith(c.image, input.parameters.repos[_])
msg := sprintf("image '%v' comes from an untrusted registry", [c.image])
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: only-internal-registry
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "registry.internal.example.com/"
Any pod pulling from an outside registry is now rejected at admission. This connects directly to the supply chain security domain, where trusted-registry enforcement is a core control.
Objective 2: Manage Kubernetes Secrets
Secrets are the classic microservice weak point. The two facts the exam tests hardest are that Secrets are base64-encoded, not encrypted by default, and that you must explicitly turn on encryption at rest in etcd.
base64 is not encryption
# Create a secret
kubectl create secret generic db-cred \
--from-literal=password=S3cr3t!
# Anyone with get access can trivially decode it
kubectl get secret db-cred -o jsonpath='{.data.password}' | base64 -d
# S3cr3t!
That’s why RBAC on Secrets matters: restrict get, list, and watch on the secrets resource to only the service accounts that truly need them. See the cluster hardening guide for the RBAC patterns.
Encrypt Secrets at rest in etcd
Without an EncryptionConfiguration, Secrets sit in etcd in plaintext (base64). The exam may hand you an unencrypted cluster and ask you to fix it. Write the config on the control-plane node:
# /etc/kubernetes/enc/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <BASE64_ENCODED_32_BYTE_KEY>
- identity: {}
Generate a valid key with head -c 32 /dev/urandom | base64. Then point the API server at it by editing the static pod manifest:
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
volumeMounts:
- name: enc
mountPath: /etc/kubernetes/enc
readOnly: true
volumes:
- name: enc
hostPath:
path: /etc/kubernetes/enc
type: DirectoryOrCreate
The kubelet restarts the API server automatically when the manifest changes. Existing Secrets are only encrypted when next written, so rewrite them all:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Exam tip: The
identity: {}provider means “no encryption.” Its position in the providers list matters — the first provider is used for writing. Put your real provider (aescbc,secretbox, orkms) first andidentitylast as a read fallback. Verify encryption by reading the raw value straight from etcd withetcdctl get /registry/secrets/<ns>/<name>— it should no longer be readable plaintext.
Mount Secrets safely
Prefer mounting Secrets as files over environment variables — env vars leak into logs, crash dumps, and /proc/<pid>/environ, and can be read by any process in the container:
spec:
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: creds
mountPath: /etc/creds
readOnly: true
volumes:
- name: creds
secret:
secretName: db-cred
defaultMode: 0400
Also set automountServiceAccountToken: false on pods and service accounts that don’t call the Kubernetes API, so a compromised container can’t use the mounted token to talk to the API server.
Objective 3: Container Runtime Sandboxes
Standard containers share the host kernel. If a workload is untrusted — multi-tenant SaaS, running third-party code, or processing hostile input — a kernel exploit escapes straight to the node. Runtime sandboxes add a second isolation boundary. The two you must know are gVisor (a user-space kernel that intercepts syscalls) and Kata Containers (lightweight VMs per pod).
RuntimeClass is the mechanism
You select a sandbox per pod via a RuntimeClass, which maps to a handler configured in the container runtime (containerd/CRI-O) on the node:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc # the runtime handler configured in containerd
Then reference it from a pod:
apiVersion: v1
kind: Pod
metadata:
name: untrusted
spec:
runtimeClassName: gvisor
containers:
- name: app
image: untrusted-code:latest
Confirm the sandbox is active
gVisor’s runsc presents a different kernel to the workload, so uname and dmesg behave differently than on a native runtime:
kubectl exec untrusted -- dmesg | head
# gVisor's synthetic dmesg output confirms runsc is in effect
Exam tip: If a task says “run this pod in a sandboxed runtime,” the two-part answer is (1) a
RuntimeClasswhosehandlermatches what’s configured on the node and (2)runtimeClassNameon the pod. The runtime binary (runscfor gVisor) must already be installed on the node — the exam sets that up; your job is the Kubernetes objects.
| Sandbox | Isolation model | Trade-off |
|---|---|---|
| gVisor (runsc) | User-space kernel intercepts syscalls | Lower overhead; some syscall incompatibilities |
| Kata Containers | Lightweight VM per pod | Stronger isolation; higher startup cost |
Objective 4: Pod-to-Pod Encryption with mTLS
By default, traffic between pods is unencrypted on the cluster network. Mutual TLS (mTLS) encrypts pod-to-pod traffic and authenticates both ends, so a workload can’t be impersonated and traffic can’t be sniffed on the wire.
Where mTLS comes from
You rarely hand-roll mTLS on the CKS. In practice it comes from a service mesh (Istio, Linkerd) that injects a sidecar proxy into each pod and transparently upgrades connections to mTLS. In Istio you set a PeerAuthentication policy to STRICT:
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: payments
spec:
mtls:
mode: STRICT # reject any plaintext traffic in this namespace
STRICT means every connection into a mesh pod in payments must be mTLS; PERMISSIVE (the default) allows both while you migrate. Pair mesh mTLS with NetworkPolicy — mTLS encrypts and authenticates, NetworkPolicy controls who is allowed to talk to whom at all. They’re complementary:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: payments
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
A default-deny NetworkPolicy plus strict mTLS gives you both “only permitted pods can connect” and “every permitted connection is encrypted and mutually authenticated.”
The certificate rotation angle
The value of mesh-managed mTLS is automatic certificate issuance and rotation — short-lived workload certificates minted by the mesh’s control plane, so there are no long-lived secrets to leak. Understanding that mTLS = encryption plus identity plus short-lived rotating certs is exactly the conceptual point the exam checks.
Putting It Together: A Hardened Microservice
A workload that satisfies this domain layers all four controls at once:
apiVersion: v1
kind: Pod
metadata:
name: hardened-svc
namespace: payments # namespace enforces 'restricted' via PSA
spec:
runtimeClassName: gvisor # runtime sandbox
automountServiceAccountToken: false
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.internal.example.com/app:1.0 # trusted registry (Gatekeeper)
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: creds
mountPath: /etc/creds
readOnly: true
volumes:
- name: creds
secret:
secretName: db-cred # encrypted at rest in etcd
defaultMode: 0400
Layer a default-deny NetworkPolicy and STRICT PeerAuthentication over the namespace and you have a workload that is admission-checked, sandboxed, running as non-root with a read-only filesystem, pulling only from a trusted registry, using encrypted-at-rest Secrets, and communicating over authenticated mTLS. That is the full expression of the Minimize Microservice Vulnerabilities domain.
Minimize Microservice Vulnerabilities Cheat Sheet
| Task | Mechanism | Key command / field |
|---|---|---|
| Enforce a pod security profile | Pod Security Admission | pod-security.kubernetes.io/enforce=restricted (ns label) |
| Custom admission policy | OPA Gatekeeper / Kyverno | ConstraintTemplate + Constraint |
| Encrypt Secrets at rest | EncryptionConfiguration | --encryption-provider-config on API server |
| Rewrite existing Secrets | kubectl | kubectl get secrets -A -o json | kubectl replace -f - |
| Mount secret as file | pod spec | volumes.secret, defaultMode: 0400 |
| Disable token automount | pod / SA spec | automountServiceAccountToken: false |
| Sandbox a workload | RuntimeClass | runtimeClassName: gvisor |
| Encrypt pod-to-pod traffic | Service mesh mTLS | PeerAuthentication mode: STRICT |
| Restrict pod connectivity | NetworkPolicy | default-deny ingress/egress |
Practice on a Real Cluster Before Exam Day
This domain is 20% of the CKS and every task is hands-on. Reading that Pod Security Admission is namespace-scoped is easy; reliably labeling a namespace, debugging why a pod was rejected against restricted, writing an EncryptionConfiguration and rewriting every Secret, and wiring a RuntimeClass to a pod — all under a ticking clock — is a different skill entirely, and it only comes from repetition on a live cluster.
Sailor.sh’s Certified Kubernetes Security Specialist (CKS) Mock Exam Bundle runs on a real Kubernetes cluster with exam-style performance tasks that mirror the format and difficulty of the actual CKS, including Pod Security Standards, Secret encryption, and sandbox scenarios like the ones above. If you’d rather warm up on the free, open-source practice terminal first, the CKS practice environment guide and how to practice CKS for free walk through getting started. Pair the hands-on work with a structured CKS study plan, confirm you’ve met the CKS prerequisites, and connect the dots with the Kubernetes security best practices overview and the adjacent supply chain security and runtime security domains.
Frequently Asked Questions
What replaced PodSecurityPolicy on the CKS?
PodSecurityPolicy (PSP) was deprecated in Kubernetes 1.21 and removed in 1.25. It’s replaced by Pod Security Admission (PSA), a built-in admission controller that enforces the three Pod Security Standards (privileged, baseline, restricted) through namespace labels. For custom rules beyond those three profiles, use a policy engine like OPA Gatekeeper or Kyverno.
Are Kubernetes Secrets encrypted by default?
No. By default Secrets are only base64-encoded in etcd, which is trivially reversible — it’s encoding, not encryption. To actually encrypt Secrets at rest you must configure an EncryptionConfiguration (e.g. with the aescbc or a KMS provider) and pass it to the API server via --encryption-provider-config. Existing Secrets must be rewritten (kubectl replace) to become encrypted.
How do the three Pod Security Standard profiles differ?
privileged applies no restrictions (for trusted infrastructure), baseline blocks known privilege escalations while staying permissive (a sensible default), and restricted enforces current pod-hardening best practices — non-root, no privilege escalation, RuntimeDefault seccomp, and dropped capabilities. Each is applied per namespace in enforce, audit, or warn mode.
What is a RuntimeClass and when do I need one on the CKS?
A RuntimeClass selects which container runtime handler runs a pod, letting you place untrusted workloads in a sandbox like gVisor (runsc) or Kata Containers. You define the RuntimeClass with a handler matching the node’s runtime config, then set runtimeClassName on the pod. Use it whenever a task asks you to run a workload with stronger kernel isolation than a standard container provides.
Does mTLS replace NetworkPolicy?
No — they solve different problems and belong together. mTLS (usually from a service mesh) encrypts and mutually authenticates pod-to-pod traffic, while NetworkPolicy controls which pods are permitted to communicate at all. A hardened namespace uses a default-deny NetworkPolicy for connectivity control plus STRICT mesh mTLS for encryption and identity.
How much of the CKS is the Minimize Microservice Vulnerabilities domain?
It’s 20% of the CKS — the largest single domain, tied with Cluster Hardening in weight but broader in scope. Because it spans admission control, Secrets, sandboxes, and mTLS, it’s worth heavy practice. See the CKS exam topics breakdown for the full weighting across all domains.
Conclusion
Minimize Microservice Vulnerabilities is where Kubernetes security stops being about the cluster and starts being about the workloads. Every microservice should run under an enforced Pod Security Standard, pull only from a trusted registry, consume Secrets that are encrypted at rest and mounted as read-only files, sit inside a runtime sandbox if it’s untrusted, and talk to its peers over authenticated, encrypted mTLS — with a default-deny NetworkPolicy underneath it all.
None of these controls is hard once it’s automatic, and the only way to make them automatic is to apply them repeatedly on a real cluster. Work through the CKS mock exams, enforce restricted on a namespace and watch a bad pod get rejected, encrypt etcd and prove it with etcdctl, and wire a pod into a gVisor sandbox. Do that, and the biggest domain on the CKS turns into your strongest. For the full study path, begin with the CKS exam guide for 2026.