IMDS Access, Pod Identity, and Karpenter NodePools
If you are using EKS, 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 other instance information that the kubelet and system components rely on.
By default, pods running on those nodes can reach IMDS 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. Node roles tend to be broad ecr:GetAuthorizationToken, ec2:DescribeInstances, sometimes more because they exist to serve the node, not individual workloads.
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 role.
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. Pods on that node cannot. That is the boundary you want.
One thing worth noting, this needs 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 a gap.