AWS EKS Security: Pod Security, RBAC and Network Policies (2026)

EKS Security

Amazon Elastic Kubernetes Service (EKS) gives you a managed Kubernetes control plane, but securing the workloads running on top of it is entirely your responsibility. A misconfigured EKS cluster can expose secrets, allow privilege escalation, enable lateral movement between pods, or even give an attacker full control of your AWS account. In 2026, with the Kubernetes threat landscape more sophisticated than ever, defense-in-depth is not optional — it is the baseline.

This guide walks you through every layer of EKS security: from Pod Security Standards that restrict what containers can do, to RBAC that controls who can do what in the API server, to network policies that enforce zero-trust between pods, to IRSA that eliminates long-lived credentials, to runtime detection with Falco that catches exploits in real time. Each section includes production-ready YAML, CLI commands, and context on why the control matters. By the end you will have a hardening checklist you can apply to any EKS cluster today.

Table of Contents

  1. EKS Security Layers Overview
  2. Pod Security Standards
  3. RBAC: Roles, ClusterRoles, and RoleBindings
  4. IAM Roles for Service Accounts (IRSA)
  5. Network Policies
  6. Secrets Encryption with KMS
  7. EKS Private Endpoint
  8. Runtime Security with Falco
  9. Image Security: ECR Scanning and OPA Gatekeeper
  10. EKS Audit Logging and GuardDuty Threat Detection
  11. CIS EKS Benchmark Checklist

1. EKS Security Layers Overview

EKS security is a shared responsibility model layered on top of the Kubernetes security model. AWS secures the control plane — the etcd cluster, the API server HA setup, the underlying EC2 infrastructure. You secure everything above: the worker nodes, the container images, the Kubernetes API authorization model, the network traffic between pods, and the runtime behaviour of containers themselves.

Think of EKS security in five concentric rings:

  • Infrastructure layer — node OS hardening, managed node group patching, VPC isolation, private endpoints, security groups on nodes.
  • API server layer — authentication via IAM + aws-auth ConfigMap, authorization via RBAC, admission controllers (Pod Security Admission, OPA Gatekeeper).
  • Workload layer — Pod Security Standards, securityContext constraints, resource limits, non-root containers, read-only filesystems.
  • Network layer — Network Policies enforced by a CNI plugin (Calico, Cilium, or AWS VPC CNI + network policy support), security groups for pods.
  • Data layer — KMS envelope encryption for Kubernetes Secrets at rest, IRSA for fine-grained IAM, Secrets Manager integration, TLS everywhere in transit.

Each layer must be hardened independently. A vulnerability or misconfiguration in any one layer can undermine the others. For example, even perfect RBAC configuration is worthless if a pod runs as root and can escape the container to compromise the node, from which it can steal the node's IAM role credentials and call the AWS API directly. The sections below address each layer in turn.

Key principle: Apply the principle of least privilege at every layer — Kubernetes RBAC permissions, IAM policies attached via IRSA, network policy rules, and Linux capabilities inside containers should all grant only the minimum access required.

2. Pod Security Standards

Pod Security Standards (PSS) replaced Pod Security Policies (PSP) starting in Kubernetes 1.25. They define three security profiles — privileged, baseline, and restricted — and are enforced by the built-in Pod Security Admission controller. You apply them at the namespace level using labels, so you can have different security postures for different teams or workloads within the same cluster.

The three profiles are:

  • Privileged — no restrictions. Suitable only for system-level workloads like CNI plugins or node agents that genuinely need host access.
  • Baseline — prevents known privilege escalation vectors: no hostPID, no hostNetwork, no privileged containers, no hostPath volumes to sensitive directories, restricted capabilities (no NET_ADMIN, SYS_ADMIN, etc.).
  • Restricted — the hardest profile. Enforces all baseline rules plus: must run as non-root, must set seccompProfile, must drop ALL capabilities and only add back specific ones, volume types restricted to configMap/secret/emptyDir/projected/downwardAPI/persistentVolumeClaim.

Each profile can be applied in three enforcement modes: enforce (blocks non-compliant pods), audit (logs violations but allows the pod), and warn (returns a warning to the API client). It is best practice to start with audit and warn modes to find non-compliant workloads, then switch to enforce after remediation.

Namespace Label Configuration

