Containers

Implement per-pod image pull permissions with ECR repository policies on Amazon EKS

If your organization runs a multi-tenant Amazon Elastic Kubernetes Service (Amazon EKS) cluster, you likely need to scope access to Amazon Elastic Container Registry (Amazon ECR) container image repositories per team. Each team needs access only to their own repositories, blocked from pulling other teams’ images.

By default, Amazon EKS configures image pull permissions for Amazon ECR at the node level. Pods running on the same node share the same node credentials and the same access to ECR repositories, regardless of which team owns them. Each team might own their own ECR repositories, but the shared node role gives pods in the cluster equal access to Amazon ECR.

For platform teams that want to assign repository permissions at the pod level, giving each team’s applications their own scoped access to specific repositories, this has been a challenge. It required custom admission policies, third-party policy engines, or per-team node groups. Starting with Kubernetes 1.34, Kubernetes Enhancement Proposal (KEP) 4412 introduces per-pod credential support for kubelet image pulls, making pod-level repository permissions possible with AWS native controls. AWS added full support for this feature on Amazon EKS v1.35 or later.

This post shows how to scope Amazon ECR image pull permissions to individual Kubernetes pods using the ecr-credential-provider and ECR repository policies on Amazon EKS.

Solution overview

In an Amazon EKS cluster, each node is assigned a node AWS Identity and Access Management (IAM) role. The kubelet process on each node uses this role to execute operations required for provisioning containers, such as pulling container images from Amazon ECR. Image pull authentication is handled by provider-specific binaries, called credential providers. When a pod is being provisioned, the kubelet matches the image URI against its CredentialProviderConfig and invokes the corresponding credential provider binary. EKS nodes include the ecr-credential-provider with a baseline configuration that allows the kubelet to authenticate with ECR and pull container images using the node’s IAM role.

Because the node IAM role includes ECR pull permissions by default, all pods on the node share the same access to ECR repositories. The image pull process has no visibility into Kubernetes-specific details such as namespaces, pod labels, or service accounts, which are typically used for team isolation.

KEP 4412 introduces per-pod credential support for kubelet image pulls. When the tokenAttributes field is configured in the CredentialProviderConfig, the kubelet projects a service account token bound to the pod and passes it to the credential provider. The ECR credential provider can then use an IAM role defined in the pod service account annotation to authenticate with ECR, instead of using the node IAM role. It does this by calling the AWS Security Token Service (AWS STS) AssumeRoleWithWebIdentity API. This allows each team to use their own IAM role for ECR access, while platform teams can use ECR repository policies to block or allow access to private repositories.

Two layers of enforcement work together: the team IAM role scopes who the pod authenticates as, and the ECR repository policy decides what that identity can access.

Diagram of the per-pod image pull flow from the pod service account through the ECR credential provider and AWS STS to Amazon ECR repository policy enforcement

Figure 1: Per-pod Amazon ECR image pull flow with AWS STS and repository policy enforcement

The full flow works as follows:

  1. A pull IAM role is created per team, with an OpenID Connect (OIDC) trust policy scoped to the team’s service accounts.
  2. Team applications create pods with the eks.amazonaws.com/ecr-role-arn service account annotation, pointing to the team’s pull IAM role.
  3. The kubelet projects a service account token bound to the pod and passes it to the ecr-credential-provider.
  4. The provider reads the ecr-role-arn annotation and calls STS AssumeRoleWithWebIdentity with the OIDC token and the role ARN.
  5. STS validates the OpenID Connect (OIDC) token against the cluster’s OIDC provider and returns temporary credentials scoped to the team IAM role.
  6. The provider calls ECR GetAuthorizationToken using the team IAM role credentials.
  7. ECR returns an authorization token. The provider passes it back to the kubelet.
  8. The kubelet (through the container runtime) uses the ECR token to pull the ECR image.
  9. ECR evaluates the repository’s resource policy at this point. If the policy allows the team IAM role, the pull succeeds. If it denies the principal (for example, another team’s role or the node role), the pull fails with 403 Forbidden.

This feature relies on a similar mechanism to IAM Roles for Service Accounts (IRSA), but it targets a different outcome. IRSA uses the eks.amazonaws.com/role-arn service account annotation to define an IAM role that a running pod assumes to access AWS resources at runtime. The ECR pull role uses the eks.amazonaws.com/ecr-role-arn annotation to tell the kubelet and the ECR credential provider which role to assume when pulling the pod’s image from ECR. In short, IRSA scopes what the application can do once running. The ECR pull role scopes what image the pod is allowed to pull.

