Back to Blog

Minimize Microservice Vulnerabilities for the CKS Exam: Pod Security Standards, Secrets, mTLS & Sandboxes

A hands-on guide to the Minimize Microservice Vulnerabilities domain of the CKS exam — enforcing Pod Security Standards with Pod Security Admission, managing Kubernetes Secrets safely, isolating workloads with gVisor and Kata sandboxes, and encrypting pod-to-pod traffic with mTLS, all with copy-paste YAML and commands you can run on a real cluster.

By Sailor Team , July 5, 2026

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:

  1. Enforce OS-level security domains with Pod Security Standards and Pod Security Admission
  2. Manage Kubernetes Secrets safely, including encryption at rest
  3. Isolate workloads with container runtime sandboxes (gVisor, Kata Containers)
  4. 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

ProfileWhat it allowsUse for
privilegedUnrestricted — no restrictions at allTrusted, infrastructure workloads (CNI, storage)
baselineBlocks known privilege escalations; permissive otherwiseThe common default for most apps
restrictedHeavily hardened, current pod-hardening best practicesSecurity-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:

ModeEffect
enforceRejects pods that violate the policy
auditAllows the pod but records a violation in the audit log
warnAllows 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 x can 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, or kms) first and identity last as a read fallback. Verify encryption by reading the raw value straight from etcd with etcdctl 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 RuntimeClass whose handler matches what’s configured on the node and (2) runtimeClassName on the pod. The runtime binary (runsc for gVisor) must already be installed on the node — the exam sets that up; your job is the Kubernetes objects.

SandboxIsolation modelTrade-off
gVisor (runsc)User-space kernel intercepts syscallsLower overhead; some syscall incompatibilities
Kata ContainersLightweight VM per podStronger 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

TaskMechanismKey command / field
Enforce a pod security profilePod Security Admissionpod-security.kubernetes.io/enforce=restricted (ns label)
Custom admission policyOPA Gatekeeper / KyvernoConstraintTemplate + Constraint
Encrypt Secrets at restEncryptionConfiguration--encryption-provider-config on API server
Rewrite existing Secretskubectlkubectl get secrets -A -o json | kubectl replace -f -
Mount secret as filepod specvolumes.secret, defaultMode: 0400
Disable token automountpod / SA specautomountServiceAccountToken: false
Sandbox a workloadRuntimeClassruntimeClassName: gvisor
Encrypt pod-to-pod trafficService mesh mTLSPeerAuthentication mode: STRICT
Restrict pod connectivityNetworkPolicydefault-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.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now