Skip to main content

Kubernetes Security Best Practices: A Hardening Checklist

Hardening Kubernetes is a small set of enforceable controls, not a checklist of hundreds of flags: RBAC least privilege, Pod Security Standards (restricted where possible), seccomp and non-root workloads, default-deny NetworkPolicies, a locked-down API server, and image provenance gates. This guide gives you the controls in deployment order, the YAML to apply, the commands that prove each one works, and the realistic cost of every control.

Why default clusters are a liability

Kubernetes is secure by configuration, not by default. The API server is the only component that must authenticate callers, and most of the defaults that bite later are permissive on purpose so that a fresh cluster is easy to operate: pods may run privileged and as root, any pod can reach any other pod, every namespace's default service account carries a token that can talk to the API, and RBAC objects are only as good as the least-privilege discipline applied to them. The Kubernetes project's own RBAC good-practices page states it plainly: RBAC is "a key security control to ensure that cluster users and workloads have only the access to resources required to execute their roles."

The consequence of permissive defaults is that a single foothold becomes cluster compromise. The attack chains that dominate Kubernetes incidents follow a consistent shape:

Attack vectorDefault postureControl (section in this guide)
Exposed or weak API server accessAnonymous access allowed unless explicitly disabled; authorization must be switched from AlwaysAllow to RBACControl plane (P0)
Stolen or over-privileged service account tokenDefault service account token auto-mounted into every pod; broad Roles and ClusterRoles are commonRBAC (P0)
Container escape or privileged podNo admission policy: privileged, hostPID/hostNetwork, root, and missing seccomp all permittedPod security (P0)
Lateral movement inside the clusterNo isolation: every pod can reach every pod and the Kubernetes DNS serviceNetworkPolicy (P1)
Compromised or malicious imageImages pulled by mutable tag, unscanned, unsigned; secrets readable from the image layer or the podSupply chain (P1)
Vulnerable cluster componentAdd-on and control-plane versions drift from supported trackBaseline hygiene
  1. 1. Attacker

    Gets code execution in one pod (app bug or bad image)

  2. 2. Pod

    Auto-mounted service account token is readable

  3. 3. API server

    Broad RBAC lets the token create or read workloads

  4. 4. Cluster

    Flat network and privileged pods allow lateral movement

How one foothold becomes cluster compromise on default settings. Each arrow is a control in this guide.

These are not theoretical. IngressNightmare (CVE-2025-1974 and related CVEs, disclosed March 2025) was a critical remote code execution in the ingress-nginx add-on that shipped with many clusters; unauthenticated exploitation of a misconfigured admission webhook configuration gave attackers code execution inside cluster pods. It was fixed in ingress-nginx 1.12.1 and 1.11.5, and was exploited in the wild, exactly the pattern the NSA/CISA Kubernetes Hardening Guidance (first released 2021, updated 2022, now at version 1.2) and the CIS Kubernetes Benchmark exist to stop: an attacker who reaches one pod should not inherit the cluster.

Two reference documents define the target state. The CIS Kubernetes Benchmark is a scored, versioned control set for control-plane components, etcd, worker nodes, and cluster policy; kube-bench automates it. The NSA/CISA guidance adds threat-model framing and container-specific controls (non-root, read-only root filesystems, resource limits). NIST SP 800-190 provides the container-security foundation beneath both. This guide maps controls to all three, and each control below names its verification command: a control you cannot verify is a belief, not a control.

The P0-P2 hardening checklist

Work in priority order. P0 items stop the cluster-compromise chains above and are cheap. P1 items contain blast radius. P2 items are hygiene that prevents slow-burn exposure. Each row maps to the section that shows the exact configuration and verification.

