In short

The provider secures the control plane; you still own who can reach and call the API, node patching on standard node pools, workload privileges, network policy and admission. Make the API endpoint private or restrict it to known addresses, and authenticate people through the cloud identity provider. Remove static administrator paths: use EKS access entries instead of the aws-auth ConfigMap, and disable AKS local accounts. Give pods their own cloud identity and keep them away from the node's. Keep nodes on current images, enforce Pod Security restricted with seccomp RuntimeDefault, and add AppArmor and user namespaces (hostUsers: false, GA in 1.36) on nodes that support them. Then check the cluster with kube-bench against your service's CIS benchmark.

  • EKS Auto Mode, GKE Autopilot and AKS Automatic shift node patching to the provider. On standard node pools you choose when nodes update: AKS publishes node images weekly and supports images up to 90 days old.
  • Defaults favor convenience. An EKS API endpoint is public, its creator is an invisible cluster administrator, and a cluster created through the API starts in CONFIG_MAP mode. AKS local accounts bypass Microsoft Entra ID until you disable them.
  • Pod Security restricted rejected a privileged, hostPID pod on Kubernetes 1.37 and listed all six violations; on GKE Autopilot, RuntimeDefault seccomp applies to every pod.
  • User namespaces are GA in 1.36. In our 1.37 test, root in the pod mapped to host UID 2001731584. They need Linux 6.3 or later and containerd 2.0 or CRI-O 1.25 or later.
  • AppArmor fails closed: on a node without it, the kubelet rejected our pod with AppArmor is not enabled on the host.
  • kube-bench 0.16.0 bundles CIS EKS 1.8.0, AKS 1.8 and GKE 1.9.0 checks, but its sample job files still name older benchmarks and the :latest image.

Verify it

  1. From outside your network, curl the API endpoint and confirm it is unreachable or refused; from an authorized network, kubectl get ns works only with a cloud-identity login.
  2. On EKS, run aws eks describe-cluster --name prod --query cluster.accessConfig and confirm API; on AKS, confirm az aks get-credentials --admin fails with Getting static credential isn't allowed.
  3. Apply a privileged pod to a restricted namespace and confirm the API server refuses it and names each violation.
  4. In a pod with hostUsers: false, cat /proc/self/uid_map must map 0 to a high host UID, not 0.
  5. From a pod, request the metadata service and confirm it times out or returns no node credentials.
  6. Run kube-bench against your service's current CIS benchmark and record every FAIL as fixed, accepted with a reason, or not applicable because the provider owns it.
Applies to
  • Kubernetes 1.37 (tested with kind 0.33.0, containerd 2.3.4, Linux 6.12)
  • Amazon EKS Standard and Auto Mode
  • Azure Kubernetes Service Standard and Automatic
  • Google Kubernetes Engine Standard and Autopilot
  • kube-bench 0.16.0

Amazon EKS, Azure Kubernetes Service and Google Kubernetes Engine on Kubernetes 1.35 to 1.37. Feature status and defaults change with each release; check the current documentation for your cluster version.

Part of
Learning path: Runtime and cloud defense (step 4 of 13). Workflow: Review a design
Learning goals and prerequisitesAbout 32 minutes: 9 to read and 23 for the exercise

What you will learn

  • Split responsibility for a managed cluster between you and the provider, by node mode.
  • Restrict API access and remove static administrator paths on EKS, AKS and GKE.
  • Enforce Pod Security restricted with seccomp, and add AppArmor and user namespaces where nodes support them.
  • Check a cluster against the CIS benchmark for its service and the NSA/CISA guidance.

Before you start

Platform engineers and cluster owners who run EKS, AKS or GKE, and the security engineers who review them

  • Read Kubernetes pod specs and RBAC objects.
  • Use your provider's CLI with a role that can view cluster configuration.

Refresh first: Run Kubernetes workloads with only the privileges they need · Review cloud permissions and role trust together

Go to the guided practice

Know what the provider secures and what you still own

All three providers run the control plane for you: the API server, etcd, the scheduler and their patching. What remains yours depends on how nodes are managed. With EKS managed node groups, AKS Standard and GKE Standard you still decide when nodes get new images. EKS Auto Mode, GKE Autopilot and AKS Automatic move node configuration and patching to the provider and lock some settings down. EKS on Fargate goes further: each pod gets its own kernel, but DaemonSets, and with them most runtime sensors, cannot run there.