Apply PSS to a namespace by adding labels. The label key format is pod-security.kubernetes.io/<mode> with the value being the profile name.

# Apply restricted enforcement to a production namespace
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.29
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.29
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.29
# Apply baseline enforcement to a staging namespace (more permissive)
apiVersion: v1
kind: Namespace
metadata:
  name: staging
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.29
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.29
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.29

Compliant Pod Spec for Restricted Profile

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: production
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: my-app:1.2.3
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
        - ALL
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "500m"
        memory: "256Mi"
    volumeMounts:
    - name: tmp
      mountPath: /tmp
  volumes:
  - name: tmp
    emptyDir: {}
Tip: Use kubectl label --dry-run=server with the warn label first to discover which workloads in an existing namespace would be blocked before applying enforce.

3. RBAC: Roles, ClusterRoles, and RoleBindings

Kubernetes Role-Based Access Control (RBAC) is the primary authorization mechanism for the Kubernetes API. In EKS, all human users and CI/CD systems authenticate via IAM (mapped through the aws-auth ConfigMap or EKS access entries) and are then authorized by RBAC rules. Every action on the Kubernetes API — reading pod logs, creating deployments, deleting secrets — is an RBAC decision.

RBAC has four key object types: Role (namespace-scoped permissions), ClusterRole (cluster-wide permissions), RoleBinding (assigns a Role or ClusterRole to subjects within a namespace), and ClusterRoleBinding (assigns a ClusterRole cluster-wide). The golden rule is to use namespace-scoped Role and RoleBinding wherever possible, reserving ClusterRole for cross-namespace operations that genuinely need cluster-wide access.

Developer Role — Namespace Scoped

This role gives a developer team read/write access to workload resources in their namespace but denies access to secrets and RBAC resources themselves.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: team-alpha
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log", "pods/exec", "services", "configmaps", "endpoints"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["autoscaling"]
  resources: ["horizontalpodautoscalers"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["batch"]
  resources: ["jobs", "cronjobs"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-binding
  namespace: team-alpha
subjects:
- kind: Group
  name: team-alpha-developers   # Maps to IAM group via aws-auth
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: developer
  apiGroup: rbac.authorization.k8s.io

Read-Only ClusterRole

A read-only cluster role is useful for monitoring tools, auditors, and on-call engineers who need visibility but must not be able to modify any resources.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-readonly
rules:
- apiGroups: [""]
  resources:
  - nodes
  - namespaces
  - pods
  - pods/log
  - services
  - endpoints
  - configmaps
  - persistentvolumes
  - persistentvolumeclaims
  - events
  - resourcequotas
  - limitranges
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["autoscaling"]
  resources: ["horizontalpodautoscalers"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
  resources: ["jobs", "cronjobs"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
  resources: ["ingresses", "networkpolicies"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
  resources: ["nodes", "pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-readonly-binding
subjects:
- kind: Group
  name: ops-readonly
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-readonly
  apiGroup: rbac.authorization.k8s.io

Mapping IAM to Kubernetes via EKS Access Entries

# Using the newer EKS Access Entries API (recommended over aws-auth ConfigMap)
aws eks create-access-entry \
  --cluster-name my-cluster \
  --principal-arn arn:aws:iam::123456789012:role/DeveloperRole \
  --kubernetes-groups team-alpha-developers

# Associate a policy with the access entry
aws eks associate-access-policy \
  --cluster-name my-cluster \
  --principal-arn arn:aws:iam::123456789012:role/DeveloperRole \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy \
  --access-scope type=namespace,namespaces=team-alpha
Warning: Never grant cluster-admin to IAM users or roles used by applications. Reserve cluster-admin for the cluster bootstrap role only. Audit ClusterRoleBindings regularly with kubectl get clusterrolebindings -o wide.

4. IAM Roles for Service Accounts (IRSA)

Before IRSA, the only way to give a pod access to AWS services was to attach an IAM role to the EC2 node. This meant every pod on that node shared the same AWS permissions — a massive over-privilege problem. IRSA solves this by binding an IAM role directly to a Kubernetes ServiceAccount using OIDC federation. A pod that references the annotated ServiceAccount receives a short-lived web identity token that it exchanges for temporary AWS credentials scoped only to that pod's IAM role. No static credentials, no node-level sharing, full auditability in CloudTrail.

Step 1: Enable OIDC Provider for Your Cluster

# Get the OIDC issuer URL
aws eks describe-cluster --name my-cluster \
  --query "cluster.identity.oidc.issuer" --output text

# Associate the OIDC provider (do this once per cluster)
eksctl utils associate-iam-oidc-provider \
  --cluster my-cluster \
  --approve

Step 2: Create the IAM Role with Trust Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:production:s3-reader",
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}
# Create the role and attach a policy
aws iam create-role \
  --role-name eks-s3-reader-role \
  --assume-role-policy-document file://trust-policy.json

aws iam attach-role-policy \
  --role-name eks-s3-reader-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

Step 3: Annotate the Kubernetes ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: s3-reader
  namespace: production
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-s3-reader-role
    # Optional: set token expiry (default 86400s = 24h, minimum 3600s)
    eks.amazonaws.com/token-expiration: "3600"

Step 4: Reference the ServiceAccount in Pod Spec

apiVersion: apps/v1
kind: Deployment
metadata:
  name: s3-reader-app
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: s3-reader-app
  template:
    metadata:
      labels:
        app: s3-reader-app
    spec:
      serviceAccountName: s3-reader     # binds to annotated SA
      automountServiceAccountToken: true # required for IRSA token injection
      containers:
      - name: app
        image: my-app:1.0.0
        env:
        - name: AWS_DEFAULT_REGION
          value: us-east-1

Using eksctl for IRSA Setup

# eksctl can create the IAM role and annotate the SA in one command
eksctl create iamserviceaccount \
  --cluster my-cluster \
  --namespace production \
  --name s3-reader \
  --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
  --override-existing-serviceaccounts \
  --approve
Best practice: Always scope the trust policy Condition to the exact namespace and ServiceAccount name. A wildcard (system:serviceaccount:*:*) defeats the purpose of IRSA and allows any pod in the cluster to assume the role.

5. Network Policies

By default, Kubernetes has no network segmentation between pods. Any pod can reach any other pod in the cluster over the flat pod network, regardless of namespace. Network Policies are Kubernetes objects that act as a namespace-scoped firewall, controlling ingress and egress at the pod level using label selectors. To enforce Network Policies in EKS you need a CNI plugin that supports them — the default AWS VPC CNI added network policy support in 2023 (via a separate network policy controller), or you can use Calico or Cilium.

Default Deny-All Ingress Policy

Apply a default deny-all ingress policy to every namespace as the baseline. This ensures no traffic reaches any pod unless explicitly allowed by a subsequent NetworkPolicy.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}          # selects ALL pods in the namespace
  policyTypes:
  - Ingress                # only controls ingress; egress still open
# Apply a default deny-all policy (both ingress AND egress) for maximum isolation
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Allow Specific Namespace Ingress

After applying the default deny, create explicit allow rules. This policy allows ingress to the backend pods in the production namespace only from pods labelled app: frontend in the same namespace, and from the monitoring namespace (for Prometheus scraping).

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
      namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: production
    ports:
    - protocol: TCP
      port: 8080
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
    ports:
    - protocol: TCP
      port: 9090   # Prometheus metrics endpoint

Allow DNS Egress

When you apply a default deny-all egress policy, pods lose DNS resolution. You must explicitly allow egress to kube-dns.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
Note: NetworkPolicy rules are additive — if multiple policies select the same pod, the union of all their rules applies. There is no "deny" verb inside a NetworkPolicy rule; isolation is achieved by applying the default-deny policy and then only adding explicit allows.

6. Secrets Encryption with KMS

By default, Kubernetes Secrets in EKS are stored in etcd encrypted at rest using AES-256 with AWS-managed keys — but the encryption is at the etcd volume level, meaning anyone with API access to the Kubernetes secrets endpoint can retrieve them in plaintext. Envelope encryption using AWS KMS adds a second layer: each Secret is encrypted with a unique data encryption key (DEK), and the DEK itself is encrypted (wrapped) by your KMS Customer Managed Key (CMK). Even if someone exfiltrates the etcd data, they cannot decrypt it without access to the KMS key. This is a CIS EKS Benchmark requirement.

Enable Envelope Encryption at Cluster Creation

# Create a KMS key for EKS secrets encryption
aws kms create-key \
  --description "EKS secrets encryption key for my-cluster" \
  --key-usage ENCRYPT_DECRYPT \
  --key-spec SYMMETRIC_DEFAULT

# Note the KeyArn from the output, then create the cluster with encryption
eksctl create cluster \
  --name my-cluster \
  --region us-east-1 \
  --version 1.29 \
  --node-type m5.large \
  --nodes 3 \
  --encrypt-secrets-kms-key-arn arn:aws:kms:us-east-1:123456789012:key/mrk-abc123def456

Enable Encryption on an Existing Cluster

aws eks associate-encryption-config \
  --cluster-name my-cluster \
  --encryption-config '[{"resources":["secrets"],"provider":{"keyArn":"arn:aws:kms:us-east-1:123456789012:key/mrk-abc123def456"}}]'

# Monitor the update status
aws eks describe-update \
  --cluster-name my-cluster \
  --update-id <update-id-from-above-output>

Required KMS Key Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/AWSServiceRoleForAmazonEKS"
      },
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "*"
    }
  ]
}
Important: After enabling KMS encryption, you must re-encrypt all existing Secrets by running kubectl get secrets --all-namespaces -o json | kubectl replace -f -. New secrets will be encrypted automatically, but existing ones are not re-encrypted until touched.

7. EKS Private Endpoint

By default, EKS creates a public API server endpoint accessible from the internet. While it requires valid IAM credentials and RBAC authorization, exposing the Kubernetes API to the public internet is an unnecessary attack surface. Brute-force, credential stuffing, and vulnerability exploits against the API server all become easier when it is publicly reachable. The EKS private endpoint configuration controls whether the API server is reachable from the internet, from within the VPC, or both.

Endpoint Configuration Options

  • Public only — default, API accessible from internet (not recommended for production).
  • Public + Private — API accessible from internet AND from within the VPC. Node-to-API traffic stays in the VPC.
  • Private only — API accessible only from within the VPC or via VPN/Direct Connect. Most secure.
# Enable private endpoint and disable public endpoint
aws eks update-cluster-config \
  --name my-cluster \
  --resources-vpc-config \
    endpointPublicAccess=false,\
    endpointPrivateAccess=true
# If you must keep public access, restrict to specific CIDRs (your office/VPN IPs)
aws eks update-cluster-config \
  --name my-cluster \
  --resources-vpc-config \
    endpointPublicAccess=true,\
    endpointPrivateAccess=true,\
    publicAccessCidrs="203.0.113.0/24,198.51.100.0/24"

Security Group Rules for Private Endpoint

When using private endpoint only, your bastion host, CI/CD runners, and administrative workstations must be inside the VPC or connected via VPN. The cluster security group automatically allows traffic from nodes; you need to add rules for administrative access.

# Allow kubectl access from a bastion host security group
aws ec2 authorize-security-group-ingress \
  --group-id sg-cluster-security-group-id \
  --protocol tcp \
  --port 443 \
  --source-group sg-bastion-security-group-id

# Allow kubectl access from a specific CIDR within the VPC
aws ec2 authorize-security-group-ingress \
  --group-id sg-cluster-security-group-id \
  --protocol tcp \
  --port 443 \
  --cidr 10.0.0.0/8
Note: With private endpoint only, kubectl commands must be run from inside the VPC. Set up AWS Systems Manager Session Manager on a bastion instance, or use AWS Client VPN, to enable secure access without opening inbound SSH ports.

8. Runtime Security with Falco

All the preventive controls above (PSS, RBAC, network policies) reduce attack surface, but they cannot stop a zero-day vulnerability or a compromised container. Runtime security detects attacks as they happen by monitoring system calls made by running containers. Falco, a CNCF graduated project, uses eBPF (or kernel modules as fallback) to trace every syscall in real time and compares them against a rule engine. When a container opens a shell, reads /etc/shadow, spawns a network scanner, or drops a setuid binary, Falco fires an alert within milliseconds.

Install Falco on EKS with Helm

# Add the Falco Helm repository
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update

# Install Falco with eBPF driver (recommended for EKS managed nodes)
helm install falco falcosecurity/falco \
  --namespace falco \
  --create-namespace \
  --set driver.kind=ebpf \
  --set falcosidekick.enabled=true \
  --set falcosidekick.config.slack.webhookurl="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" \
  --set falcosidekick.config.slack.minimumpriority=warning \
  --set collectors.kubernetes.enabled=true \
  --set tty=true
# Verify Falco is running
kubectl get pods -n falco
kubectl logs -n falco -l app.kubernetes.io/name=falco --tail=50

Sample Custom Falco Rules

Falco comes with hundreds of built-in rules. You can add custom rules via a ConfigMap or Helm values. Below are high-value rules for an EKS environment:

- rule: Shell Spawned in Container
  desc: A shell was spawned inside a running container. This may indicate an attacker
        establishing interactive access or a misconfigured entrypoint.
  condition: >
    spawned_process and
    container and
    shell_procs and
    not container.image.repository in (allowed_shell_containers)
  output: >
    Shell spawned in container
    (user=%user.name user_loginuid=%user.loginuid
     container_id=%container.id container_name=%container.name
     image=%container.image.repository:%container.image.tag
     shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)
  priority: WARNING
  tags: [container, shell, mitre_execution]

- rule: Write to Sensitive Directory
  desc: A process attempted to write to /etc, /bin, /sbin, /usr/bin, or /usr/sbin
        inside a container. Legitimate apps should use writable volumes for runtime data.
  condition: >
    open_write and
    container and
    (fd.name startswith /etc/ or
     fd.name startswith /bin/ or
     fd.name startswith /sbin/ or
     fd.name startswith /usr/bin/ or
     fd.name startswith /usr/sbin/)
  output: >
    Write to sensitive directory
    (user=%user.name file=%fd.name container=%container.name
     image=%container.image.repository)
  priority: ERROR
  tags: [container, filesystem, mitre_persistence]

- rule: EKS Metadata Server Access from Container
  desc: A container attempted to access the EC2 instance metadata service (IMDS).
        This may indicate credential theft via SSRF or a compromised container.
  condition: >
    outbound and
    container and
    fd.sip = "169.254.169.254"
  output: >
    EKS metadata server access from container
    (user=%user.name container=%container.name image=%container.image.repository
     connection=%fd.name)
  priority: CRITICAL
  tags: [container, network, aws, credential_access]
Integration tip: Route Falco alerts to Amazon EventBridge via Falcosidekick, then trigger a Lambda function that automatically cordons the affected node and creates a PagerDuty incident. This gives you automated isolation in under 60 seconds from alert to containment.

9. Image Security: ECR Scanning and OPA Gatekeeper

Container image security has two distinct problems: known vulnerabilities in image layers (addressed by scanning), and policy enforcement at admission time to prevent unsafe images from being deployed (addressed by admission controllers like OPA Gatekeeper). Both must be in place — scanning without enforcement means developers can ignore scan results, and enforcement without scanning means you are blocking on metadata rather than actual security posture.

ECR Enhanced Scanning with Inspector

# Enable Enhanced Scanning (Inspector v2) for the ECR registry
aws ecr put-registry-scanning-configuration \
  --scan-type ENHANCED \
  --rules '[{
    "repositoryFilters": [{
      "filter": "*",
      "filterType": "WILDCARD"
    }],
    "scanFrequency": "CONTINUOUS_SCAN"
  }]'

# Check scan findings for a specific image
aws ecr describe-image-scan-findings \
  --repository-name my-app \
  --image-id imageTag=1.2.3 \
  --query 'imageScanFindings.findings[?severity==`CRITICAL`]'

Block :latest Tag with OPA Gatekeeper

Using :latest tag is dangerous because it makes deployments non-deterministic and bypasses vulnerability scanning (you never know which image version is actually running). OPA Gatekeeper enforces this at the API server admission stage, blocking any Pod that references an image with no tag or the :latest tag.

# First install Gatekeeper
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper/gatekeeper \
  --name-template=gatekeeper \
  --namespace gatekeeper-system \
  --create-namespace \
  --set replicas=2
# ConstraintTemplate: defines the policy logic in Rego
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sblocklatestimage
spec:
  crd:
    spec:
      names:
        kind: K8sBlockLatestImage
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package k8sblocklatestimage

      violation[{"msg": msg}] {
        container := input.review.object.spec.containers[_]
        endswith(container.image, ":latest")
        msg := sprintf("Container '%v' uses ':latest' tag. Pin to a specific immutable digest or version tag.", [container.name])
      }

      violation[{"msg": msg}] {
        container := input.review.object.spec.containers[_]
        not contains(container.image, ":")
        msg := sprintf("Container '%v' has no tag. All images must specify an explicit version tag.", [container.name])
      }

      violation[{"msg": msg}] {
        container := input.review.object.spec.initContainers[_]
        endswith(container.image, ":latest")
        msg := sprintf("Init container '%v' uses ':latest' tag.", [container.name])
      }
# Constraint: activates the ConstraintTemplate for all namespaces
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockLatestImage
metadata:
  name: block-latest-image
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
    - apiGroups: ["apps"]
      kinds: ["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"]
  enforcementAction: deny    # use "warn" to start, then switch to "deny"
Best practice: Reference images by digest (my-app@sha256:abc123...) rather than a version tag in production. Digests are immutable — a tag can be overwritten, but a digest always refers to the exact same image layers.

10. EKS Audit Logging and GuardDuty EKS Threat Detection

The Kubernetes API server emits detailed audit logs recording every request — who made it, what resource was targeted, what the response was, and the source IP. In EKS, control plane audit logs are sent to Amazon CloudWatch Logs. These logs are indispensable for incident response, compliance (PCI-DSS, SOC2, ISO27001 all require audit trails), and threat hunting. Amazon GuardDuty extends this with machine-learning-based anomaly detection specifically for EKS, analyzing audit logs and runtime activity to surface threats like privilege escalation attempts, cryptomining, and lateral movement.

Enable EKS Control Plane Logging

# Enable all log types (api, audit, authenticator, controllerManager, scheduler)
aws eks update-cluster-config \
  --name my-cluster \
  --logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'

# Confirm the log group in CloudWatch Logs
aws logs describe-log-groups \
  --log-group-name-prefix /aws/eks/my-cluster/cluster

Query Audit Logs with CloudWatch Insights

# Find all privileged container creation attempts in the last 24 hours
fields @timestamp, @message
| filter @logStream like /kube-apiserver-audit/
| filter ispresent(requestURI)
| filter requestURI like /pods/
| filter verb = "create"
| filter @message like /privileged.*true/
| sort @timestamp desc
| limit 100

# Detect anonymous API calls (potential misconfigured admission or attack)
fields @timestamp, user.username, sourceIPs.0, requestURI, verb
| filter @logStream like /kube-apiserver-audit/
| filter user.username = "system:anonymous"
| sort @timestamp desc
| limit 50

Enable GuardDuty EKS Protection

# Enable GuardDuty in the region if not already enabled
aws guardduty create-detector --enable --finding-publishing-frequency FIFTEEN_MINUTES

# Enable EKS Audit Log monitoring
DETECTOR_ID=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)

aws guardduty update-detector \
  --detector-id $DETECTOR_ID \
  --features '[{"Name":"EKS_AUDIT_LOGS","Status":"ENABLED"}]'

# Enable EKS Runtime Monitoring (agent-based, deeper visibility)
aws guardduty update-detector \
  --detector-id $DETECTOR_ID \
  --features '[{
    "Name": "EKS_RUNTIME_MONITORING",
    "Status": "ENABLED",
    "AdditionalConfiguration": [{
      "Name": "EKS_ADDON_MANAGEMENT",
      "Status": "ENABLED"
    }]
  }]'

Key GuardDuty EKS Finding Types

  • Execution:Kubernetes/ExecInKubeSystemPod — exec into a kube-system pod, likely attacker probing.
  • PrivilegeEscalation:Kubernetes/PrivilegedContainer — privileged container created.
  • Persistence:Kubernetes/ContainerWithSensitiveMount — container mounting sensitive host paths.
  • Discovery:Kubernetes/KubernetesAPICalledFromProxyIP — API calls proxied through anonymizing services.
  • CryptoCurrency:Kubernetes/MaliciousIPCaller — pod communicating with known cryptomining pools.
  • Impact:Kubernetes/SuccessfulAnonymousAccess — anonymous user successfully called the API.
Automation: Create an EventBridge rule that forwards GuardDuty findings with severity >= 7.0 to an SNS topic for immediate PagerDuty alerting, and a Lambda function that automatically quarantines the affected pod by applying a restrictive NetworkPolicy.