PriorityControlVerify withSection
P0Disable anonymous access; enforce RBAC authorization on API server and kubeletkube-bench; unauthenticated API probe returns 401Control plane
P0Encrypt secrets at rest (EncryptionConfiguration)etcdctl read shows ciphertext prefixControl plane
P0No auto-mounted default service account tokens; RBAC least privilege per namespacekubectl auth can-i; inspect role bindingsRBAC
P0Enforce Pod Security Standards on namespaces (baseline minimum, restricted for app workloads)kubectl apply of a violating pod is rejectedPod security
P0Run workloads non-root with seccomp RuntimeDefault and no privilege escalationPod spec inspection; kube-bench policy checksPod security
P1Default-deny ingress and egress NetworkPolicies per namespacekubectl get networkpolicies; connectivity probeNetwork
P1Image scanning gate, digest-pinned immutable images, signature verificationtrivy image --exit-code; cosign verifySupply chain
P1Read-only root filesystem, resource limits on podsPod spec; kube-benchPod security
P2Scheduled kube-bench / Trivy Operator scans with tracked exceptionsScan job results in CI or clusterVerification
P2Kubernetes and add-on versions on a supported track; audit logging enabled and retainedkubectl version; audit policy inspectionBaseline hygiene

A note on managed clusters (EKS, AKS, GKE): control-plane checks for components the provider operates (API server flags, etcd) are the provider's responsibility. kube-bench ships platform profiles (EKS, GKE, AKS) that run the applicable subset, and the CIS EKS Benchmark is versioned separately from the generic CIS Kubernetes Benchmark. Run those profiles instead of the generic one, and focus your effort on the workload-side controls, which are always yours.

P0: Control plane: API server, kubelet, secrets at rest

The API server is the front door to every secret and every control plane capability. The three non-negotiable settings, all present in the CIS Kubernetes Benchmark control-plane section: no anonymous access, RBAC (not AlwaysAllow) as the authorization mode, and all component communication over TLS with verified client certificates. kubeadm clusters render these as static pod manifests under /etc/kubernetes/manifests/; the same flags belong in your kube-apiserver invocation on any installer.

Default-insecure vs hardened kube-apiserver flags

# default-insecure (or silently absent):
#   --anonymous-auth=true (default)  -> unauthenticated requests reach RBAC
#   --authorization-mode=AlwaysAllow -> any authenticated caller may do anything
#   --encryption-provider-config=    -> secrets stored as plaintext in etcd

# hardened: add to kube-apiserver (kubeadm: /etc/kubernetes/manifests/kube-apiserver.yaml)
--anonymous-auth=false
--authorization-mode=Node,RBAC
--service-account-lookup=true        # default; keeps deleted SA tokens unusable
--enable-admission-plugins=NodeRestriction
--encryption-provider-config=/etc/kubernetes/encryption/encryption-config.yaml
--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxage=30
--audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml

Verify each setting the way an attacker would, anonymously and from outside:

$ curl -sk https://<api-server>:6443/version
Unauthorized                    # <-- anonymous auth disabled: good
# (before the fix this returns the version JSON with no credentials)

# confirm the effective flags on the running process:
$ ps -ef | grep kube-apiserver | tr ' ' '\n' | grep -E 'anonymous-auth|authorization-mode'

Anonymous access exists so health probes and discovery can work without credentials; the CIS-correct posture is to disable it and make every caller authenticate. If a component genuinely needs unauthenticated access (for example an external health-check), scope it with an RBAC Role/ClusterRole bound to system:unauthenticated instead of leaving the door open.

Secrets at rest: EncryptionConfiguration

By default Kubernetes stores Secret objects as plaintext in etcd. Anyone who reads etcd (a backup file, a snapshot, a compromised etcd pod) reads every secret. Encryption at rest fixes that at the API server, which encrypts before writing:

# /etc/kubernetes/encryption/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              # 32 random bytes, base64-encoded:
              # openssl rand -base64 32
              secret: <base64 32-byte key>
      - identity: {}   # MUST be last: lets the API server read legacy plaintext once

Restart the API server with --encryption-provider-config pointed at the file, then re-create existing secrets so they are rewritten encrypted (encryption applies on write; existing secrets stay plaintext until rewritten). Verify by reading etcd directly. The value must carry the provider prefix:

$ ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/server.crt \
    --key=/etc/kubernetes/pki/etcd/server.key \
    get /registry/secrets/default/my-secret | head -1
k8s:enc:aescbc:v1:key1:2V7i1bU...   # ciphertext prefix: encrypted
# a plaintext value here (no k8s:enc: prefix) means encryption is NOT active

For production, prefer a KMS provider (cloud KMS or a local KMS like Vault) over a raw aescbc key in a file: KMS moves key management out of the control plane and supports key rotation without restart. Whatever the provider, protect the encryption key file with the same discipline as the etcd CA: anyone with the key and an etcd snapshot can decrypt.

Kubelet: authenticate and authorize every node request

The kubelet listens on port 10250 and can execute commands in pods, read pod secrets, and drain nodes. CIS worker-node checks require the kubelet to reject anonymous requests and to authorize via the API server (Webhook), never AlwaysAllow. kubeadm clusters configure this in the kubelet config file:

# /var/lib/kubelet/config.yaml (KubeletConfiguration)
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
authorization:
  mode: Webhook
protectKernelDefaults: true
seccompDefault: true          # apply RuntimeDefault to pods that set no profile
rotateCertificates: true

protectKernelDefaults: true makes the kubelet refuse to start if kernel hardening parameters (such as vm.overcommit_memory, kernel.panic, and kernel.panic_on_oops) are not set, closing the sysctl-tuning path to host compromise. The kubelet also needs --node-labels and TLS serving on 10250 only; a node with an open read-only port (legacy 10255 in older releases) is a recon gift. Check that nothing listens on it.

P0: RBAC: least privilege for humans and workloads

The Kubernetes RBAC good-practices documentation is unambiguous about the failure modes that matter: giving a principal list or watch on Secrets is equivalent to giving get (the list response includes secret contents); permission to create workloads implicitly grants access to the Secrets, ConfigMaps, and PersistentVolumes those pods can mount, plus the API access of any service account in the namespace; and membership in system:masters bypasses RBAC entirely and cannot be revoked by removing bindings. Membership of system:unauthenticated should be reviewed and removed: it grants anyone who can reach the API server network-wise whatever rights the binding carries.

Namespace-scoped roles instead of cluster-admin for humans

# Role: scoped to one namespace, no wildcards, no secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: payments
  name: payments-operator
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "services", "configmaps"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "statefulsets"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: payments
  name: payments-operator
subjects:
  - kind: User
    name: alice@example.com    # from your IdP mapping
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: payments-operator
  apiGroup: rbac.authorization.k8s.io

Verify least privilege by asking the API what a principal can actually do, and by listing who holds the crown jewels:

# what can this user do in this namespace?
$ kubectl auth can-i --list --namespace payments --as alice@example.com

# who holds cluster-admin today? (system: default bindings + YOUR additions)
$ kubectl get clusterrolebindings.rbac.authorization.k8s.io -o custom-columns=NAME:.metadata.name,SUBJECTS:.subjects[*].name | grep -i cluster-admin

# which service accounts can read secrets anywhere?
$ kubectl get clusterrolebindings.rbac.authorization.k8s.io -o json \
    | jq -r '.items[] | select(.roleRef.name=="cluster-admin" or .roleRef.name=="view") | .metadata.name'

Stop auto-mounting the default service account token

Every pod that does not opt out gets the namespace's default service account token mounted at /var/run/secrets/kubernetes.io/serviceaccount/. If that service account carries broad rights, any code execution in the pod (from insecure deserialization, template injection or anything else) is a step toward cluster admin. Two changes: disable auto-mount on the default service account (opt-in per pod), and keep the token off pods that never call the API.

# one-time per namespace: default SA no longer auto-mounts a token
$ kubectl patch serviceaccount default -n payments -p '{"automountServiceAccountToken": false}'

# or per pod (this also belongs in the hardened Deployment template in the next section):
# spec:
#   automountServiceAccountToken: false

# list namespaces where the default SA still auto-mounts:
$ kubectl get serviceaccounts -A -o json | jq -r '.items[] | select(.automountServiceAccountToken != false) | .metadata.namespace + "/" + .metadata.name'

