Reviewing a Kubernetes manifest for security is harder than reviewing code, because the dangerous version usually looks cleaner. The defaults are permissive, so an insecure Deployment is simply a short one.
A container with no securityContext may run as root, can gain privileges through setuid binaries, has a writable root filesystem, and keeps Linux capabilities it almost certainly does not use. None of that appears in the file.
This post covers what to add, what to remove, and what a manifest check cannot do. The Kubernetes Manifest Checker applies all of it to pasted YAML.
The lines to add
This is the security context most workloads can run with:
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
| Setting | What it prevents |
|---|---|
runAsNonRoot: true | The image starting as root |
allowPrivilegeEscalation: false | A setuid binary gaining more privileges than the process started with |
readOnlyRootFilesystem: true | An attacker writing tools or modifying the application on disk |
capabilities.drop: ["ALL"] | Use of kernel capabilities the process does not need |
If the application writes to disk, mount an emptyDir at the paths it writes to and keep the rest read-only.
The lines to remove
Each of these gives a pod a route to the node it runs on.
privileged: true: nearly the same access as root on the node.hostNetwork,hostPID,hostIPC: the pod shares the node's network stack, process table, or IPC namespace.hostPathvolumes: the pod reads or writes the node's filesystem.- A mounted container runtime socket: access to
docker.sockorcontainerd.socklets the pod start privileged containers, which is root on the node. - Added capabilities such as
SYS_ADMINorNET_ADMIN.
Node-level agents sometimes need one of these. An application workload almost never does.
Secrets do not belong in the manifest
Two versions of the same mistake:
env:
- name: DB_PASSWORD
value: hunter2
and a Secret resource committed to Git with its values in data. Base64 is an encoding, not encryption. Anyone who can read the repository can read the value.
Reference a Secret with valueFrom.secretKeyRef, and keep the Secret itself out of Git with External Secrets, Sealed Secrets, or SOPS.
The settings that cause incidents, not breaches
- No resource requests. The scheduler cannot place the pod sensibly, and it is evicted first under pressure.
- No memory limit. One leaking container can exhaust the node and take other pods down with it.
- Image tagged
latest. The tag can point at a different image tomorrow, so rolling back the Deployment does not roll back the code. - No readiness probe. Traffic arrives before the application can serve it.
- A single replica. A node drain takes the workload offline.
CPU limits are a judgement call. They can cause throttling, and many teams set CPU requests and leave the limit off.
RBAC in the same file
Manifests often ship with their own permissions. Look for a ClusterRoleBinding to cluster-admin, a Role with verbs: ["*"] on resources: ["*"], and any Role that can get or list secrets. Reading secrets in a namespace usually means reading every credential in it.
What a manifest check cannot do
A check reads the YAML someone chose to show it. It cannot stop anyone from applying something else. Enforce the same rules in the cluster with Pod Security Admission, Kyverno, or Gatekeeper, so an unsafe workload is refused at deploy time. Use the manifest check earlier, in review, where a fix is cheapest.
Check a manifest. Paste one resource or a whole multi-document file.