Two main requirements are needed to enable KEP 4412 in EKS:

  • EKS v1.35 or later (cluster and nodes): The KubeletServiceAccountTokenForCredentialProviders feature gate reached beta and is on by default in EKS 1.34. However, the ecr-credential-provider only added full support for the feature (including fallback to the node role) in v1.35. EKS v1.35 or later ensures the feature is fully functional end to end.
  • Role-based access control (RBAC) audience permission applied before nodes join: The kubelet needs permission to request service account tokens with the sts.amazonaws.com audience. Without the required ClusterRole and ClusterRoleBinding in place, the kubelet cannot project tokens, all image pulls fail, and nodes will not become Ready. In the following walkthrough, you will create the required ClusterRole and ClusterRoleBinding before provisioning EKS nodes.

Walkthrough

In this walkthrough, you will create an Amazon EKS cluster, provision the necessary infrastructure, and deploy sample applications for two teams (Team A and Team B) to demonstrate per-pod ECR pull permissions. The full source code, Terraform configuration, and deployment scripts are available in the GitHub repository.

Prerequisites

Step 1: Configure the environment for deployment

Clone the repository:

git clone https://github.com/aws-samples/sample-ecr-per-pod-permission
cd sample-ecr-per-pod-permission

Update infra-tf/variables.tf to configure how resources will be provisioned in your environment:

  • aws_region (required) – AWS Region for all resources.
  • cluster_name (required) – EKS cluster name.
  • cluster_version – Kubernetes version (>= 1.35). Default: 1.35.
  • node_instance_type – Amazon Elastic Compute Cloud (Amazon EC2) instance type for nodes. Default: t3.medium.

Step 2: Deploy infrastructure

Run the script:

./scripts/create-infra.sh

This script uses Terraform to provision all required AWS resources and configuration to showcase this solution.

EKS cluster and VPC networking

Terraform creates an EKS cluster and the required VPC networking.

RBAC audience permission

Terraform provisions a ClusterRole and ClusterRoleBinding that grant the system:nodes group the ability to request tokens with the sts.amazonaws.com audience. This grants permissions to the kubelet to project pod service account tokens when calling the ECR credential provider.

This ClusterRole and ClusterRoleBinding must exist before any node joins the cluster.

EKS node group with per-pod credential provider configuration

Nodes must be provisioned with an updated ECR credential provider configuration. The standard configuration includes matchImages patterns for ECR registries. The key addition is the tokenAttributes section, relevant for KEP 4412. This configuration tells the kubelet to attach a service account token when invoking the credential provider, which provides per-pod identity when calling ECR APIs. This allows the ecr-credential-provider to read the ecr-role-arn annotation from the pod service account and assume the team IAM role to authenticate with ECR.

When a pod service account doesn’t have the ecr-role-arn annotation, the provider falls back to the node IAM role. This means that system pods (VPC CNI, kube-proxy, CoreDNS) and shared workloads continue to pull images without changes.

The following snippet, from infra-tf/eks.tf file in the GitHub repository, shows the node UserData that writes the CredentialProviderConfig at node boot:

cat > /etc/eks/image-credential-provider/config.json <<'CONFIG'
{
  "apiVersion": "kubelet.config.k8s.io/v1",
  "kind": "CredentialProviderConfig",
  "providers": [
    {
      "name": "ecr-credential-provider",
      "matchImages": [
        "*.dkr.ecr.*.amazonaws.com",
        "*.dkr-ecr.*.on.aws",
        "*.dkr.ecr.*.amazonaws.com.cn",
        "*.dkr-ecr.*.on.amazonwebservices.com.cn",
        "*.dkr.ecr-fips.*.amazonaws.com",
        "*.dkr-ecr-fips.*.on.aws",
        "*.dkr.ecr.*.c2s.ic.gov",
        "*.dkr.ecr.*.sc2s.sgov.gov",
        "*.dkr.ecr.*.cloud.adc-e.uk",
        "*.dkr.ecr.*.csp.hci.ic.gov",
        "*.dkr.ecr.*.amazonaws.eu",
        "public.ecr.aws",
        "ecr-public.aws.com"
      ],
      "defaultCacheDuration": "12h0m0s",
      "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
      "tokenAttributes": {
        "serviceAccountTokenAudience": "sts.amazonaws.com",
        "cacheType": "ServiceAccount",
        "requireServiceAccount": false,
        "optionalServiceAccountAnnotationKeys": [
          "eks.amazonaws.com/ecr-role-arn"
        ]
      }
    }
  ]
}
CONFIG