Workloads that do need the API get a dedicated service account with a Role granting only the operations they perform. The standard pattern is one service account per workload, bound to a namespace Role, never to cluster-admin. Human break-glass access can be impersonation-based: keep a low-privilege account that can impersonate a named admin role, so actions are attributable and no standing superuser credential exists.

P0: Pod security: standards, seccomp, non-root, no privilege escalation

Pod Security Standards (PSS) define three cumulative policy levels. The Kubernetes documentation describes them exactly: Privileged is "unrestricted," Baseline is "minimally restrictive" and prevents known privilege escalations, and Restricted is "heavily restricted," following current pod hardening best practices. Enforcement is built in: the Pod Security Admission controller (stable since Kubernetes v1.25) evaluates every pod creation against the level configured on its namespace.

RequirementBaselineRestricted adds
Privileged containersForbidden(inherits baseline)
Host namespaces (network, PID, IPC)Forbidden(inherits baseline)
Added Linux capabilitiesRestricted to a small default setMust drop ALL; may add back only NET_BIND_SERVICE
hostPath volumesForbidden(inherits baseline)
allowPrivilegeEscalationNot restrictedMust be false
Root executionNot restrictedrunAsNonRoot must be true; runAsUser may not be 0
seccomp profileMust not be set to Unconfined (leaving it unset is allowed)Must be RuntimeDefault or Localhost (Unconfined and absent are rejected)
Volume typesAny type except hostPathRestricted allowlist (no hostPath, no projected host paths)

Roll out with warn and audit before enforce

The admission controller supports three modes per namespace: enforce (reject), audit (annotate the audit log), and warn (warn the caller). The safe rollout is: label with warn and audit first, fix the workloads that surface, then flip to enforce. Pin a version label so policy behavior does not drift silently across Kubernetes upgrades.

# step 1: observe (audit + warn) for a cycle
$ kubectl label ns payments \
    pod-security.kubernetes.io/audit=restricted \
    pod-security.kubernetes.io/warn=restricted

# step 2: enforce once workloads comply
$ kubectl label ns payments \
    pod-security.kubernetes.io/enforce=restricted \
    pod-security.kubernetes.io/enforce-version=v1.31 --overwrite

# see the configured level per namespace
$ kubectl get ns -l pod-security.kubernetes.io/enforce -o custom-columns=NS:.metadata.name,ENFORCE:.metadata.labels.pod-security.kubernetes.io/enforce

The restricted-compliant pod template

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
  namespace: payments
spec:
  replicas: 3
  selector:
    matchLabels:
      app: payments-api
  template:
    metadata:
      labels:
        app: payments-api
    spec:
      automountServiceAccountToken: false   # no API access needed
      securityContext:
        runAsNonRoot: true                  # image must not run as root
        seccompProfile:
          type: RuntimeDefault              # container runtime's default profile
      containers:
        - name: api
          image: registry.example.com/payments/api@sha256:9f3c...  # pinned digest
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
              add: ["NET_BIND_SERVICE"]     # bind :8443 as non-root
            runAsNonRoot: true
            runAsUser: 65532                # or the image's declared non-root USER
          resources:                        # hard limits: NSA/CISA guidance
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
          volumeMounts:
            - name: tmp
              mountPath: /tmp
          ports:
            - containerPort: 8443
      volumes:
        - name: tmp
          emptyDir: {}

This template passes the Restricted profile: non-root, privilege escalation off, all capabilities dropped, seccomp explicitly RuntimeDefault, read-only root filesystem, and no auto-mounted token. Note the trade-offs baked in: readOnlyRootFilesystem: true breaks any application that writes to its own image: mount emptyDir volumes at the writable paths (/tmp, /var/run, cache directories), and images that run as root must be rebuilt with a non-root USER before they can pass.

Verify enforcement by attempting to create a violating pod:

$ kubectl -n payments run nginx-privileged --image=nginx \
    --restart=Never --overrides='{"spec":{"containers":[{"name":"p","image":"nginx","securityContext":{"privileged":true}}]}}'
