The “Application Environment, Configuration and Security” domain is worth roughly 20% of the Certified Kubernetes Application Developer (CKAD) exam, and two topics inside it trip people up more than any others: SecurityContext and ServiceAccounts. They are not hard once you see the pattern, but they hide in troubleshooting questions where a Pod runs fine locally and then fails the moment security constraints are applied. A container that expects to write to disk but gets a read-only filesystem, a process that needs to bind port 80 but is told it can’t, an application that calls the Kubernetes API and gets 403 Forbidden — every one of those is a SecurityContext or ServiceAccount question in disguise.
This guide treats both as mechanical skills. By the end you will be able to read a scenario, decide whether it needs a field on the Pod, a field on the container, a ServiceAccount change, or an RBAC binding, and write the YAML from memory under time pressure.
What SecurityContext Actually Controls
A securityContext defines the privilege and access-control settings for a Pod or a container. It answers questions the container runtime needs before it starts your process: Which user ID runs this? Can it escalate privileges? Which Linux capabilities does it hold? Can it write to its own root filesystem?
There are two places you can set it, and the distinction matters on the exam:
| Level | Where it lives | Scope | Notable fields |
|---|---|---|---|
| Pod | spec.securityContext | Applies to all containers, sets volume ownership | runAsUser, runAsGroup, fsGroup, runAsNonRoot, seccompProfile |
| Container | spec.containers[].securityContext | Applies to one container, overrides the Pod | capabilities, allowPrivilegeEscalation, readOnlyRootFilesystem, privileged, plus all the runAs* fields |
The rule to memorise: when a field exists at both levels, the container-level value wins. Set runAsUser: 1000 on the Pod and runAsUser: 2000 on one container, and that container runs as 2000 while its siblings run as 1000. Note also that capabilities, readOnlyRootFilesystem, allowPrivilegeEscalation, and privileged are container-only — you cannot set them at the Pod level.
The Core SecurityContext Fields
Here is a Pod that exercises the fields the exam cares about most. Read it as a checklist, not a wall of YAML:
apiVersion: v1
kind: Pod
metadata:
name: hardened
spec:
securityContext: # Pod level
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
runAsNonRoot: true
containers:
- name: app
image: busybox
command: ["sh", "-c", "sleep 3600"]
securityContext: # container level
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
runAsUser/runAsGroupset the UID and primary GID of the container process. If your app writes files, they will be owned by this UID.fsGroupsets a supplemental group that owns mounted volumes. Kubernetes recursively changes group ownership of the volume so the process can read and write it — this is the field that fixes “permission denied” on aPersistentVolumeoremptyDir.runAsNonRoot: trueis a guard, not an assignment. It does not pick a UID; it tells the kubelet to refuse to start the container if the image would run as UID 0. If you set it without arunAsUserand the image defaults to root, the Pod fails withCreateContainerConfigError.allowPrivilegeEscalation: falsestops a process from gaining more privileges than its parent (it blocks setuid binaries and theno_new_privsbit).readOnlyRootFilesystem: truemounts the container’s root filesystem read-only. Anything that needs to write must use a mounted volume — a common exam twist is pairing this with anemptyDirat/tmp.
Linux Capabilities: Drop All, Add Back
Capabilities break the old “root or not root” binary into fine-grained privileges. The practitioner pattern the exam rewards is drop everything, then add back only what you need:
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"] # bind to ports below 1024
Note the naming: in YAML you drop the CAP_ prefix, so CAP_NET_ADMIN becomes NET_ADMIN. The two capabilities that show up most often are NET_BIND_SERVICE (bind privileged ports like 80/443 as a non-root user) and NET_ADMIN (manage network interfaces). If a question says “the container must listen on port 80 but must not run as root,” the answer is a non-root runAsUser plus add: ["NET_BIND_SERVICE"].
Avoid privileged: true unless a question explicitly demands it. It grants the container almost all host access and is the opposite of what a hardening question wants.
Fast Imperative Moves for SecurityContext
You cannot create a full SecurityContext with a single kubectl flag, so the fastest path on the exam is to generate a Pod skeleton and edit it:
# Generate the manifest, don't create it yet
kubectl run hardened --image=busybox --dry-run=client -o yaml \
--command -- sleep 3600 > pod.yaml
# Edit pod.yaml to add securityContext, then:
kubectl apply -f pod.yaml
For quick edits to a running object you can kubectl edit pod <name>, but remember many SecurityContext fields are immutable on a running Pod — you will usually delete and recreate. Practising the --dry-run=client -o yaml skeleton until it is muscle memory is one of the highest-leverage things you can do; the same trick powers most of the exam. If you want a full command reference, keep the CKAD kubectl cheat sheet open while you drill.
ServiceAccounts: Identity for Your Pods
A ServiceAccount is the identity a Pod presents when it talks to the Kubernetes API server. Every Pod runs as a ServiceAccount whether you specify one or not — omit it and the Pod uses the default ServiceAccount in its namespace, which by design can do almost nothing.
The relationships are worth drawing in your head:
| Object | Purpose |
|---|---|
| ServiceAccount | An in-cluster identity, namespaced |
| Role / ClusterRole | A set of permissions (verbs on resources) |
| RoleBinding / ClusterRoleBinding | Grants a Role’s permissions to a ServiceAccount |
| Projected token | The credential mounted into the Pod that proves the identity |
Create one and attach it to a Pod imperatively:
# Create a ServiceAccount
kubectl create serviceaccount app-sa
# Attach it to a new Pod via the manifest
kubectl run api-caller --image=curlimages/curl \
--dry-run=client -o yaml --command -- sleep 3600 > sa-pod.yaml
In the manifest, the field is spec.serviceAccountName:
apiVersion: v1
kind: Pod
metadata:
name: api-caller
spec:
serviceAccountName: app-sa
containers:
- name: curl
image: curlimages/curl
command: ["sh", "-c", "sleep 3600"]
There is also a handy imperative shortcut for existing workloads:
kubectl set serviceaccount deployment/web app-sa
The Token, and How to Turn It Off
Kubernetes mounts a short-lived, automatically rotated token into every Pod at:
/var/run/secrets/kubernetes.io/serviceaccount/
Inside that directory are token, ca.crt, and namespace. An application uses the token as a bearer credential when calling the API server. On modern clusters this is a projected, bound token with an expiry — not the long-lived Secret of older Kubernetes versions.
If a Pod never talks to the API, mounting a credential it doesn’t need is needless attack surface. Turn it off in two places, and know which wins:
# On the ServiceAccount (default for all Pods using it)
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-token-sa
automountServiceAccountToken: false
---
# On the Pod (overrides the ServiceAccount setting)
apiVersion: v1
kind: Pod
metadata:
name: quiet
spec:
serviceAccountName: no-token-sa
automountServiceAccountToken: false
containers:
- name: app
image: busybox
command: ["sh", "-c", "sleep 3600"]
The Pod-level setting always overrides the ServiceAccount-level setting — a pattern you will now recognise from SecurityContext.
Wiring Permissions with RBAC
A ServiceAccount with no binding can authenticate but not do anything. To let a Pod list Pods in its namespace, create a Role and bind it. The imperative commands are fast enough to use under exam time:
# A Role that allows reading Pods
kubectl create role pod-reader \
--verb=get,list,watch --resource=pods
# Bind that Role to the ServiceAccount
kubectl create rolebinding app-sa-pods \
--role=pod-reader \
--serviceaccount=default:app-sa
Note the --serviceaccount format is namespace:name. Use create clusterrole / create clusterrolebinding when the scope must be cluster-wide. This authentication-and-authorization wiring is exactly the “understand authentication, authorization and admission control” objective in the syllabus, so expect at least one question that ends in a RoleBinding.
Verify permissions the way an examiner would — with kubectl auth can-i impersonating the ServiceAccount:
kubectl auth can-i list pods \
--as=system:serviceaccount:default:app-sa
# yes
That command is your single best troubleshooting tool for “the app gets 403.” If it prints no, the binding is missing or wrong; if it prints yes but the app still fails, the problem is the token mount or the code, not RBAC.
Admission Control: Pod Security Admission
The syllabus mentions admission control, and the piece most relevant to CKAD is Pod Security Admission (PSA) — the built-in admission controller that replaced the old PodSecurityPolicy. PSA enforces the three Pod Security Standards at the namespace level using labels:
kubectl label namespace dev \
pod-security.kubernetes.io/enforce=restricted
| Level | Meaning |
|---|---|
privileged | Unrestricted, no constraints |
baseline | Blocks known privilege escalations (no privileged, no host namespaces) |
restricted | Hardened: requires runAsNonRoot, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], a seccomp profile |
The exam-relevant insight: if a namespace enforces restricted and you submit a Pod without a proper SecurityContext, the API server rejects it outright. This is why the SecurityContext skills above matter — they are exactly what a restricted namespace demands. If you see a Pod that “won’t even create” with an error mentioning violates PodSecurity, add runAsNonRoot: true, allowPrivilegeEscalation: false, and capabilities.drop: ["ALL"] and it will pass.
Troubleshooting Playbook
Three failure signatures cover most of what the exam throws at you:
CreateContainerConfigErrorwith “container has runAsNonRoot and image will run as root” — the image defaults to UID 0 but you setrunAsNonRoot: true. Add a non-zerorunAsUser, or use an image that runs as non-root.- App logs show “permission denied” writing a file — either
readOnlyRootFilesystem: trueis blocking a write to the root FS (mount a volume for the writable path), or a volume is owned by the wrong group (setfsGroup). - App gets
403 Forbiddenfrom the API server — an RBAC problem. Confirm withkubectl auth can-i ... --as=system:serviceaccount:<ns>:<sa>and add the missing Role/RoleBinding.
For a broader diagnostic workflow that layers on top of these, the CKAD application troubleshooting guide walks through the full describe → logs → events loop.
How This Connects to the Rest of the Exam
SecurityContext and ServiceAccounts rarely live alone. They thread into other domains: a fsGroup fix depends on how you mounted the volume, which overlaps ConfigMaps and Secrets; a restricted namespace interacts with the resource requests and limits you set; and multi-container Pods each carry their own container-level SecurityContext, which ties into multi-container Pod patterns. Seeing them as one connected system is what separates a 66% from an 85%. For the full map of what’s tested and how the domains weigh against each other, start with the CKAD exam domains breakdown.
Practice Until It’s Reflex
Reading YAML is not the same as writing it against a two-hour clock in a live terminal. The CKAD is a performance exam — you are graded on working objects, not multiple-choice recall — so the only preparation that transfers is repetition in a real cluster. Build a Pod that drops all capabilities and adds back NET_BIND_SERVICE. Create a ServiceAccount, bind it to a pod-reader Role, and confirm with kubectl auth can-i. Label a namespace restricted and watch a lax Pod get rejected. Do each three times and the muscle memory sticks.
If you want that repetition under realistic exam conditions — a browser-based terminal, timed scenarios, and detailed explanations for every task — the Sailor.sh CKAD Certification Ready Mock Exam Bundle runs five full performance labs covering security, configuration, and the rest of the syllabus. It is designed as hands-on practice, not a question dump, so you finish knowing you can do the tasks, not just recognise them. You can also warm up with the free CKAD practice guide before committing to a full mock.
Frequently Asked Questions
What is the difference between Pod-level and container-level SecurityContext?
Pod-level securityContext applies to every container in the Pod and controls volume ownership via fsGroup. Container-level securityContext applies to one container and can override the Pod’s runAs* values. Fields like capabilities, readOnlyRootFilesystem, allowPrivilegeEscalation, and privileged are container-only. When a field is set at both levels, the container-level value wins.
Does runAsNonRoot pick a user ID for me?
No. runAsNonRoot: true is a guardrail that makes the kubelet refuse to start a container that would run as UID 0. It does not assign a UID — you still need runAsUser (or an image that already runs as non-root). Set runAsNonRoot without a non-zero user on a root image and the Pod fails with CreateContainerConfigError.
How do I stop Kubernetes from mounting a ServiceAccount token?
Set automountServiceAccountToken: false. You can set it on the ServiceAccount (affecting all Pods that use it) or on an individual Pod, and the Pod-level setting overrides the ServiceAccount-level one. Do this for workloads that never call the Kubernetes API to reduce attack surface.
Why does my Pod get 403 Forbidden when calling the API?
The Pod’s ServiceAccount lacks the required RBAC permissions. Verify with kubectl auth can-i <verb> <resource> --as=system:serviceaccount:<namespace>:<serviceaccount>. If it returns no, create a Role (or ClusterRole) with the needed verbs and bind it to the ServiceAccount with a RoleBinding.
What Linux capability lets a non-root container bind to port 80?
NET_BIND_SERVICE. Add it under securityContext.capabilities.add while keeping drop: ["ALL"]. This lets a process running as a non-root runAsUser bind to privileged ports below 1024 without granting broad root-equivalent access.
Is SecurityContext heavily tested on the CKAD?
Yes. It falls under “Application Environment, Configuration and Security,” about 20% of the exam, and shows up both directly (“run this container as user 1000 with a read-only root filesystem”) and indirectly inside troubleshooting and namespaces that enforce the restricted Pod Security Standard. It is high-value, mechanical, and worth drilling until you can write the YAML from memory.