Blocking IMDS access on Karpenter NodePools
The first time I really looked at what a pod on an EKS node could reach, this is the thing that bothered me. Every EC2 instance exposes the Instance Metadata Service at 169.254.169.254. It is how the node gets its IAM role credentials, region, availability zone, and the rest of the instance information the kubelet and system components rely on.
The problem is that by default the pods running on those nodes can reach it too. They are on the same network, the address is well-known, and nothing blocks the connection.
A pod can request http://169.254.169.254/latest/meta-data/iam/security-credentials/, get back the name of the attached instance profile, and then retrieve its credentials.
If your instance profile has any meaningful permissions, any pod on that node can assume them.
This is not a theoretical risk. It is a direct path from a compromised container to AWS credentials scoped to whatever role was attached to the node at launch. And node roles tend to be broad, ecr:GetAuthorizationToken, ec2:DescribeInstances, sometimes more, because they exist to serve the node, not the individual workloads sitting on it.
Pod Identity is the right tool for workloads
If a pod needs to talk to AWS, reading from S3, publishing to SQS, assuming a role the right approach is to give that pod its own IAM role with exactly the permissions it needs. Nothing inherited from the node.
There are two mechanisms for this in EKS:
- IRSA (IAM Roles for Service Accounts) — associates a Kubernetes service account with an IAM role via OIDC federation. The older approach, requires per cluster OIDC setup.
- EKS Pod Identity — the newer approach, available since EKS 1.24. Simpler to configure and does not require an OIDC provider.
Either way, the pod gets short lived credentials scoped to its own role, not the node's. The node's instance profile should exist for the node, not for the workloads running on it.
Blocking IMDS in Karpenter
With Karpenter, node configuration lives in EC2NodeClass. The metadataOptions field maps directly to EC2's instance metadata options, and this is where IMDS access is controlled.
The key setting is httpPutResponseHopLimit. IMDSv2 requires a PUT request to obtain a session token before any metadata request can be made. The hop limit controls how many network hops that PUT is allowed to traverse.
Set it to 1 and only the node itself can reach IMDS. A container's network namespace introduces an extra hop so the PUT never completes, the token is never issued, and the pod cannot access the service. The kubelet and other node-level processes are unaffected.
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
metadataOptions:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 1
httpTokens: requiredhttpTokens: required enforces IMDSv2 across the board, no unauthenticated requests to IMDS at all. Combined with a hop limit of 1, pods are fully blocked. The node can still reach IMDS, the pods on it cannot, and that is the boundary you want.
The part that is easy to get wrong is that this has to be set on every EC2NodeClass in the cluster. If you have multiple NodePools backed by different NodeClasses, each one needs httpPutResponseHopLimit: 1. A single unconfigured NodeClass is enough to leave the whole gap open, and it will not announce itself, it just quietly stays reachable until someone goes looking. So it is worth treating this as something you assert across every NodeClass rather than set once and trust.