Error from server (Forbidden): pods "nginx-privileged" is forbidden: violates PodSecurity "restricted:v1.31": privileged (container "p" must not set securityContext.privileged=true)
# (example output — the exact wording tracks your policy level and version)

Seccomp is the control that limits what a container can do even after code execution, and it is the one most often missing. Since Kubernetes 1.19 the seccomp profile field is stable; setting RuntimeDefault applies the container runtime's default allowlist (which blocks the exotic syscalls that container escapes rely on), and the kubelet seccompDefault: true option (GA since Kubernetes 1.27) applies it to every pod that does not specify a profile. Privileged containers always run as Unconfined. Seccomp cannot be applied to them, which is one more reason the Restricted profile forbids privileged.

P1: NetworkPolicy: default deny, then allow what is necessary

Kubernetes network policy semantics reward a deny-first mindset: a pod is non-isolated (all traffic allowed) until a NetworkPolicy selects it, and policies are additive: the allowed set is the union of all matching policies. The Kubernetes documentation is explicit that policies apply to a connection only if both ends allow it: egress policy on the source pod and ingress policy on the destination pod must both permit the connection. Reply traffic for allowed connections is implicitly allowed, and traffic between a pod and its own node is always allowed.

Two prerequisites are worth stating before any YAML: a network plugin that implements NetworkPolicy enforcement (Calico, Cilium, OVN-Kubernetes, and the major managed offerings) is required: creating NetworkPolicy resources without a supporting plugin has no effect. And default-deny changes nothing you can see in a dashboard; it changes what works. Egress default-deny is also what stops an SSRF in one pod from reaching internal services and the cloud metadata endpoint.

Default-deny for the namespace

# deny-all.yaml — apply once per namespace you protect
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: payments
spec:
  podSelector: {}          # selects all pods in the namespace
  policyTypes:
    - Ingress
    - Egress

Then allow the real paths: web to api, api to db, api to DNS

# allow-ingress-to-api.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-api
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: payments-api
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: payments-web
      ports:
        - protocol: TCP
          port: 8443
---
# allow-egress-from-api.yaml — DNS (kube-system kube-dns) + the DB port only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-egress
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: payments-api
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    - to:
        - podSelector:
            matchLabels:
              app: payments-db
      ports:
        - protocol: TCP
          port: 5432

The DNS egress rule is the classic operational trap: once egress is denied, pods that cannot reach kube-dns fail name resolution everywhere, and the error messages look nothing like a network policy problem. The NSA/CISA guidance and most reference recipes therefore always pair a default-deny egress policy with an explicit DNS allowance. If your CNI supports FQDN-based egress rules (Cilium does), you can restrict DNS itself instead of allowing the whole kube-dns service.

# verify: every protected namespace must carry the deny-all policy
$ kubectl get networkpolicies -A
NAMESPACE   NAME          POD-SELECTOR   AGE
payments    default-deny  <none>         12d

# verify behavior: exec into the web pod and try the DB port directly
$ kubectl exec -n payments deploy/payments-web -- bash -c \
    'timeout 3 bash -c "</dev/tcp/payments-db/5432"' && echo OPEN || echo BLOCKED
BLOCKED   # expected when no policy allows web -> db:5432

P1: Supply chain: scan, pin, sign

An image is a compressed attack surface: the base OS, every layer's packages, and the runtime dependencies. Three controls cover the realistic threat model: scan images before deploy and gate on findings, reference images by digest so a mutable tag can never be swapped under you, and verify signatures at admission so only images your pipeline signed can run.

# scan in CI: fail the build on un-fixed HIGH/CRITICAL
$ trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
    registry.example.com/payments/api:1.4.2

# resolve a tag to a digest and pin it in the manifest
$ docker buildx imagetools inspect registry.example.com/payments/api:1.4.2
Digest: sha256:9f3c...  # -> use image: ...@sha256:9f3c... in the Deployment