Key fields in tokenAttributes:

Field Value Purpose
serviceAccountTokenAudience sts.amazonaws.com The audience claim in the projected token, required by STS for AssumeRoleWithWebIdentity API.
cacheType ServiceAccount Cache credentials per service account (all pods with the same service account share cached credentials).
requireServiceAccount false Allow pods without a service account to fall back to the node role.
optionalServiceAccountAnnotationKeys eks.amazonaws.com/ecr-role-arn Tells the kubelet to include this annotation value from the pod service account in the request to the provider.

IAM roles per team for ECR pull access

Terraform provisions one IAM role per team, scoped to the team’s namespace. Each team annotates their pod service accounts with their ECR pull role to provide credentials for image pull operations. The role trust policy uses the cluster’s OIDC provider as the federated principal to allow all pods in the team’s namespace to assume it. This pattern enforces a namespace trust boundary, which is standard for Kubernetes multi-tenant clusters.

The following snippet, inside the infra-tf/iam-roles.tf file, shows the Team A role trust policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::12345EXAMPLE:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/12345EXAMPLE8203BA1E322C42A"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/12345EXAMPLE8203BA1E322C42A:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "oidc.eks.us-east-1.amazonaws.com/id/12345EXAMPLE8203BA1E322C42A:sub": "system:serviceaccount:team-a:*"
        }
      }
    }
  ]
}

Team roles include the AWS managed policy AmazonEC2ContainerRegistryPullOnly, the same policy AWS recommends attaching to the EKS node IAM role for baseline ECR pull permissions. Reusing it here means each team’s pull role grants the minimum permissions needed to pull images, matching the node role’s baseline.

ECR repositories with access policies

ECR repositories show different use cases, such as team private, shared, or baseline repositories with no repository policy attached.

Repositories:

  • team-a/app – private repository for Team A.
  • team-b/app – private repository for Team B.
  • shared/app – shared repository accessible to both teams.
  • baseline/app – unprotected repository with no access policy.

As a platform team managing shared and private ECR repositories, you’re now in a position to control access using ECR repository policies.

For each private repository, deny access to all AWS principals and define an exception list to allow access to specific AWS principals. Because this is an explicit Deny, principals not listed in the exception are blocked regardless of their own IAM permissions. Make sure the exception list includes every principal that legitimately needs access, such as the team’s pull role, the image-push tooling for continuous integration and continuous delivery (CI/CD), and any break-glass admin. Otherwise, those pulls or pushes will also fail.

The following snippet, found in the infra-tf/ecr.tf file, shows the ECR repository policy for the Team A repository:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyAllExceptTeamAAndAdmin",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "ecr:*",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalArn": [
            "arn:aws:iam::12345EXAMPLE:role/ecr-pod-permission-test-ecr-pull-team-a",
            "arn:aws:iam::12345EXAMPLE:role/Admin"
          ]
        }
      }
    }
  ]
}

Step 3: Configure cluster access

After the infrastructure is created, configure access to the EKS cluster:

$(terraform -chdir=infra-tf output -raw configure_kubectl)

Step 4: Push container images to ECR

Push container images to ECR repositories. The script builds and pushes four images to the Amazon ECR repository you created with Terraform. It requires a container runtime in your environment to build and push images to ECR. It automatically detects and uses docker, podman, or finch.

./scripts/build-push-images.sh

Step 5: Deploy applications and verify access

The following script creates namespaces and deploys sample workloads for different teams to showcase multiple scenarios: team pod service accounts annotated with the team’s pull IAM role, and pods without service account using the node role as a fallback mechanism.

./scripts/apply-manifests.sh

Eleven pods across three identity types demonstrate per-pod credential isolation. The baseline/app repository acts as a control with no ECR repository policy, showing the default behavior where any AWS principal can pull. The protected repositories (team-a/app, team-b/app, shared/app) show how repository policies enforce team-level isolation.