In every mode you still own who can reach and call the API, RBAC, workload identity, pod privileges, network policy, admission policy, the images you run and your audit log retention. AWS sums it up for Auto Mode: customers keep responsibility for their application containers and for VPC and cluster configuration.

Know what the provider secures and what you still own
AreaEKSAKSGKE
Control plane and etcdAWSMicrosoft; etcd backed up every 30 minutesGoogle
Node OS patching, standard modeYou trigger node group or AMI updatesShared: weekly node images; you choose a channel or upgrade within 90 daysGoogle patches; you keep node pools on auto-upgrade
Node OS patching, managed modeAuto Mode: AWS replaces nodes within 21 days at mostAutomatic: NodeImage channel preconfiguredAutopilot: always auto-upgraded
Node accessAuto Mode blocks SSH and SSMChanges outside the AKS API are unsupportedAutopilot blocks SSH
Default pod hardeningNone beyond Kubernetes defaultsAutomatic enforces baseline Pod Security via deployment safeguardsAutopilot: no privileged pods, RuntimeDefault seccomp, CAP_NET_RAW dropped
RBAC, network policy, workload identity, imagesYouYouYou

Use the CIS and NSA/CISA baselines as checklists

The CIS Kubernetes Benchmark is written for clusters where you run the control plane. For managed clusters, use the provider benchmarks (CIS Amazon EKS, CIS AKS and CIS GKE), which mark control-plane items as the provider's responsibility. GKE's documentation says customers cannot evaluate its managed control plane against the benchmark at all. The NSA and CISA Kubernetes Hardening Guide, version 1.2 of August 2022, covers the same ground by theme: pod security, network separation, authentication and authorization, audit logging, and upgrades.

kube-bench runs the node, policy and managed-service checks as a Job. Many items cannot be checked automatically and come back as WARN for manual review, so treat its output as a work list, not a score. Its generic checks stop at CIS Kubernetes Benchmark 1.12, which covers Kubernetes 1.32 to 1.34; on managed clusters run the provider benchmark instead. The node and container images are yours to benchmark in every mode: /domains/runtime/containers/image-hardening-benchmarks/ scans them with OpenSCAP against CIS and STIG profiles and fails the build on deviations nobody approved.

run-kube-bench.shShell
# kube-bench 0.16.0 bundles CIS Kubernetes 1.12, EKS 1.8.0, AKS 1.8 and GKE 1.9.0 checks.
# The job files in its repository default to older benchmarks and the :latest image, so
# download one, pin the image and set the benchmark you report against.
curl -sSL https://raw.githubusercontent.com/aquasecurity/kube-bench/v0.16.0/job-eks.yaml   | sed -e 's|docker.io/aquasec/kube-bench:latest|docker.io/aquasec/kube-bench:v0.16.0@sha256:75506f222d1eb6ce2a751a5533bdc0a3b54c898e2e49e7751d0ee22cfb862679|'         -e 's|"eks-1.5.0"|"eks-1.8.0"|' > job-eks.yaml
kubectl apply -f job-eks.yaml
kubectl wait --for=condition=complete job/kube-bench --timeout=300s
kubectl logs job/kube-bench > kube-bench-eks-1.8.0.txt

Restrict who can reach and call the API

Treat the API endpoint as an administrative interface: private where your deployment tooling allows it, otherwise limited to known egress addresses. Authenticate people through the cloud identity provider, so leavers and MFA are handled where the rest of their access is, and remove every credential that bypasses it.

On EKS, switch to access entries. The change from CONFIG_MAP is one-way, so migrate the entries first. Access policy permissions don't appear in kubectl auth can-i --list; review them with aws eks list-associated-access-policies. On AKS, disabling local accounts stops az aks get-credentials --admin from issuing a static admin certificate; on an existing cluster, rotate the cluster certificates afterwards. On GKE, enable private nodes and authorized networks, or use the DNS-based endpoint with IAM.