# emit an SBOM as a build artifact (CycloneDX)
$ trivy image --format cyclonedx --output api.sbom.json \
    registry.example.com/payments/api:1.4.2

Signing without admission enforcement is ceremony. A Kyverno or OPA Gatekeeper policy that requires a valid cosign signature on every image turns the signature into a control: unsigned images are rejected at admission, which also stops "someone pushed to the same tag" supply-chain substitution even before digest pinning. The full implementation (Sigstore/cosign keys, Kyverno verifyImages rule, registry trust policy) is its own guide, but the pattern is stable: sign in CI after scan, verify at admission, and never let a mutable tag into a manifest.

P2: Verify continuously: kube-bench and image scanning in the loop

The CIS Kubernetes Benchmark is the scored control set behind most of the checks in this guide, and kube-bench automates it: it inspects control-plane component flags, etcd configuration, kubelet settings, and cluster policy objects, and reports PASS/FAIL/WARN per control. It needs host access (PID namespace and config directories) to check running processes, so the supported pattern is a one-shot Job on the node:

$ kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
job.batch/kube-bench created

$ kubectl logs job/kube-bench
[INFO] 1 Master Node Security Configuration
[INFO] 1.1 API Server
[PASS] 1.1.1 Ensure that the API Server pod specification file permissions are set to 644 or more restrictive
[FAIL] 1.1.12 Ensure that the API Server --anonymous-auth argument is set to false
[INFO] 2 Etcd Node Configuration
...
# (example output; section and check numbering follow your kube-bench/CIS version)

Two honest caveats. First, benchmark versions do not map one-to-one to Kubernetes releases. kube-bench auto-selects the test set from the detected Kubernetes version, and the CIS numbering scheme changed between editions (the CIS 2.0 profile targets Kubernetes 1.34-1.35 with its own check numbering). Always read the remediation text for your benchmark version rather than memorizing check numbers. Second, a benchmark is a floor, not a ceiling: it checks configuration, and several controls in this guide (default-deny NetworkPolicy coverage, secret handling, image signing) are checked weakly or not at all by scored benchmarks. That is why the P0-P2 checklist pairs each control with its own verification command.

For continuous operation, the Trivy Operator runs kube-bench and image/vulnerability scans in-cluster on a schedule and reports findings as Kubernetes custom resources; a scheduled scan with a tracked exception list beats a heroic annual audit. Whatever cadence you choose, the scan results must be reviewed by a human and the exceptions must be dated and owned.

What breaks when you enable this

Every control in this guide has an operational cost. Naming it up front is what makes the checklist executable instead of aspirational:

ControlTypical breakageMitigation
Restricted Pod Security StandardsImages that run as root or write to their root filesystem are rejected; NET_ADMIN/SYS_PTRACE workloads breakwarn/audit rollout; rebuild with non-root USER and emptyDir mounts; documented baseline namespace for infrastructure pods
Default-deny NetworkPolicyDNS resolution fails (egress to kube-dns not allowed); health checks and monitoring scrapes time out; cross namespace calls breakAlways pair egress deny with DNS allowance; add monitoring/ingress-controller namespaces to policies before flipping
Seccomp RuntimeDefaultWorkloads invoking uncommon syscalls fail at runtimeTest under RuntimeDefault early; audit-mode seccomp profiles for the rare workload that needs more
Encryption at restRotating the provider key requires API server restart; lost key = undecryptable secretsKMS provider with automatic key rotation; test restore from etcd snapshot in staging
No auto-mounted SA tokenWorkloads that implicitly used the API break with 403sGive each workload an explicit service account + Role; monitor API error rates after rollout
Image scan + signature gatesDeploys fail on new HIGH/CVEs; unsigned hotfixes are blockedTriage SLA for new findings; keep the signing key path in the incident runbook

Recommended order of operations for an existing cluster: (1) close the P0 configuration gaps on the control plane and kubelet; (2) label the highest-risk namespaces with Pod Security warn/audit and fix what surfaces; (3) apply default-deny NetworkPolicies namespace by namespace with the DNS rule in place, starting in staging; (4) flip to enforce and remove auto-mounted tokens; (5) wire scan/sign gates into CI; (6) schedule kube-bench and review the report monthly. Each step is independently verifiable with the commands in its section, which means each step can be reverted cleanly if a workload breaks.

Key takeaways

  • Kubernetes is permissive by default; the highest-leverage controls are cheap configuration: disable anonymous access, enforce RBAC, encrypt secrets at rest.
  • Pod Security Standards with the built-in admission controller (enforce baseline minimum, restricted for workloads) replace the retired PodSecurityPolicy machinery and are stable since Kubernetes 1.25.
  • The restricted baseline for every app pod: non-root, allowPrivilegeEscalation false, drop ALL capabilities, seccomp RuntimeDefault, read-only root filesystem with emptyDir for writable paths.
  • NetworkPolicies are additive and require both ends to allow a connection: default-deny per namespace, always with an explicit DNS egress rule, or nothing resolves.
  • RBAC failure modes to hunt: secrets list/watch equals read, workload creation equals namespace rights, wildcard privileges, and any human in cluster-admin or system:masters.
  • Scan images with severity gates, pin digests, and verify signatures at admission; benchmarks (CIS via kube-bench) are the floor, and each control needs its own verification command to be real.
  • Roll out namespace by namespace with warn/audit before enforce, staging first: every control here has a named breakage and a named mitigation.

Frequently asked questions

What are the most important Kubernetes security controls?
Start with cheap configuration: disable anonymous API access, enforce RBAC with least privilege, and encrypt secrets at rest. Then enforce Pod Security Standards (baseline minimum, restricted for workloads), add default-deny NetworkPolicies per namespace, and gate images by scan, digest pinning and signature.
What replaced PodSecurityPolicy?
Pod Security Admission, the built-in admission controller that enforces the Pod Security Standards (privileged, baseline, restricted) through namespace labels. It has been stable since Kubernetes 1.25, when PodSecurityPolicy was removed.
Why does a default-deny NetworkPolicy break DNS?
Default-deny egress blocks every outbound connection from the selected pods, including DNS queries to the cluster DNS service, so nothing resolves. Add an explicit egress rule allowing UDP and TCP port 53 to kube-dns alongside the deny policy.
How do you verify a Kubernetes hardening baseline?
Run kube-bench against the CIS Kubernetes Benchmark as a floor, then check each control with its own command, for example that the anonymous API request is rejected, that namespaces carry the Pod Security enforce label, and that no human is bound to cluster-admin.

Kokkuvõte eesti keeles

Kubernetes ei ole vaikimisi turvaline: turvalisus tuleb konfiguratsioonist. Käesolev juhend annab tegevusjärjekorras kontrollnimekirja: (P0) API-serveri anonüümse juurdepääsu keelamine ja RBAC-kasutusloogika, secret-ide krüpteerimine puhkeolekus (etcd-s), kubeleti autentimine ja autoriseerimine, Pod Security Standards'i jõustamine (restricted profiil rakenduste jaoks: mitte-root, ilma privilege-eskalatsioonita, kõik capability'd eemaldatud, seccomp RuntimeDefault, kirjutuskaitstud juurfailisüsteem), (P1) vaikimisi keelavad NetworkPolicy'd koos DNS-reegliga ning piltide skaneerimine, digesti kasutamine ja allkirjastamine, (P2) pidev kube-bench skaneerimine. Iga kontrolli juures on nii YAML-konfiguratsioon kui ka käsk, mis tõestab, et kontroll töötab, ning iga kontrolli "hind" ehk see, mis selle sisselülitamisel katki läheb ja kuidas seda leevendada. Soovituslik kasutuselevõtt: warn/audit enne enforce'i, staadium enne toodangut, namespace haaval. Täielikud konfiguratsioonid ja käsud on ülal inglise keeles.

Sources

Related Guides

Injection & Deserialization Attacks

Business Logic & Concurrency

Infrastructure & Configuration Hardening

All security guides by attack family