← Back to DevBytes

EKS Security: IAM Policies and Network Security

Understanding EKS Security: IAM Policies and Network Security

Amazon Elastic Kubernetes Service (EKS) abstracts much of the control plane management, but security remains a shared responsibility. Two foundational pillars of EKS security are IAM Policies—governing who can do what—and Network Security—governing how traffic flows. Misconfigurations in either can expose your cluster to privilege escalation, data exfiltration, or unauthorized access. This tutorial walks you through both domains with practical, production-ready examples.

Part 1: IAM Policies for EKS

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

What Are IAM Policies in the Context of EKS?

IAM policies in EKS operate at two distinct layers. The first is the AWS IAM layer, which controls actions against the EKS service itself, its associated infrastructure (EC2, Load Balancers, subnets), and the Kubernetes authentication path via aws-auth ConfigMap or IAM Roles for Service Accounts (IRSA). The second is the Kubernetes RBAC layer, which controls in-cluster authorization once a user or service has been authenticated. These layers work together: IAM handles "can you reach the cluster," and RBAC handles "what can you do inside it."

Why IAM Policies Matter for EKS Security

Without properly scoped IAM policies, an attacker who compromises a pod could assume excessive AWS permissions via IRSA, delete cluster resources, or escalate privileges through the aws-auth ConfigMap. Conversely, overly restrictive policies can break legitimate workloads. The goal is least privilege: grant only what a user, role, or service needs, nothing more.

How to Implement IAM Policies for EKS

1. Controlling Access to the EKS Management Plane

Use IAM identity-based policies to govern who can call EKS APIs like eks:DescribeCluster, eks:UpdateClusterConfig, or eks:DeleteCluster. Below is an example policy that allows read-only access to EKS clusters while restricting destructive actions.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EKSReadOnly",
      "Effect": "Allow",
      "Action": [
        "eks:DescribeCluster",
        "eks:ListClusters",
        "eks:DescribeAddon",
        "eks:ListAddons",
        "eks:DescribeUpdate",
        "eks:DescribeFargateProfile",
        "eks:ListFargateProfiles"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyDestructiveActions",
      "Effect": "Deny",
      "Action": [
        "eks:DeleteCluster",
        "eks:DeleteAddon",
        "eks:DeleteFargateProfile",
        "eks:UpdateClusterConfig",
        "eks:UpdateAddon"
      ],
      "Resource": "*"
    }
  ]
}

Attach this policy to a developer or operations IAM user or role. The explicit deny ensures that even if a broader policy accidentally grants delete access, the destructive actions remain blocked.

2. Mapping IAM Principals to Kubernetes RBAC via aws-auth

By default, only the IAM user or role that created the EKS cluster has system:masters access inside Kubernetes. To grant additional IAM users or roles access to the cluster, you must add them to the aws-auth ConfigMap in the kube-system namespace. Below is a complete ConfigMap that maps an IAM role to a Kubernetes group, then binds that group to a ClusterRole via RBAC.

First, update the aws-auth ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - roleARN: arn:aws:iam::123456789012:role/EKSDeveloperRole
      username: developer-role
      groups:
        - eks-developers
    - roleARN: arn:aws:iam::123456789012:role/EKSAdminRole
      username: admin-role
      groups:
        - system:masters
  mapUsers: |
    - userARN: arn:aws:iam::123456789012:user/alice
      username: alice
      groups:
        - eks-viewers

Apply it:

kubectl apply -f aws-auth-configmap.yaml

Next, create a Kubernetes ClusterRole that grants read-only access to pods, deployments, and services:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: eks-developer-readonly
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log", "services", "endpoints", "configmaps"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get", "list", "watch"]

Bind the ClusterRole to the eks-developers group:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: eks-developers-binding
subjects:
- kind: Group
  name: eks-developers
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: eks-developer-readonly
  apiGroup: rbac.authorization.k8s.io

Now any IAM user or role in the eks-developers group has the read-only permissions defined above when they authenticate to the cluster.

3. IAM Roles for Service Accounts (IRSA)

IRSA is the preferred method for granting AWS permissions to pods. Instead of attaching IAM policies directly to EC2 worker nodes (which grants every pod on that node the same permissions), IRSA associates an IAM role with a Kubernetes service account. Only pods using that service account receive the corresponding AWS credentials.

Step-by-step implementation:

Step 1: Create an IAM OIDC identity provider for your cluster (if not already created):

eksctl utils associate-iam-oidc-provider --cluster my-cluster --region us-east-1 --approve

Step 2: Create an IAM policy. Here, a policy allowing read-only access to an S3 bucket named my-app-bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket",
        "s3:GetObjectVersion"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-bucket",
        "arn:aws:s3:::my-app-bucket/*"
      ]
    }
  ]
}

Save this as s3-readonly-policy.json and create it:

aws iam create-policy \
  --policy-name S3ReadOnlyMyAppBucket \
  --policy-document file://s3-readonly-policy.json

Step 3: Create an IAM role that the service account can assume. Use eksctl or the AWS CLI. With eksctl, the entire flow is streamlined:

eksctl create iamserviceaccount \
  --name my-app-sa \
  --namespace default \
  --cluster my-cluster \
  --region us-east-1 \
  --attach-policy-arn arn:aws:iam::123456789012:policy/S3ReadOnlyMyAppBucket \
  --approve

This command creates the IAM role, attaches the policy, annotates the service account with the role ARN, and creates the service account if it doesn't exist. The resulting service account looks like:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app-sa
  namespace: default
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eksctl-my-cluster-my-app-sa-role
automountServiceAccountToken: true

Step 4: Deploy a pod using this service account:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      serviceAccountName: my-app-sa
      containers:
      - name: app
        image: amazon/aws-cli:latest
        command: ["sleep", "3600"]
        env:
        - name: AWS_REGION
          value: "us-east-1"

The pod now has temporary credentials scoped to the S3ReadOnlyMyAppBucket policy, automatically rotated by the EKS credential provider.

IAM Best Practices for EKS

"Condition": {
  "BoolIfExists": {
    "aws:MultiFactorAuthPresent": "true"
  }
}

Part 2: Network Security for EKS

What Is Network Security in EKS?

Network security in EKS encompasses the controls that govern traffic flow between pods, between pods and external services, and between the cluster and the internet. It spans AWS constructs (Security Groups, VPC design, Network ACLs, VPC Endpoints) and Kubernetes-native constructs (Network Policies, service mesh sidecars, Ingress controllers). A robust network security posture ensures that only intended traffic flows, minimizing the blast radius of a compromise.

Why Network Security Matters

By default, Kubernetes allows all pod-to-pod communication. An attacker who gains a foothold in one container can scan and attack all other pods in the cluster, exfiltrate data, or pivot to cloud metadata services. Proper network segmentation, ingress/egress filtering, and explicit deny-by-default policies dramatically reduce lateral movement opportunities.

How to Implement Network Security in EKS

1. VPC and Subnet Design

Start with a well-architected VPC. Place EKS worker nodes in private subnets and use NAT gateways or VPC endpoints for outbound traffic. Expose only load balancers in public subnets. Below is a Terraform snippet for reference (conceptual):

resource "aws_vpc" "eks_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "eks-vpc"
  }
}

resource "aws_subnet" "private_a" {
  vpc_id            = aws_vpc.eks_vpc.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"
  tags = {
    Name = "eks-private-us-east-1a"
  }
}

resource "aws_subnet" "private_b" {
  vpc_id            = aws_vpc.eks_vpc.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1b"
  tags = {
    Name = "eks-private-us-east-1b"
  }
}

resource "aws_subnet" "public_a" {
  vpc_id            = aws_vpc.eks_vpc.id
  cidr_block        = "10.0.101.0/24"
  availability_zone = "us-east-1a"
  map_public_ip_on_launch = true
  tags = {
    Name = "eks-public-us-east-1a"
  }
}

Provision your EKS cluster using these private subnets for worker nodes. Only the load balancer subnets should be public-facing.

2. Security Groups for Worker Nodes and Load Balancers

AWS Security Groups act as stateful firewalls for EC2 instances (worker nodes) and Load Balancers. Design them with the principle of least privilege:

# Security group for worker nodes
resource "aws_security_group" "eks_workers" {
  name        = "eks-worker-sg"
  description = "Security group for EKS worker nodes"
  vpc_id      = aws_vpc.eks_vpc.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]  # Allow HTTPS from within VPC only
    description = "Allow kubelet communication"
  }

  ingress {
    from_port   = 10250
    to_port     = 10250
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
    description = "Kubelet API"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound (restricted via NetworkPolicy later)"
  }
}