Turn on control plane audit logs and keep them. EKS sends no control plane logs to CloudWatch until you enable each type; the runtime detection and response guide at /domains/runtime/detection/runtime-detection-response/ shows an audit policy and detections that use them.

cluster-access.shShell
# --- Amazon EKS ---------------------------------------------------------------
# Private endpoint only (or keep public with publicAccessCidrs set to your egress IPs).
aws eks update-cluster-config --name prod \
  --resources-vpc-config endpointPublicAccess=false,endpointPrivateAccess=true
# Access entries instead of the deprecated aws-auth ConfigMap (one-way change).
aws eks update-cluster-config --name prod --access-config authenticationMode=API
aws eks create-access-entry --cluster-name prod \
  --principal-arn arn:aws:iam::111122223333:role/platform-oncall --type STANDARD
aws eks associate-access-policy --cluster-name prod \
  --principal-arn arn:aws:iam::111122223333:role/platform-oncall \
  --access-scope type=namespace,namespaces=payments \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicy
# Control plane logs are off until you enable them.
aws eks update-cluster-config --name prod \
  --logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'

# --- Azure Kubernetes Service --------------------------------------------------
az aks update --resource-group rg-prod --name prod \
  --enable-aad --aad-admin-group-object-ids <entra-group-id> \
  --enable-azure-rbac --disable-local-accounts \
  --enable-oidc-issuer --enable-workload-identity \
  --api-server-authorized-ip-ranges 203.0.113.0/24 \
  --node-os-upgrade-channel NodeImage
# After disabling local accounts on an existing cluster, rotate its certificates.
az aks rotate-certs --resource-group rg-prod --name prod

# --- Google Kubernetes Engine (Standard) -----------------------------------------
gcloud container clusters create prod --location europe-west1 \
  --release-channel regular \
  --workload-pool PROJECT_ID.svc.id.goog \
  --enable-private-nodes \
  --enable-master-authorized-networks --master-authorized-networks 203.0.113.0/24 \
  --shielded-secure-boot \
  --enable-dataplane-v2

Give pods their own cloud identity and keep them off the node's

A pod that can read the node's credentials from the instance metadata service can act as the node: on EKS that is the node IAM role, on AKS the kubelet's managed identity, on GKE the node service account. Give each workload its own identity (EKS Pod Identity or IRSA, Microsoft Entra Workload ID, Workload Identity Federation for GKE), then remove the path to the node's.

On EKS, require IMDSv2 with a hop limit of 1 on node launch templates, so a token request from a pod's network namespace never gets an answer. GKE's metadata server, enabled by Workload Identity Federation for GKE and always on in Autopilot, hides node credentials from pods. On AKS every pod can reach IMDS by default; the IMDS restriction was still a preview on 11 October 2026 and conflicts with several add-ons. Where no platform control fits, a NetworkPolicy that excludes the metadata address from pod egress works with any CNI that enforces egress policy. It does not cover pods with hostNetwork: true, which Pod Security restricted forbids anyway.

Configurationegress-without-metadata.yamlYAML
# Pods in payments may resolve DNS and reach anything except the node's metadata service.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: egress-without-metadata
  namespace: payments
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - {protocol: UDP, port: 53}
        - {protocol: TCP, port: 53}
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except: [169.254.169.254/32]

Keep nodes current and isolate risky workloads

Every container on a node shares its kernel, so a kernel bug or a container escape reaches every pod on the node. Keep node images current: AKS publishes node images weekly and doesn't support images older than 90 days, and EKS Auto Mode replaces nodes within 21 days at most. Prefer a minimal container OS such as Bottlerocket, Container-Optimized OS or Azure Linux.

Run untrusted or multi-tenant code on separate nodes with taints and tolerations, or in a sandbox with its own kernel. GKE Sandbox runs pods under gVisor with runtimeClassName: gvisor, which Autopilot supports by default; on GKE 1.37.0-gke.4713000 or later the microvm runtime class runs pods in lightweight VMs instead, on nodes with nested virtualization. AKS Pod Sandboxing runs each pod in a Kata Containers VM with runtimeClassName: kata-vm-isolation, on Azure Linux node pools only. On EKS, Fargate gives each pod its own kernel. Microsoft's own guidance is that only a hypervisor boundary is safe for hostile multi-tenant workloads.

Enforce Pod Security restricted, seccomp, AppArmor and user namespaces

Pod Security Admission is built into every supported Kubernetes version. Label each workload namespace to enforce the restricted level and pin enforce-version, so a cluster upgrade cannot change what is allowed without you noticing. On our Kubernetes 1.37 test cluster, the namespace below rejected a pod that set privileged: true and hostPID: true, and the error listed all six violations: host namespaces, privileged, allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true and seccompProfile.

The pod below passes restricted and adds a user namespace. Running it on that cluster, /proc/self/uid_map read 0 2001731584 65536: UID 0 in the container was UID 2001731584 on the node. /proc/self/status showed CapEff and CapBnd of zero, NoNewPrivs 1 and Seccomp 2. User namespaces need Linux 6.3 or later on the node, containerd 2.0 or CRI-O 1.25 or later with runc 1.2 or crun 1.9 or later, and filesystems with idmap mount support. They cannot be combined with hostNetwork, hostPID, hostIPC or NFS volumes. AKS documents them for Kubernetes 1.33 and later on Azure Linux 3.0 or Ubuntu 24.04 nodes.

AppArmor has been GA since Kubernetes 1.31 and is set with the appArmorProfile field rather than annotations. It fails closed. Our node had no AppArmor, so the kubelet rejected the pod with Cannot enforce AppArmor: AppArmor is not enabled on the host. Set it only on node pools whose OS runs AppArmor. AKS documents it for Ubuntu and for Azure Linux 3.0 from the 7 November 2025 node image; EKS Auto Mode nodes use SELinux instead.

Configurationpayments-restricted.yamlYAML
apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.37
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.37
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.37
---
apiVersion: v1
kind: Pod
metadata:
  name: api
  namespace: payments
spec:
  hostUsers: false # user namespace: root in the pod is not root on the node
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 65532
    runAsGroup: 65532
    seccompProfile:
      type: RuntimeDefault
    # appArmorProfile:   # only on node pools whose OS runs AppArmor (see below)
    #   type: RuntimeDefault
  containers:
    - name: api
      image: ghcr.io/acme/api@sha256:<digest>
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      resources:
        limits:
          memory: 256Mi
          cpu: 500m

Enforce what Pod Security cannot at admission

Pod Security Standards have three fixed levels. They cannot require an approved registry, signed images, user namespaces or resource limits. ValidatingAdmissionPolicy (GA in 1.30) and MutatingAdmissionPolicy (GA in 1.36) run CEL in the API server with no webhook to keep alive. Kyverno and Gatekeeper add image verification and policy reports, and AKS and GKE offer managed versions: the Azure Policy add-on, which runs Gatekeeper, and GKE Policy Controller.

On our 1.37 cluster, the policy below rejected a pod without hostUsers: false in a labeled namespace, with the policy's message, and admitted the compliant pod. It matches pods, so a Deployment that violates it fails when its ReplicaSet creates pods. Add a matching rule for workload templates if you want the error at apply time. The container image hardening guide at /domains/runtime/containers/container-image-hardening/ shows Kyverno signature verification, and the container network and admission guide at /domains/runtime/containers/container-network-admission/ shows a policy for network boundaries.

Inventory the admission webhooks that add-ons install, and check who can reach them. The ingress-nginx case study (/domains/runtime/containers/case-study-ingress-nginx/) shows an unauthenticated webhook reachable from every pod, in a controller that could read every Secret, and the project's retirement in March 2026 means it will not be fixed again.

Configurationrequire-user-namespaces.yamlYAML
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-user-namespaces
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: "has(object.spec.hostUsers) && object.spec.hostUsers == false"
      message: "Pods in this namespace must set spec.hostUsers: false"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-user-namespaces
spec:
  policyName: require-user-namespaces
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        security.example.com/user-namespaces: required

Pitfalls

  • Assuming 'managed' means hardened. The provider runs the control plane; the defaults for endpoint exposure, local accounts and node access are yours to change.
  • Leaving the EKS cluster creator as an implicit administrator, or keeping aws-auth entries after moving to access entries.
  • Pinning Pod Security to latest, so an upgrade silently changes what is enforced.
  • Setting AppArmor or user namespaces cluster-wide without checking every node pool's OS and kernel.
  • Reading kube-bench output as a pass rate. WARN means you must check it by hand, and control-plane items on managed clusters are the provider's.
  • Blocking metadata with a NetworkPolicy while allowing hostNetwork pods, which bypass it.

Guided practice

You inherit the EKS cluster described below. Decide what the provider covers, what you must fix first and how you will prove each fix.

Supplied scenario material
Plain text
aws eks describe-cluster --name prod (excerpt)
  version: 1.36
  resourcesVpcConfig: endpointPublicAccess=true, publicAccessCidrs=[0.0.0.0/0], endpointPrivateAccess=false
  accessConfig: authenticationMode=CONFIG_MAP
  logging: all types disabled
Node groups
  ng-general: managed node group, AL2023 AMI from 4 months ago, launch template HttpTokens=optional, hop limit 2
  ng-batch:   self-managed, Ubuntu 24.04, runs customer-supplied scripts
Workloads
  payments namespace: no pod-security labels; 3 pods set privileged: true "for performance"
  All pods use the node role for S3 access; no Pod Identity or IRSA
kube-bench (eks-1.8.0): 9 FAIL, 31 WARN
  1. 1. Split responsibility

    Which items above does AWS own, and which are yours?

    Expected result and reasoning

    AWS owns the control plane and etcd. Everything listed is yours: endpoint exposure, authentication mode, logging, node AMI updates, IMDS settings, pod privileges and workload identity.

  2. 2. Order the fixes

    Pick the three changes that cut the most risk first, and say why.

    Expected result and reasoning

    Restrict the endpoint (private or known CIDRs) and move to access entries, removing implicit creator access. Require IMDSv2 with hop limit 1 after giving pods their own identity through Pod Identity, because every pod can currently use the node role. Enforce Pod Security restricted on payments after removing privileged: true. Then patch the AMIs and enable audit logs.

  3. 3. Isolate ng-batch

    How should customer-supplied scripts run?

    Expected result and reasoning

    On a dedicated, tainted node group with no access to other namespaces' secrets or identities, with user namespaces on Ubuntu 24.04, or in a separate VM boundary such as Fargate or a separate cluster if the code is hostile.

  4. 4. Prove it

    Name the evidence that closes each fix.

    Expected result and reasoning

    describe-cluster shows the private endpoint and API mode; a privileged pod is rejected with the restricted violations; a pod's metadata token request times out; the S3 calls in CloudTrail show the Pod Identity role; kube-bench FAILs are fixed or recorded with reasons.

  5. 5. Plan the rollout

    How do you avoid an outage when enforcing Pod Security on payments?

    Expected result and reasoning

    Apply warn and audit labels first, fix the pods the warnings name, then add enforce with a pinned version, using a canary namespace or a dry run with kubectl label --dry-run=server.

Your deliverable: A prioritized hardening plan for the cluster that marks each item as yours or the provider's, gives the command or manifest for each fix and the evidence that proves it worked.

The cluster description is supplied. The Pod Security, user namespace, AppArmor and admission behavior on this page was tested on a local Kubernetes 1.37 cluster; provider CLI commands were checked against current documentation, not run.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. Amazon EKS best practices: security and the shared responsibility model
  2. Amazon EKS Auto Mode: shared responsibility
  3. Amazon EKS: grant IAM users and roles access to Kubernetes APIs
  4. AKS support policies and shared responsibility
  5. AKS: manage local accounts with Microsoft Entra integration
  6. AKS: secure container access (user namespaces, AppArmor, seccomp)
  7. GKE shared responsibility model
  8. GKE Autopilot security measures
  9. NSA and CISA Kubernetes Hardening Guide 1.2. Version 1.2 (August 2022).
  10. Kubernetes: user namespaces. Kubernetes v1.37.
  11. Kubernetes v1.36: user namespaces are GA. 23 April 2026.
  12. Kubernetes: Pod Security Admission. Kubernetes v1.37.
  13. Kubernetes: restrict a container's access to resources with AppArmor. Kubernetes v1.37.
  14. kube-bench v0.16.0: benchmark versions by platform (cfg/config.yaml)

Reference information

Download this guide as Markdown