kube2iam: per-pod AWS IAM credentials via EC2 metadata interception, with an explicit EKS caveat
kube2iam provides different AWS IAM roles for pods running on Kubernetes
At a glance
- What is it?
- kube2iam assigns different IAM roles to Kubernetes pods by intercepting EC2 metadata traffic on each node and returning per-pod STS credentials. Its README opens with a recommendation to prefer IAM roles for service accounts on EKS.
- Who is it for?
- kube2iam suits teams running Kubernetes on EC2 outside EKS, where per-pod IAM isolation is a requirement and IAM roles for service accounts is not an option. Skip it on EKS: the README calls IRSA the preferred drop-in replacement.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 157 days ago.
- What is it written in?
- Mainly HTML, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
How kube2iam intercepts metadata requests to give each pod its own IAM role
In a Kubernetes cluster on AWS, every pod on a node shares the same EC2 instance metadata endpoint at 169.254.169.254. AWS SDK calls fetch temporary credentials from this address. Assigning the node an IAM role broad enough for all tenant pods produces a union of all permissions, which the README calls unacceptable from a security perspective.
kube2iam resolves this by sitting between pods and the metadata endpoint. It runs as a DaemonSet on each node, captures credential requests via an iptables DNAT rule, and routes them to port 8181. When a pod's request arrives, kube2iam reads the pod's iam.amazonaws.com/role annotation, calls AWS STS to retrieve temporary credentials for that specific role, and returns them. Calls that are not credential requests pass through to the real EC2 metadata service unchanged.
From the pod's perspective, no AWS SDK change is needed. Credentials arrive from 169.254.169.254 as usual; what changes is the role they represent, which is the one named in the annotation rather than the node's instance role.
For EKS, the project's own guidance recommends IAM roles for service accounts instead
kube2iam's README opens with an EKS caveat, placed before the problem statement. It says that if you are using EKS, IAM roles for service accounts should be a drop-in-replacement for kube2iam and should be preferred, and it points to issue #368 for context.
IAM roles for service accounts provides the same per-pod role isolation without the iptables interception architecture. kube2iam's audience is engineers running Kubernetes on EC2 outside EKS, or maintaining existing clusters where switching to IRSA has not yet happened.
An EKS/amazon-vpc-cni-k8s entry (`eni+`) still appears in kube2iam's CNI interface name table alongside eight other CNI configurations. Its presence, despite the EKS guidance, indicates that kube2iam is deployed on some EKS clusters. That also means the documentation supports the interface even if the project steers new EKS users elsewhere.
Node worker role setup: sts:AssumeRole, ec2:DescribeRegions, and Trust Relationships
Two permissions are required on the node's IAM role before kube2iam can operate. aws-go-sdk-v2, used by kube2iam, does not include a built-in region list, so ec2:DescribeRegions must be granted explicitly alongside sts:AssumeRole. Earlier SDK versions did not need this second permission.
Target roles, the ones pods will actually use, each need a Trust Relationship naming the worker node role as a trusted principal. Below is the example from the README:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
},
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/kubernetes-worker-role"
},
"Action": "sts:AssumeRole"
}
]
}Every target role must contain a statement like the second one above. Replacing the placeholder ARN with the actual worker role ARN is the step that ties the trust together.
Deploying as a DaemonSet with hostNetwork: true and the --base-role-arn argument
Every kube2iam pod must run with `hostNetwork: true` in the pod spec so it can reach 169.254.169.254 from the node's own network namespace. A pod-networked container is isolated and cannot reach that link-local address. Below is the container spec from the README:
containers:
- image: jtblin/kube2iam:latest
name: kube2iam
args:
- "--base-role-arn=arn:aws:iam::123456789012:role/"
- "--node=$(NODE_NAME)"
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
ports:
- containerPort: 8181
hostPort: 8181
name: http`--base-role-arn` sets the ARN prefix that kube2iam prepends to role names from pod annotations. `--node` identifies the node; the README derives it from spec.nodeName via the Kubernetes downward API. Port 8181 is both the container port and the host port.
For a Helm-based setup, the Helm command is:
helm install kube2iam oci://ghcr.io/jtblin/kube2iam-chart --version [VERSION]Chart-specific configuration options live in charts/kube2iam/README.md.
Redirecting 169.254.169.254 with iptables and the race condition in --iptables=true
Pods reach 169.254.169.254 directly until an iptables DNAT rule intercepts that traffic. A PREROUTING rule is required to send port 80 traffic for that address to kube2iam's port 8181:
iptables \
--append PREROUTING \
--protocol tcp \
--destination 169.254.169.254 \
--dport 80 \
--in-interface docker0 \
--jump DNAT \
--table nat \
--to-destination `curl 169.254.169.254/latest/meta-data/local-ipv4`:8181Setting `--iptables=true` and providing `HOST_IP` lets kube2iam add this rule itself, running in a privileged security context. A warning in the README states that pods starting on a node before kube2iam is ready get direct access to the EC2 metadata API. Such a pod can assume the node's EC2 instance role and, from there, assume any other role that instance role can reach. Use this option only when pod creation on the cluster is fully controlled.
Deleting the DaemonSet must be paired with rule cleanup. Running the following command in the pod's preStop hook removes the rule:
eval `iptables-save | grep 169.254.169.254 | sed s/-A/'iptables -t nat -D'/g`CNI interface names and the shared cni0 value for flannel and generic CNI
Different CNI plugins use different network interface names, and the iptables rule's `--in-interface` argument must match the one active on each node. Nine configurations have documented values:
- Calico: `cali+` - kops (kubenet): `cbr0` - CNI: `cni0` - EKS with amazon-vpc-cni-k8s: `eni+` - Weave: `weave` - Flannel: `cni0` - kube-router: `kube-bridge` - OpenShift: `tun0` - Cilium: `lxc+`
Generic CNI and flannel share `cni0`. Switching between them on a cluster requires no change to the iptables rule or the `--host-interface` argument. Plus signs in `cali+` and `eni+` are prefix wildcards: Calico creates per-pod interfaces like `cali1234567890`, and the prefix matches all of them.
What kube2iam does not handle and where it should not be used
kube2iam is specific to AWS. Without the EC2 instance metadata service at 169.254.169.254, there is nothing to intercept and no STS endpoint to call. GKE, AKS and any non-EC2 Kubernetes deployment are outside its scope entirely.
Only credential requests are intercepted; regular AWS API calls from pods pass through standard AWS SDK channels without touching kube2iam. There is no support for OIDC tokens, workload identity federation or any credential mechanism other than the STS AssumeRole path via the metadata endpoint.
On EKS, the iptables interception approach adds ongoing operational cost: a DaemonSet to maintain, iptables rules whose startup order must stay ahead of other pods, and Trust Relationships on each target role. IRSA avoids all of these.
The last push was on 2026-05-08, and the most recent release is 0.15.0, published on 2026-05-04. kube2iam is distributed under the BSD-3-Clause licence. Multi-arch Docker images cover linux/arm/v7, linux/arm64/v8 and linux/amd64, as the Makefile PLATFORMS variable shows.
Editorial conclusion
kube2iam suits teams running Kubernetes on EC2 outside EKS, where per-pod IAM isolation is a requirement and IAM roles for service accounts is not an option. Skip it on EKS: the README calls IRSA the preferred drop-in replacement. Before deploying, confirm the correct CNI interface name from the --host-interface table, decide whether to apply the iptables rule before kubelet starts or accept the --iptables=true race condition, and replace the placeholder ARN arn:aws:iam::123456789012:role/ in all configuration with the actual base role ARN.
Frequently asked questions
What is kube2iam?
kube2iam is a Kubernetes DaemonSet that intercepts EC2 metadata requests from pods and returns per-pod AWS IAM credentials via STS, based on an iam.amazonaws.com/role annotation on each pod. It allows different pods on the same node to use different IAM roles without sharing the node's instance role.
Should I use kube2iam with EKS?
kube2iam's README explicitly recommends IAM roles for service accounts for EKS and describes it as a drop-in replacement that should be preferred. kube2iam is aimed at Kubernetes clusters running on EC2 outside EKS.
How do I install kube2iam?
kube2iam can be installed as a DaemonSet YAML with hostNetwork: true, or via Helm using helm install kube2iam oci://ghcr.io/jtblin/kube2iam-chart --version [VERSION]. Both paths require the node IAM role to carry sts:AssumeRole and ec2:DescribeRegions permissions.
What is the kube2iam iptables rule for?
Without an iptables DNAT rule, pods can reach 169.254.169.254 directly and bypass kube2iam. The PREROUTING rule redirects TCP port 80 traffic for that address to kube2iam's port 8181. Setting --iptables=true automates this but introduces a timing risk for pods that start before kube2iam.
What CNI interface should I use with kube2iam?
It depends on the CNI plugin: Calico uses cali+, Weave uses weave, Cilium uses lxc+, and both generic CNI and flannel use cni0. Pass the correct value as --host-interface or use it in the iptables --in-interface argument.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jtblin-kube2iam)