📦 Container and Kubernetes Hardening

hardening & compliance / survival guide

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)