# Security group for an internal load balancer
resource "aws_security_group" "internal_lb" {
  name        = "eks-internal-lb-sg"
  description = "Allow traffic from VPC to internal load balancer"
  vpc_id      = aws_vpc.eks_vpc.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
    description = "HTTP from VPC"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
    description = "HTTPS from VPC"
  }
}

When creating the EKS cluster, reference these security groups. For managed node groups, you can specify the worker security group in the launch template.

3. Kubernetes Network Policies

Network Policies are the cornerstone of in-cluster network segmentation. They act as firewall rules at the pod level, enforced by the CNI plugin (Amazon VPC CNI now supports Network Policies when integrated with Calico or when using the native VPC CNI Network Policy feature). Always start with a deny-all default policy, then explicitly allow required traffic.

Default deny-all for a namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}  # Applies to all pods in the namespace
  policyTypes:
  - Ingress
  - Egress

With no ingress or egress rules specified, all traffic is blocked.

Allow ingress only from a specific namespace and port:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend-api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          team: frontend
    ports:
    - protocol: TCP
      port: 8080

This permits TCP traffic on port 8080 to pods labeled app: backend-api, only from pods in namespaces labeled team: frontend.

Allow egress only to a specific CIDR block (e.g., a database or external API):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: restrict-egress-to-database
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend-api
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.100.0/24
    ports:
    - protocol: TCP
      port: 5432
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 10.0.0.0/8
    ports:
    - protocol: TCP
      port: 443
    - protocol: UDP
      port: 53

This allows egress to the database subnet on port 5432, and to the internet (excluding internal subnets) for DNS (UDP 53) and HTTPS (TCP 443). All other egress is blocked.

Apply the policies:

kubectl apply -f network-policies.yaml

Verify they are active:

kubectl get networkpolicy -n production

4. Securing the Metadata Service (IMDS)

AWS Instance Metadata Service (IMDS) provides sensitive credentials and configuration data. Pods should not be able to access IMDS unless explicitly required. Use a combination of NetworkPolicy and node-level iptables rules or the metadata service hop limit.

NetworkPolicy to block metadata access:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-metadata
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32
    - ipBlock:
        cidr: 10.0.0.0/8
        except:
        - 169.254.169.254/32

Additionally, when launching worker nodes, set the metadata hop limit to 1 and require IMDSv2 (token-based retrieval) to prevent SSRF attacks from containers:

# In launch template or EC2 instance metadata options
metadata_options {
  http_endpoint               = "enabled"
  http_tokens                 = "required"
  http_put_response_hop_limit = 1
}

This ensures that only the host (hop count 0) can reach IMDS; containers with hop count 1 are blocked.

5. VPC Endpoints for AWS Services

To keep traffic between your pods and AWS services (S3, ECR, CloudWatch, etc.) within the AWS backbone and off the public internet, use VPC endpoints. This also allows you to restrict egress traffic to only the endpoint IPs.

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.eks_vpc.id
  service_name      = "com.amazonaws.us-east-1.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [aws_route_table.private.id]
  policy            = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject", "s3:ListBucket"]
        Resource = ["*"]
        Principal = "*"
      }
    ]
  })
}

resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = aws_vpc.eks_vpc.id
  service_name        = "com.amazonaws.us-east-1.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.private_a.id, aws_subnet.private_b.id]
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = aws_vpc.eks_vpc.id
  service_name        = "com.amazonaws.us-east-1.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.private_a.id, aws_subnet.private_b.id]
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
}

With private_dns_enabled = true, pods resolve ECR endpoints to the VPC endpoint IPs automatically, keeping image pulls entirely within the VPC.

Network Security Best Practices for EKS

Bringing It All Together

A secure EKS cluster requires both IAM and network controls working in concert. A typical hardened deployment looks like this: worker nodes sit in private subnets, IRSA grants pods scoped AWS permissions, Network Policies enforce a deny-all posture with explicit allows, IMDS is blocked, VPC Endpoints keep traffic internal, and IAM policies restrict cluster management to specific roles with MFA conditions. Regular auditing of aws-auth, CloudTrail logs, and VPC Flow Logs completes the operational security loop. By implementing the patterns in this tutorial, you move from a permissive default state to a defense-in-depth architecture that significantly reduces your attack surface while maintaining workload functionality.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles