Container and Kubernetes Hardening: from the Dockerfile to the cluster
Covers: images and Dockerfiles, Docker/Podman on a single host, Kubernetes pods, admission, RBAC, network policy, secrets, kubelet and control plane, supply chain, detection, and a rollout plan
Facts checked 2026-09-29 against the upstream Kubernetes docs (current release: 1.37, released August 26, 2026), Docker and OWASP guidance, and vendor notices. Kubernetes moves fast: always check the docs for YOUR version.
The one idea that explains everything
A container is not a small virtual machine. It is an ordinary Linux process with a restricted view of the system, and every container on a node SHARES the host kernel. So container security is layers, and the layers below can undo the ones above:
image What is inside: packages, users, secrets you forgot about
container How it runs: user, capabilities, filesystem, seccomp, resource limits
pod / cluster Who may run what, who may talk to whom, who may read which secret (Kubernetes)
node and kernel The operating system underneath. A kernel bug can break out of any container: see Dirty Frag and Copy Fail on the Security tab
supply chain Where images and manifests come from, and whether you can prove it
detection Logs and alerts so you notice when the other layers fail
Managed Kubernetes (EKS, AKS, GKE and similar): the provider looks after the control plane. Your job is still workloads, RBAC, network policies, secrets, node configuration (unless the mode is fully managed), and the images. Read your provider's shared-responsibility page.
Running Azure Kubernetes Service (AKS) with Gatekeeper? Read the AKS Hardening sheet on this tab as well: it covers the Azure Policy add-on, Deployment Safeguards, node images and the AKS-specific defaults (for example seccomp and Pod Security both default to no restrictions on AKS Standard).
Only run the commands on clusters and hosts you are authorised to change. Test on a non-production cluster first. The "Rollout plan" near the end is written for exactly that.
PART 1: IMAGES AND DOCKERFILES
Build small, boring, non-root images
- Start from a minimal, maintained base (a "slim" or distroless image), not a full OS. Fewer packages = fewer vulnerabilities and fewer tools for an attacker.
- Pin the base image by digest (image@sha256:...), not just a tag, so a rebuild cannot silently change under you. Update the digest on a schedule.
- Use multi-stage builds: compilers and build tools stay in the build stage, only the result goes in the final image.
- Create and use a non-root user with a NUMERIC UID (see below for why).
- Never put secrets in the image: not in ENV, ARG, COPY or a layer you later "delete". Layers are forever. Use runtime secrets.
- Use a .dockerignore so .git, .env and keys are not copied in.
- Do not install curl, wget, shells or package managers in the final image unless you need them.
A reasonable pattern:
FROM python:3-slim@sha256:REPLACE_WITH_THE_DIGEST_YOU_TESTED
RUN groupadd --system --gid 10001 app && useradd --system --uid 10001 --gid app --no-create-home app
WORKDIR /app
COPY --chown=10001:10001 requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=10001:10001 . .
USER 10001:10001
EXPOSE 8080
CMD ["python", "app.py"]
Listen on a port above 1024 (8080 here) so you do not need extra privileges.
Scan, list and sign
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 registry.example.com/app:1.4.2 # fail a CI build on serious, fixable findings
trivy config . # scan Dockerfiles, Kubernetes YAML and Terraform for misconfigurations
Also: an SBOM (software bill of materials) per image, and image signing (for example cosign from the Sigstore project) so admission policy can check that only YOUR pipeline's images run.
Scan the same image again later: new vulnerabilities are published every day for code you have not changed.
PART 2: RUNNING CONTAINERS ON A SINGLE HOST (Docker or Podman)
These flags come from OWASP's Docker Security Cheat Sheet and Docker's documentation.
docker run -u 10001 --read-only --tmpfs /tmp --cap-drop all --security-opt=no-new-privileges \
--memory=512m --cpus=0.5 -p 127.0.0.1:8080:8080 registry.example.com/app:1.4.2
-u 10001 run as a non-root user
--read-only --tmpfs /tmp read-only root filesystem, with a writable scratch area only where needed
--cap-drop all drop every Linux capability, then add back only what you need: --cap-add CHOWN
--security-opt=no-new-privileges stop setuid tricks from gaining privileges
--memory / --cpus limits, so one container cannot starve the host
-p 127.0.0.1:8080:8080 bind a published port to localhost unless the world must reach it (a bare -p 8080:8080 listens on ALL interfaces, and Docker's iptables rules can bypass ufw)
NEVER:
--privileged (turns most isolation off)
-v /var/run/docker.sock:/var/run/docker.sock (whoever holds the Docker socket is root on the host)
-H tcp://0.0.0.0:2375 (an open, unauthenticated Docker API is remote root)
Also: a user who is in the docker group is effectively root on that machine. Treat membership like sudo.
Keep the default seccomp and AppArmor/SELinux profiles on. Do not disable them to make something work: find out which call or path is blocked instead.
The same ideas in Docker Compose:
services:
app:
image: registry.example.com/app@sha256:REPLACE_WITH_DIGEST
user: "10001:10001"
read_only: true
tmpfs: [ "/tmp" ]
cap_drop: [ "ALL" ]
security_opt: [ "no-new-privileges:true" ]
ports: [ "127.0.0.1:8080:8080" ]
mem_limit: 512m
Run the daemon with less power:
Rootless mode (daemon AND containers run as an ordinary user), as documented by Docker:
sudo apt install uidmap # provides newuidmap and newgidmap (needed; the package name can differ by distribution)
dockerd-rootless-setuptool.sh install
export DOCKER_HOST=unix:///run/user/1000/docker.sock
systemctl --user restart docker.service
# needs at least 65,536 subordinate UIDs/GIDs for the user in /etc/subuid and /etc/subgid
userns-remap (the daemon still runs as root, but root inside a container maps to an unprivileged user on the host):
/etc/docker/daemon.json: { "userns-remap": "default", "no-new-privileges": true }
Rootless is stronger. Both have side effects (privileged containers, host networking and some volume setups behave differently): test your workloads.
Podman runs rootless by default and has no always-on root daemon, which is one reason people choose it.
Check a Docker host against the CIS Docker Benchmark with Docker Bench for Security (read the script before you run it):
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security && sudo sh docker-bench-security.sh
Docker Secrets exist for Swarm services; on plain hosts use a secrets manager or files mounted read-only, never ENV variables baked into an image or a Compose file in git.
PART 3: KUBERNETES PODS
Pod Security Standards: the three levels
Privileged Unrestricted. For system and infrastructure workloads only.
Baseline Blocks the known privilege escalations: no privileged containers, no hostNetwork/hostPID/hostIPC, no hostPath volumes, only a short list of capabilities, seccomp must not be Unconfined.
Restricted Current best practice. Everything in Baseline, plus:
runAsNonRoot: true (and no runAsUser of 0)
allowPrivilegeEscalation: false
capabilities: drop: ["ALL"] (only NET_BIND_SERVICE may be added back)
seccompProfile: type: RuntimeDefault (or Localhost)
volume types limited to configMap, downwardAPI, emptyDir, persistentVolumeClaim, projected, secret
The Standards do NOT require readOnlyRootFilesystem or resource limits. Those are good practice, and you add them yourself (see admission policy below).
A pod spec that meets Restricted, plus the good-practice extras:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
selector:
matchLabels: { app: app }
template:
metadata:
labels: { app: app }
spec:
automountServiceAccountToken: false # no API token unless the app really calls the Kubernetes API
# hostUsers: false # user namespaces: see below for the requirements
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/app@sha256:REPLACE_WITH_DIGEST
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # good practice, not required by Restricted
capabilities:
drop: ["ALL"]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 128Mi } # memory limit equal to the request, as the Kubernetes security checklist advises
volumeMounts:
- { name: tmp, mountPath: /tmp } # a writable scratch dir, since the root filesystem is read-only
volumes:
- name: tmp
emptyDir: {}
The Kubernetes security checklist also asks for a CPU limit on sensitive workloads, and a memory limit no higher than the request (a higher limit can expose the whole node to out-of-memory trouble). Many teams skip CPU limits elsewhere to avoid throttling.
Enforcing the Standards: Pod Security Admission (built in, stable since Kubernetes 1.25, replaced PodSecurityPolicy)
It works through labels on each namespace:
pod-security.kubernetes.io/enforce: restricted blocks pods that break the level
pod-security.kubernetes.io/audit: restricted records violations in the audit log
pod-security.kubernetes.io/warn: restricted shows a warning to the user
pod-security.kubernetes.io/enforce-version: v1.37 (optional) pin the policy to a Kubernetes version, or use latest
Roll out safely: warn and audit first, see what would break, then enforce.
kubectl label --dry-run=server --overwrite ns --all pod-security.kubernetes.io/enforce=baseline # what WOULD be rejected? (changes nothing)
kubectl label --overwrite ns --all pod-security.kubernetes.io/audit=baseline pod-security.kubernetes.io/warn=baseline
kubectl label --overwrite ns my-namespace pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=v1.37
Details that bite:
- Enforce applies to the resulting PODS, not to Deployments: a Deployment is accepted and then its pods are refused. Watch the ReplicaSet events, and rely on warn/audit to catch the workload earlier.
- Exemptions (users, runtime classes, namespaces) are configured in the admission controller. Do not exempt controller service accounts: that quietly exempts everyone who can create the matching workload.
- System namespaces (kube-system, CNI, storage drivers, monitoring agents) often need Privileged or Baseline. Give them their own level, do not weaken the rest.
Beyond the Standards: admission policy
Pod Security Admission does not check images, labels, resource limits or readOnlyRootFilesystem. Three ways to add rules:
ValidatingAdmissionPolicy Built in, stable since 1.30, rules written in CEL, no webhook to run. Good for simple rules.
OPA Gatekeeper Rego-based constraints; a large policy library; audits existing objects too.
Kyverno Kubernetes-style YAML policies; can also mutate and verify image signatures.
A ValidatingAdmissionPolicy that refuses the :latest tag (start with Warn/Audit, then Deny; Deny and Warn cannot be combined):
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: no-latest-tag.example.com
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest'))"
message: "Pin images by version or digest, not :latest"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: no-latest-tag-binding.example.com
spec:
policyName: no-latest-tag.example.com
validationActions: [Warn, Audit] # later: [Deny]
matchResources:
namespaceSelector:
matchLabels: { environment: production }
If your cluster runs Gatekeeper, remember it applies to EVERYONE, including admins doing debugging. The "Logging In to Kubernetes Nodes with kubectl debug (and what to do when Gatekeeper blocks it)" sheet on the Cloud Tutorials tab covers that exact trap.
Go one layer deeper on the pod
- User namespaces (hostUsers: false, stable since Kubernetes 1.36): root inside the container is an unprivileged user on the host, so many container-breakout bugs stop working. Kubernetes lists these requirements: Linux 6.3 or newer on the node, containerd 2.0+ or CRI-O 1.25+, runc 1.2+ or crun 1.9+, and filesystems that support idmapped mounts. Test on one node pool first. It does not fix kernel bugs, but it limits the blast radius.
- Seccomp: RuntimeDefault everywhere (Restricted requires it). Localhost profiles for sensitive workloads.
- AppArmor or SELinux: keep them on (the Standards allow RuntimeDefault or Localhost AppArmor and the standard SELinux types).
- Sandboxed runtimes (gVisor, Kata Containers) add a second boundary for untrusted code. Kubernetes' own FAQ notes that sandboxed pods should still meet the Restricted level.
- Dedicated nodes: use taints, tolerations and node affinity so powerful or sensitive pods do not share nodes with untrusted workloads (also advised in Kubernetes' RBAC good practices).
- Do not run pods that mount the Docker or containerd socket, use hostPath, or use hostNetwork unless there is a written reason.
PART 4: WHO CAN DO WHAT (RBAC AND SERVICE ACCOUNTS)
From the Kubernetes RBAC good practices:
- Least privilege. Prefer Roles and RoleBindings (one namespace) over ClusterRoles and ClusterRoleBindings.
- No wildcards (*) in verbs or resources.
- Never use system:masters for people or components. It bypasses RBAC entirely: keep it as a break-glass certificate, not a daily login.
- Use cluster-admin only where truly needed, bound to a GROUP from your identity provider, with short-lived credentials (OIDC/SSO).
- Remove bindings to system:unauthenticated.
- Set automountServiceAccountToken: false by default (on the ServiceAccount or the pod) and turn it on only for workloads that talk to the API.
Permissions that are more powerful than they look (each can lead to cluster-wide access):
- create on pods or workloads: lets the holder read every Secret in the namespace and run as any service account there, unless Pod Security Standards stop it
- list and watch on secrets: returns the secret contents, the same as get
- create on serviceaccounts/token, escalate, bind, impersonate: privilege escalation verbs
- create on persistentvolumes: hostPath volumes give node filesystem access
- nodes/proxy: direct access to the kubelet API
- write access to validating or mutating webhook configurations, and create on certificatesigningrequests
Service account tokens are short-lived, bound tokens by default on current versions. Do not create long-lived Secret-based tokens.
Check it:
kubectl auth can-i --list # what can I do here?
kubectl auth can-i --list --as=system:serviceaccount:my-namespace:my-sa # what can that service account do?
kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name + " -> " + ([.subjects[]? | .kind + ":" + .name] | join(", "))'
kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.subjects[]?.name=="system:unauthenticated") | .metadata.name' # should print nothing
Review access on a schedule. Old checklists for people who left are a classic finding.
PART 5: NETWORK POLICY
By default every pod can talk to every other pod. NetworkPolicy fixes that, but only if your CNI plugin enforces it: creating a NetworkPolicy on a cluster whose CNI does not support them silently does nothing. Check your CNI's documentation.
Policies are additive (the union of everything that applies to a pod), so start with default deny and then allow what is needed. Per namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
After default-deny egress, pods cannot even resolve DNS. Allow it:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
Block workloads from the cloud metadata service (169.254.169.254), which hands out cloud credentials (the Kubernetes checklist recommends filtering it). This example allows outbound traffic EXCEPT to that address:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-no-metadata
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
Also from the checklist: never expose the Kubernetes API, the kubelet API or etcd to the internet, and restrict LoadBalancer and ExternalIPs use (the DenyServiceExternalIPs admission controller helps).
NetworkPolicy is layer 3/4 only. For encryption between services or per-request rules, use a service mesh or your CNI's extended policies.
PART 6: SECRETS
- A Kubernetes Secret is base64-ENCODED, not encrypted. By default the API server stores Secrets in etcd in plain text. Anyone who can read etcd or its backups can read every secret.
- Turn on encryption at rest. The kube-apiserver needs --encryption-provider-config=<file>, containing something like:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <BASE64 OF 32 RANDOM BYTES> # generate: head -c 32 /dev/urandom | base64
- identity: {} # LAST: lets the API server still read old, unencrypted data
Kubernetes documentation recommends a KMS provider (v2) for production, so the key lives in a KMS or HSM instead of in a file on the control-plane node. On managed clusters turn on the provider's envelope encryption with your own KMS key.
- Existing secrets are NOT encrypted until they are rewritten:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
- Rotating keys: add the new key FIRST in the list (the first key of the first provider is used to encrypt, all keys are tried to decrypt), re-encrypt everything, then remove the old key.
- Least privilege for reading secrets (see RBAC): remember that list and watch reveal contents.
- Prefer an external secrets manager (Vault, your cloud's secret store, with the External Secrets Operator or a CSI driver) and mount secrets as files rather than environment variables, which leak into logs and crash dumps.
- Never commit secrets to git. If one leaked, rotate it: deleting the commit is not enough.
PART 7: THE CLUSTER ITSELF (control plane, kubelet, nodes)
Kubelet: the Kubernetes docs warn that by default (no configuration) requests to the kubelet's HTTPS endpoint that are not rejected are treated as anonymous, and the default authorization mode is AlwaysAllow. Set it explicitly (KubeletConfiguration; the equivalent flags are --anonymous-auth=false, --client-ca-file, --authentication-token-webhook=true and --authorization-mode=Webhook):
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false
webhook:
enabled: true
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
readOnlyPort: 0 # switch off the old unauthenticated read-only port
Kubeadm and managed services usually ship safe defaults: verify, do not assume. From another machine on your OWN network:
curl -sk https://NODE_IP:10250/pods # should answer 401 Unauthorized, never a list of pods
curl -s http://NODE_IP:10255/pods # should refuse to connect (read-only port off)
API server and etcd:
- The API server should be reachable only from where it needs to be (private endpoint, VPN, allow-listed CIDRs), with SSO/OIDC for people.
- etcd: TLS between members and from the API server, client certificate authentication, and network access limited to the API server. Encrypt etcd backups.
- Turn on API audit logging (--audit-policy-file, --audit-log-path and friends; on managed clusters, enable the control-plane logs). It is the record of who did what.
- Controller manager: --use-service-account-credentials enabled (from the Kubernetes checklist). Certificates: the checklist advises leaf certificates expire no later than 3 years out.
Nodes and kernel:
- Use a minimal, container-focused node OS where you can, and patch nodes on a schedule. Draining and rebooting nodes for kernel updates is normal in Kubernetes.
- Patch the container runtime. Example: in November 2025 three runc container-escape flaws (CVE-2025-31133, CVE-2025-52565, CVE-2025-52881) were disclosed; runc versions 1.2.8, 1.3.3 and 1.4.0-rc.3 contain the fixes. Check yours with runc --version and containerd --version, or your managed service's release notes.
- A shared kernel means kernel bugs are container-escape bugs. See the Dirty Frag and Copy Fail sheets on the Security tab: patching the NODE kernel fixes them for every container on it.
- Do not run kubectl debug node/... with a privileged image on production casually. When you must, do it through a controlled, audited procedure.
Benchmark the cluster:
kube-bench checks NODE and control-plane configuration against the CIS Kubernetes Benchmark (not your workloads).
kubectl apply -f job.yaml # the Job from the kube-bench repository: runs a pod that reads the node config, then: kubectl logs job/kube-bench
kube-bench run --targets node # or run it directly on a node (options: --benchmark, --version, --json)
Managed services have their own job files and benchmarks: job-eks.yaml, job-aks.yaml, job-gke.yaml.
Benchmark versions trail Kubernetes releases: when checked, kube-bench covered CIS Kubernetes Benchmark versions up to 1.12 (for Kubernetes 1.32 to 1.34), while the current Kubernetes was 1.37. Read the results with that in mind, and choose the benchmark version that matches your cluster.
Stay on a supported version. Around the time of checking Kubernetes 1.34 through 1.37 were supported, and 1.34's maintenance period was ending on October 27, 2026 (see endoflife.date/kubernetes). Old clusters stop getting security fixes.
PART 8: SUPPLY CHAIN AND THE THINGS YOU RUN ON TOP
- Pull only from registries you trust and control. Mirror what you need into a private registry, and scan it there.
- Deploy by digest (image@sha256:...) or immutable tags. Never :latest.
- Verify signatures at admission (Kyverno or Gatekeeper with a verification provider; or your platform's policy tooling).
- Keep CI/CD credentials short-lived and minimal: the pipeline that can deploy to the cluster is the most valuable target you have. See the GitHub Actions sheet on the DevOps tab.
- Use Helm and Kustomize with the same discipline: review rendered manifests (helm template) before applying, and scan them (trivy config).
- Retired software: the community ingress-nginx controller (kubernetes/ingress-nginx) was retired in March 2026: no more releases, bug fixes or security patches. The Ingress API itself still exists, and other controllers are maintained, but SIG Network recommends moving to Gateway API or another ingress controller. Find out if you run it:
kubectl get pods -A -o wide | grep -i ingress-nginx
kubectl get deploy,ds -A -l app.kubernetes.io/name=ingress-nginx
If you do, put migration on the plan now: an internet-facing controller without security patches is a liability.
PART 9: DETECT AND RESPOND
Prevention fails sometimes, so log the events that matter and alert on them:
- API audit logs: creation of privileged pods, changes to RBAC, access to secrets, exec or port-forward into pods, changes to admission webhooks and policies.
- Runtime detection: Falco (a CNCF project) watches syscalls and flags things like a shell starting inside a container, writes under /etc, or an unexpected outbound connection. Tetragon is a similar eBPF-based option.
- Send container logs, node logs and audit logs to a central place the cluster's admins cannot silently edit.
- Have a plan for "a pod is compromised": isolate it with a NetworkPolicy or a label, capture evidence (logs, a copy of the filesystem), rotate any secrets it could read, then replace it. Do not just delete the pod and move on.
CHECK YOUR WORK
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.containers[]?.securityContext?.privileged==true) | "\(.metadata.namespace)/\(.metadata.name)"' # privileged containers
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.hostNetwork==true) | "\(.metadata.namespace)/\(.metadata.name)"' # hostNetwork pods
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.volumes[]?.hostPath) | "\(.metadata.namespace)/\(.metadata.name)"' # hostPath volumes
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.volumes[]?.hostPath.path=="/var/run/docker.sock") | "\(.metadata.namespace)/\(.metadata.name)"' # Docker socket mounts
kubectl get netpol -A # which namespaces have any network policy at all?
kubectl get ns --show-labels | grep pod-security # which namespaces have Pod Security labels?
kubectl label --dry-run=server --overwrite ns --all pod-security.kubernetes.io/enforce=restricted # what would Restricted reject today?
trivy k8s --report summary # vulnerabilities and misconfigurations across the cluster (Trivy's flags change between versions: check trivy k8s --help)
kube-bench run --targets node # node configuration against the CIS benchmark
Popeye (see the sheet on the DevOps tab) is a handy cluster sanitizer for general hygiene. It complements, it does not replace, the checks above.
System pods (CNI, CSI drivers, monitoring agents) will appear in the first lists. That is expected: understand each one, and give them their own documented exceptions.
ROLLOUT PLAN (a sane order for a real cluster)
1. Inventory. Which namespaces, which workloads, which owners, which images. Fix your view of what is actually running before changing anything.
2. Patch and upgrade the basics: supported Kubernetes version, current node OS and kernel, patched runtime.
3. Pod Security Admission in warn and audit mode on every namespace. Collect the results for a week or two.
4. Fix the worst offenders first (privileged, hostPath, hostNetwork, docker.sock, running as root). Move each namespace to enforce, one at a time, starting with the easiest.
5. RBAC review: cluster-admin bindings, wildcards, secrets access, service account tokens. Remove what nobody can explain.
6. Network policy: default-deny in one namespace at a time, with DNS allowed, then the specific flows. Test with real traffic.
7. Secrets: encryption at rest, then an external secrets manager, then rotation.
8. Admission policies (ValidatingAdmissionPolicy, Gatekeeper or Kyverno) in warn/audit mode, then deny.
9. CI/CD: image scanning, digest pinning, signing.
10. Detection: audit logs and runtime alerts; run kube-bench on a schedule.
11. Write it down: exceptions with owners and review dates. It is what auditors and your future colleagues will ask for.
Common problems
"After enforcing Restricted my pods will not start": read the events. Typical reasons: the image runs as root, no seccomp profile, capabilities not dropped, or allowPrivilegeEscalation not set to false. Fix the pod spec, or move that workload to a namespace with a lower level (with a written reason).
"container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root": Kubernetes cannot tell if a NAME is root. Use a numeric USER in the Dockerfile, or set runAsUser in the pod.
"My web server needs to listen on port 80": use a port above 1024 and put the Service or ingress in front (for nginx, an "unprivileged" image variant listens on 8080). Only add NET_BIND_SERVICE if you really need a low port.
"The app crashes with a read-only file system": it writes somewhere. Find where (logs, /tmp, caches, /var/run) and mount an emptyDir there, or fix the app to log to stdout.
"My NetworkPolicy has no effect": the CNI does not enforce it. Verify with a test connection that should be blocked.
"Everything broke after default-deny egress": DNS (and probably your metrics, mesh and API access) was blocked. Add the DNS policy and the flows you need.
"hostUsers: false is rejected or ignored": the node kernel, container runtime or filesystem does not meet the requirements (Linux 6.3+, a recent runtime). Check the node pool.
"A sidecar or init container needs extra capabilities": some service meshes and CNI helpers need capabilities such as NET_ADMIN. Give those pods (and only those) a narrow exception, or use the mesh's CNI mode.
"Gatekeeper or PSA blocks my debug pod": that is the point. Use an approved debug procedure, and see the Cloud Tutorials sheet on kubectl debug and Gatekeeper.
"kube-bench fails everything on my managed cluster": use the managed-service job (EKS, AKS, GKE). You cannot see or change the provider's control plane.
"Secrets look encrypted but I can decode them": that is base64. Anyone with get access can read them. Encrypt at rest AND restrict access.
"Trivy shows hundreds of vulnerabilities": start with HIGH and CRITICAL that have fixes (--ignore-unfixed), rebuild on a newer base image, and track the rest. Many are in packages your app never uses, which is the argument for smaller images.
Tips
Start with the biggest risks: run as non-root, drop capabilities, no privileged pods, no docker.sock, default-deny networking and tight RBAC. That stops most real-world container attacks.
Prefer safe defaults over heroics: a namespace label and a good pod template do more than a heap of custom tools.
Automate the checks in CI and admission, so nobody has to remember them.
Keep images small and rebuild them often. An image that was clean in June is not clean in September.
Treat the node kernel as part of the container's attack surface, because it is.
Give every exception an owner and an expiry date. "Temporary" privileged pods live for years.
Kubernetes is just someone else's Linux with more YAML. The old rules (least privilege, patching, logging) still apply. They just have more indentation.
Sources (checked 2026-09-29)
Kubernetes, Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/
Kubernetes, Pod Security Admission: https://kubernetes.io/docs/concepts/security/pod-security-admission/
Kubernetes, enforce Standards with namespace labels: https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/
Kubernetes, security checklist: https://kubernetes.io/docs/concepts/security/security-checklist/
Kubernetes, RBAC good practices: https://kubernetes.io/docs/concepts/security/rbac-good-practices/
Kubernetes, network policies: https://kubernetes.io/docs/concepts/services-networking/network-policies/
Kubernetes, encrypting Secret data at rest: https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/
Kubernetes, user namespaces: https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/
Kubernetes, ValidatingAdmissionPolicy: https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
Kubernetes, kubelet authentication/authorization: https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/
Kubernetes release and support status: https://endoflife.date/kubernetes
OWASP Docker Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html
Docker rootless mode: https://docs.docker.com/engine/security/rootless/
kube-bench (CIS Kubernetes Benchmark checks): https://github.com/aquasecurity/kube-bench
NSA/CISA Kubernetes Hardening Guidance (v1.2, Aug 2022; check for newer versions): https://media.defense.gov/2022/Aug/29/2003066362/-1/-1/0/CTR_KUBERNETES_HARDENING_GUIDANCE_1.2_20220829.PDF
PCI SSC, Guidance for Containers and Container Orchestration Tools (2022 information supplement): https://blog.pcisecuritystandards.org/new-information-supplement-guidance-for-containers-and-container-orchestration-tools
runc container-escape flaws (Nov 2025), technical overview: https://www.cncf.io/blog/2025/11/28/runc-container-breakout-vulnerabilities-a-technical-overview/
Ingress NGINX retirement notice: https://www.kubernetes.dev/blog/2025/11/12/ingress-nginx-retirement/
Related sheets: AKS Hardening, Linux Hardening Basics, CIS Benchmarks, PCI DSS (this tab); Dirty Frag and Copy Fail (Security tab); Kubernetes, Docker, Helm, Popeye and GitHub Actions (DevOps tab); Kubernetes node access (Cloud Tutorials tab)