Most Kubernetes security advice is either a wall of CIS benchmark line items with no prioritization, or a listicle that says "use RBAC" and "scan your images" without explaining what actually goes wrong when you don't. Neither is useful when you're the one responsible for a cluster and need to know what to fix first.
This is the checklist I actually work through, grouped by the failure mode each item prevents, with enough reasoning that you can judge for yourself whether an item applies to your environment — not every control belongs in every cluster.
Short answer
If you only fix three things this week: remove privileged: true from any workload that doesn't strictly need it, put a default-deny NetworkPolicy baseline in every namespace, and stop granting cluster-admin to service accounts as a shortcut. Those three account for a disproportionate share of what actually turns "a compromised pod" into "a compromised cluster." Everything below is the rest of the list, in the order I'd address it.
Workload security: what a container can actually do to the host
Privileged containers. securityContext.privileged: true disables essentially every isolation boundary a container has — it's not "more permissions," it's closer to running the process on the host directly. Find them:
kubectl get pods -A -o json | \
jq -r '.items[] | select(.spec.containers[]?.securityContext.privileged == true) | "\(.metadata.namespace)/\(.metadata.name)"'
If something is privileged, ask why. Legitimate reasons exist (CNI plugins, some storage drivers, node-monitoring agents) — but an application workload running privileged almost always means someone was debugging a permissions error and reached for the sledgehammer instead of the actual fix.
Running as root. Even without privileged: true, a container running as UID 0 has a meaningfully larger attack surface inside its own namespace (writable /proc entries, capability defaults). Set this explicitly rather than trusting the image's default USER:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
runAsNonRoot: true alone doesn't set a UID — it just makes the kubelet refuse to start the container if the image's default user resolves to root. Pair it with an explicit runAsUser, and verify against images you don't control (a base image maintainer can change the default user in a patch release without you noticing).
allowPrivilegeEscalation. Defaults to true, which lets a process gain more privileges than its parent (via setuid binaries, for instance) even in a non-privileged, non-root container. Set it explicitly:
securityContext:
allowPrivilegeEscalation: false
Linux capabilities. Containers get a reduced-but-nontrivial default capability set. Drop everything and add back only what's demonstrably required:
securityContext:
capabilities:
drop: ["ALL"]
# add: ["NET_BIND_SERVICE"] # only if the app binds a privileged port
Watch specifically for SYS_ADMIN, NET_ADMIN, NET_RAW, and SYS_PTRACE being added — these are the ones that meaningfully approach root-equivalent or host-escape-adjacent capability, not routine "the app needs to bind port 80" additions.
Writable root filesystem. readOnlyRootFilesystem: true means a compromised process can't drop a second-stage payload or modify the application binary on disk. Most applications only need specific paths writable (/tmp, a cache directory) — mount those as emptyDir volumes rather than leaving the whole filesystem writable for convenience.
hostPath mounts, hostNetwork, hostPID, hostIPC. Any of these four punches a hole from the container into the host. hostPath mounting /, /etc, /proc, /var/run/docker.sock, or a container-runtime socket is functionally equivalent to host root, regardless of what the container's own securityContext says — the mount bypasses the container boundary entirely. Audit these specifically; they're rarer than the other findings but far more severe when present.
Pod Security Standards: enforcement, not documentation
Kubernetes replaced the deprecated PodSecurityPolicy with Pod Security Standards — three predefined levels (privileged, baseline, restricted) enforced via a namespace label, not a separate admission controller you have to install and maintain:
kubectl label namespace payments pod-security.kubernetes.io/enforce=restricted
The gap I see most often isn't the absence of a policy — plenty of teams have a wiki page describing their security baseline. It's that nothing enforces it. A label nobody set doesn't stop a bad manifest from deploying. Check current enforcement across the cluster:
kubectl get namespaces -o json | \
jq -r '.items[] | "\(.metadata.name): \(.metadata.labels["pod-security.kubernetes.io/enforce"] // "NONE")"'
Start new namespaces at restricted and relax deliberately (with a documented reason) rather than starting permissive and meaning to tighten later — "meaning to" is how the wiki-page-only baseline happens in the first place. Note that restricted will reject pods without the securityContext settings above, so tightening an existing namespace's label is a breaking change to validate in staging first, not something to apply directly to a production namespace mid-afternoon.
Secrets: what actually leaks
Two distinct failure modes get lumped together as "secrets management," and they need different fixes:
Plaintext credentials in manifests or environment variables, committed to Git or visible via kubectl describe pod. This is a process failure — grep your manifests for anything password/token/key-shaped:
grep -rEn '(password|secret|token|api[_-]?key):\s*[^$]' --include='*.yaml' .
The [^$] at the end is deliberate — it excludes lines that reference an environment variable or templating placeholder rather than a literal value, cutting down false positives. Anything that matches and isn't a placeholder is a literal credential sitting in version control.
Kubernetes Secrets treated as sufficiently secure by default. They're not encrypted at rest unless you've configured encryption at rest for the API server, and by default any identity with get/list on secrets in a namespace can read every Secret's value in that namespace — Secrets are access-controlled, not owner-scoped. If your RBAC grants broad Secret read access (see the RBAC audit for exactly how to find that), your Secrets are effectively as exposed as the broadest role that can read them. For anything beyond low-sensitivity config, an external secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) with the External Secrets Operator syncing into the cluster keeps the actual credential out of etcd and Git entirely, with rotation as a config change rather than a redeploy.
Image security: what you're actually running
Mutable tags. image: app:latest (or any tag that gets overwritten in place) means you can't answer "what code is actually running in production right now" with certainty, and it silently changes what a rollback deploys. Pin to a digest in production manifests:
image: registry.example.com/app@sha256:9d3f2c1e...
Provenance. Do you know where every image in the cluster actually came from? An implicit Docker Hub reference (image: nginx:1.25 with no registry host) pulls from whatever the default resolves to — worth an explicit, deliberate registry choice rather than an accident of omission.
Scanning. Vulnerability scanning at build time (Trivy, Grype, or your registry's built-in scanner) catches known CVEs before deploy. It won't catch a compromised base image or a backdoored dependency that isn't in a CVE database yet — treat it as a floor, not a guarantee.
Networking and ingress exposure
A full default-deny NetworkPolicy rollout deserves its own article — see designing default-deny without breaking your cluster for the actual implementation path, including the DNS-egress rule everyone forgets on the first attempt. For this checklist: if a namespace runs workloads and has zero NetworkPolicy objects, any pod that gets compromised can reach any other pod in the cluster by default. That's the single highest-leverage networking fix on this list.
For ingress specifically: confirm TLS is actually terminated (not just configured and silently falling back to HTTP), and check whether your ingress controller enforces rate limiting — an unauthenticated endpoint with no rate limit is an availability risk even when the application logic itself is correct.
Common mistakes
- Treating this as a one-time project. A cluster that passes every item above today drifts within a quarter as new namespaces get created without the enforce label, new workloads get shipped without a reviewed
securityContext, and RBAC grows by accretion. Security posture needs a recheck cadence, not a one-time pass. - Enforcing
restrictedPod Security Standards cluster-wide in one change. This breaks any workload that doesn't already meet it, usually in production, usually during a change window nobody planned for. Roll out namespace by namespace, starting with the newest and least-depended-on. - Scanning images without acting on results. A scanner that reports 400 CVEs and gates nothing is theater. Set an actual severity threshold that blocks the pipeline, even if it starts permissive and tightens over time.
- Assuming a managed control plane (EKS/GKE/AKS) covers workload security. The cloud provider secures the control plane. Everything covered in this article — RBAC,
securityContext, Pod Security Standards, NetworkPolicy, secrets, images — is entirely your responsibility regardless of who manages the control plane.
Checklist
- [ ] No workload runs
privileged: truewithout a documented, reviewed reason - [ ]
runAsNonRoot, explicitrunAsUser,allowPrivilegeEscalation: false, andcapabilities.drop: [ALL]are set on workload containers - [ ]
readOnlyRootFilesystem: truewhere the application allows it - [ ] No unreviewed
hostPath,hostNetwork,hostPID, orhostIPCusage - [ ] Every namespace has a Pod Security Standards
enforcelabel, not just a documented policy - [ ] No plaintext credentials in manifests or Git history
- [ ] RBAC doesn't grant broad Secret read access outside what's actually needed (see the RBAC audit)
- [ ] Production images are pinned to a digest, not a mutable tag
- [ ] Image scanning runs in CI with an enforced severity threshold
- [ ] Every namespace with workloads has a default-deny NetworkPolicy baseline
- [ ] Ingress TLS is verified end-to-end, not just configured
Conclusion
None of this is exotic — it's mostly explicit configuration replacing implicit defaults that were never designed to be secure out of the box. The hard part isn't knowing these items exist; it's finding out which ones are actually missing across every namespace, workload, and binding in a cluster that's grown for two years without a dedicated review, and prioritizing the fixes that matter before the ones that don't.
That's precisely what the Kubernetes Cluster Audit is built to do — a fixed-price, $500 assessment that runs through this checklist and the broader production-readiness list against your actual cluster and comes back with prioritized, written findings, not a generic report. See the sample audit report for what that looks like, or explore Kubernetes Security Consulting if you already know remediation work is coming.