11. CIS EKS Benchmark Checklist

The Center for Internet Security (CIS) publishes a benchmark for Amazon EKS that defines configuration standards derived from consensus among security practitioners. Running kube-bench against your cluster produces a scored report. Below are the most critical items from the CIS EKS Benchmark 1.4.0, organized by category. Treat this as a hardening checklist to verify after initial cluster setup and after any major configuration change.

Control Plane Configuration

  • Enable all EKS control plane audit log types (API, audit, authenticator, controller manager, scheduler).
  • Enable envelope encryption for Kubernetes Secrets using a KMS CMK.
  • Disable public API server endpoint; use private endpoint with VPN or AWS Client VPN for access.
  • Restrict public endpoint CIDRs to known IPs if public access cannot be disabled.

IAM and Authentication

  • Do not use the cluster creator's IAM user for day-to-day operations — create dedicated roles.
  • Use EKS Access Entries or a tightly controlled aws-auth ConfigMap — audit regularly for stale entries.
  • Use IRSA for all workloads that need AWS API access — never attach over-permissive roles to node groups.
  • Enable MFA for all IAM users with EKS cluster access.
  • Avoid using system:masters group — create purpose-specific ClusterRoles instead.

RBAC

  • Ensure no wildcard verbs or resources in ClusterRoles except for cluster-admin.
  • Ensure cluster-admin ClusterRoleBinding has no subjects other than necessary system components.
  • Do not use default ServiceAccounts for workloads — create dedicated ServiceAccounts per application.
  • Set automountServiceAccountToken: false on ServiceAccounts that do not need API access.

Pod and Container Security

  • Apply Pod Security Standards (restricted or baseline) to all non-system namespaces.
  • Ensure containers do not run as root (runAsNonRoot: true in securityContext).
  • Ensure containers have CPU and memory limits defined.
  • Ensure containers use read-only root filesystems where possible.
  • Drop all Linux capabilities and add back only required ones.
  • Disable privilege escalation (allowPrivilegeEscalation: false).

Network Security

  • Apply default-deny NetworkPolicy to all application namespaces.
  • Use Security Groups for Pods to apply AWS-level network controls to individual pods.
  • Ensure node security groups block unnecessary inter-node traffic.
  • Enable EKS control plane to node communication via private subnets only.

Node Security

  • Use EKS-managed node groups with automatic AMI updates to receive OS patches promptly.
  • Block IMDS v1 on nodes — enforce IMDSv2 with a hop limit of 1 (blocks pods from using node credentials).
  • Do not schedule non-system workloads on nodes with privileged IAM roles.
  • Enable Amazon Inspector for continuous vulnerability scanning of node AMIs.

Runtime and Monitoring

  • Deploy Falco or a commercial runtime security tool to detect anomalous system calls.
  • Enable GuardDuty EKS Audit Log Monitoring and Runtime Monitoring on all clusters.
  • Integrate GuardDuty findings with a SIEM and configure automated response playbooks.
  • Use ECR Enhanced Scanning with continuous mode for all repositories.
  • Enforce image tag immutability in ECR and block :latest tags via OPA Gatekeeper.
# Run kube-bench to check your cluster against the CIS benchmark
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-eks-cis.yaml
kubectl logs job/kube-bench

# Or run it locally using the kube-bench binary
kube-bench --benchmark eks-cis-1.4 --json > kube-bench-report.json

Summary

Securing an EKS cluster requires a layered defense strategy that spans every level of the stack. Start with the foundational controls — enable KMS secrets encryption, disable the public API endpoint, and configure IRSA to eliminate static credentials. Then harden workloads with Pod Security Standards and RBAC roles scoped to the minimum required permissions. Add network segmentation with default-deny NetworkPolicies, and enforce image hygiene with ECR scanning and OPA Gatekeeper. Finally, close the detection gap with Falco runtime monitoring and GuardDuty EKS threat detection so that any breach is detected in real time rather than discovered weeks later during an audit.

No cluster is perfectly secure, but systematically working through the CIS EKS Benchmark checklist and addressing each control puts you in a strong position against the vast majority of real-world attack scenarios. Run kube-bench regularly, integrate its findings into your security posture dashboard, and treat security hardening as an ongoing practice rather than a one-time configuration event.

Read Next