Each team pod can pull images from the team’s own repository and the shared repository. However, pods from one team cannot pull from another team’s repository, as expected.

Pods without an ECR pull role annotation fall back to the node role, which can access the baseline repository but is denied access to protected team repositories.

This confirms that pull isolation is enforced per team, and other workloads are blocked from accessing team private repositories, while pods retain baseline access to repositories without repository policy.

Verify pod status to confirm that repository access works as expected:

Team A

echo "=== team-a ==="
kubectl get pods -n team-a
Deployment ECR Image ServiceAccount Credential Used Result
team-a-to-own team-a/app sa-team-a-to-own ECR pull role (team-a) ✅ Running
team-a-to-team-b team-b/app sa-team-a-to-team-b ECR pull role (team-a) ❌ Denied by repo policy
team-a-to-shared shared/app sa-team-a-to-shared ECR pull role (team-a) ✅ Running
team-a-to-baseline baseline/app sa-team-a-to-baseline ECR pull role (team-a) ✅ Running

Notice that the cross-team access from team-a to team-b wasn’t allowed, because of the permissions attached to the respective ServiceAccount ECR pull role.

Team B

echo "=== team-b ==="
kubectl get pods -n team-b
Deployment ECR Image ServiceAccount Credential Used Result
team-b-to-own team-b/app sa-team-b-to-own ECR pull role (team-b) ✅ Running
team-b-to-team-a team-a/app sa-team-b-to-team-a ECR pull role (team-b) ❌ Denied by repo policy
team-b-to-shared shared/app sa-team-b-to-shared ECR pull role (team-b) ✅ Running
team-b-to-baseline baseline/app sa-team-b-to-baseline ECR pull role (team-b) ✅ Running

The same behavior happens here for cross-team access. Team-b cannot pull from the team-a repository.

No ECR pull role (default ServiceAccount, node role fallback)

echo "=== node-role (node role fallback) ==="
kubectl get pods -n node-role
Deployment ECR Image ServiceAccount Credential Used Result
node-role-to-team-a team-a/app default Node role (fallback) ❌ Denied by repo policy
node-role-to-shared shared/app default Node role (fallback) ❌ Denied by repo policy
node-role-to-baseline baseline/app default Node role (fallback) ✅ Running

Lastly, pods without a ServiceAccount (using the Kubernetes default ServiceAccount without an ECR pull role annotation) default to using the IAM role attached to the node for image pulls. This role isn’t allowed to pull any team-related image, including the one shared across both teams.

Clean up

To avoid incurring future charges, delete the resources created in this walkthrough.

First, remove the Kubernetes resources:

./scripts/apply-manifests.sh delete

Then destroy the infrastructure:

./scripts/cleanup.sh

This runs terraform destroy, which deletes the VPC, EKS cluster, ECR repositories (including all images), and IAM roles.

Conclusion

In this post, you implemented per-pod Amazon ECR image pull permissions on Amazon EKS using Kubernetes KEP 4412 and ECR repository policies. Platform teams can now enforce granular control over team repository access with a two-layer approach. Per-team IAM roles scope each pod’s image-pull credentials, and ECR repository policies control which identities can pull from each repository. This improves tenant isolation in multi-tenant EKS clusters using native AWS controls, without additional Kubernetes admission controllers or custom policies. System pods and existing workloads continue to work unchanged through the node role fallback, preserving backward compatibility.

To learn more, see the following resources:


About the authors

Asiel Bencomo Corona

Asiel Bencomo Corona

Asiel is a Containers Specialist Solutions Architect at Amazon Web Services (AWS), where he focuses on container orchestration, AI/ML solutions on Kubernetes, and Open Source projects. With over a decade of experience across enterprise networking and infrastructure, Asiel specializes in bridging the gap between legacy systems and modern, cloud-native architectures to help customers scale effectively. Based in New York. Connect with Asiel on LinkedIn to discuss the latest in cloud-native trends.

Rodrigo Bersa

Rodrigo Bersa

Rodrigo is a Senior Specialist Solutions Architect for Containers and AppMod, with a focus on security and infrastructure-as-code automation. In this role, Rodrigo aims to help customers achieve their business goals by applying best practices for AWS container services when building new environments or migrating existing technologies. Connect with Rodrigo on LinkedIn to discuss the latest in Containers and Agentic trends.