SMS Blog
Leveraging EKS Pod Identity on AWS EKS (Part 3 of 3): Cross-Account Access
Welcome to the final installment of our series on mastering AWS access management with EKS Pod Identity. In Part 1, “Securing Application Access“, we laid the foundation by replacing insecure legacy methods with the fine-grained control of Pod Identity for your applications. In Part 2, “Cluster Management“, we extended this unified security model to the operational add-ons that power your cluster.
Now, we’ll tackle one of the most common and complex challenges in enterprise environments: granting EKS pods access to AWS resources in a different AWS account. This post will introduce a powerful enhancement to EKS Pod Identity that dramatically simplifies cross-account permissions, allowing you to build secure, multi-account architectures with ease.
The Challenge: The Complexity of Cross-Account IAM
It’s a standard architecture to host your EKS cluster in one AWS account while your data, such as S3 buckets or DynamoDB tables, resides in another. This separation is great for security and billing, but it has historically created complexity for IAM. To make this work, you had to manually configure a chain of trust involving multiple roles, policies, and trust relationships—a process that was often frustrating and error-prone.
A Better Way: Native Cross-Account Associations
With the latest enhancements to EKS Pod Identity, this entire process is streamlined. You can now configure the cross-account relationship directly within the aws_eks_pod_identity_association
resource itself.
The underlying mechanism is still IAM role chaining, but EKS now manages the handoff for you. When creating a Pod Identity association, you can specify two roles:
- EKS Pod Identity Role (Primary Role): An IAM role that exists in the same account as your EKS cluster.
- Target IAM Role: An IAM role from the separate account that contains your AWS resources.
When your application needs credentials, the EKS Pod Identity agent seamlessly performs the two-step role assumption process, providing your pod with temporary credentials that have the permissions of the Target IAM Role.
NOTE: These recent changes to cross account EKS Pod Identity access were announced in the AWS Blog post Amazon EKS Pod Identity streamlines cross account access.
How Cross-Account Pod Identity Works
So, how does EKS Pod Identity pull off this seamless cross-account access? The process is a sophisticated and secure two-step handshake between Kubernetes and AWS IAM, orchestrated entirely by the EKS Pod Identity agent.
However, before we go through how it works, it is important to understand how the resources are connected. As in a single-account setup, each Kubernetes Pod has an associated Service Account. The aws_eks_pod_identity_association
resource is the critical bridge, but in a cross-account scenario, it connects the Service Account to two IAM Roles: a Primary Role in the cluster’s account (Account A) and a Target Role in the resource account (Account B).

Figure 1: Connecting Kubernetes resources to AWS resources across different accounts
Now let’s walk through how cross-account EKS Pod Identity works step by step.
- The Request (Account A): It all begins inside your EKS cluster in Account A. Your application, running in a Pod, needs to read an object from an S3 bucket in Account B. It uses the standard AWS SDK to make the call, completely unaware of the credential magic that’s about to happen. The Pod is configured to use a specific Kubernetes Service Account (e.g.,
my-app-sa
). - The Interception (Account A): Before that request can leave the worker node, it’s intercepted by the EKS Pod Identity Agent running on the node. The agent inspects the request and sees that it came from a pod using the
my-app-sa
Service Account. - The Association Lookup (Account A): The agent consults the
aws_eks_pod_identity_association
you created. It sees that the Service Account is mapped to theeks-pod-identity-primary-role
(the Primary Role) and, crucially, that atarget_role_arn
is also specified, pointing to thes3-reader-target-role
in Account B. - First “Assume Role” Handshake (Account A): The agent makes its first call to the AWS Security Token Service (STS). It says, “I have a request from a pod authorized to assume the Primary Role. Please grant me credentials for it.” STS validates that the Primary Role’s trust policy allows the
pods.eks.amazonaws.com
service principal and returns a set of temporary credentials for the Primary Role. - Second “Assume Role” Handshake (From Account A to B): This is the key cross-account step. The EKS Pod Identity Agent, now holding temporary credentials for the Primary Role, immediately makes a second call to STS. This time it says, “Using my current identity as the Primary Role, I want to assume the Target Role in Account B.”
- Cross-Account Validation (Account B): STS in Account B receives this second request. It examines the Target Role’s trust policy and sees that it explicitly trusts the Primary Role from Account A (
arn:aws:iam::111111111111:role/eks-pod-identity-primary-role
). - Generate Final Temporary Credentials (From Account B): Because the trust is valid, STS in Account B generates a final set of temporary credentials that have the permissions of the Target Role (i.e., read access to the S3 bucket). It sends these powerful, cross-account credentials back to the agent in Account A.
- The Secure Injection (Account A): The agent securely receives these final credentials and injects them into the application pod’s environment.
- The Authenticated Call (From Account A to B): The AWS SDK in your pod, which was patiently waiting, automatically discovers and uses these new credentials to complete its original, now-authenticated request directly to the Amazon S3 bucket in Account B. The application gets its data, and not a single static access key was ever stored in your cluster.

Figure 2: EKS Cross-Account Access Flow
Terraform in Action: Cross-Account S3 Access
Let’s walk through a complete Terraform example. We have an EKS cluster in Account A that needs read access to an S3 bucket in Account B.
Important Prerequisites & Key Learnings
Before deploying, it’s critical to understand these key requirements that were uncovered during real-world testing:
- Terraform Provider Version: You must use version
6.2.0
or higher of the Terraform AWS provider. This version introduced thetarget_role_arn
argument, which is the correct and required way to configure the cross-account association in theaws_eks_pod_identity_association
Terraform resource. Version6.3.0
was the current version of the provider when this post was written. Generally speaking, using the latest available version of the AWS provider is recommended unless it introduces an incompatibility with your code that you have not had the opportunity to address yet. - Terraform Deployment Order: Because the role in Account B must trust the role in Account A, you cannot deploy everything at once. The primary role in Account A must be created first to prevent an “Invalid principal in policy” error. We will cover this in the deployment steps.
- Primary Role Trust Policy: The primary role (in Account A) must trust the
pods.eks.amazonaws.com
service principal with bothsts:AssumeRole
andsts:TagSession
permissions. Missingsts:TagSession
will cause the Pod Identity agent to fail. - Target Role Trust Policy Format: While standard IAM policy allows combining actions in an array, we found that explicitly separating
sts:AssumeRole
andsts:TagSession
into two distinct statements in the target role’s trust policy is more reliable and is thus the recommended approach. - IAM Eventual Consistency: IAM changes are not always instantaneous. A pod may start before IAM permissions have fully propagated, causing initial authentication failures. The test pod in this guide includes a robust retry mechanism to handle this.
- EKS Pod Identity Agent Add-on: This entire process relies on the
eks-pod-identity-agent
running in your cluster. Ensure this add-on is installed and active. For installation instructions, please refer back to Part 1 of this series, “Securing Application Access.“
Step 1: Prepare Your Terraform Directories
To manage each account’s resources independently, create a main project folder with two subfolders, one for each account.
mkdir -p eks-cross-account/account-a mkdir -p eks-cross-account/account-b
You will place the Terraform files for each account into its respective directory.
Step 2: Configure Account A (EKS Cluster Account)
In the account-a
folder, create a file named main.tf
. This file will contain all the resources needed for your EKS cluster account.
# eks-cross-account/account-a/main.tf terraform { required_providers { aws = { source = "hashicorp/aws" # Version 6.2.0 or higher is required for the target_role_arn argument. # These code samples were tested with the 6.3.0 version version = "~> 6.3" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.20" } } } provider "aws" { region = var.region # Ensure your CLI is authenticated to Account A } variable "region" { description = "The AWS region where your EKS cluster is located." type = string default = "us-east-1" } variable "eks_cluster_name" { description = "The name of your existing EKS cluster." type = string default = "my-eks-cluster" # <-- Replace with your cluster name } variable "account_b_id" { description = "The AWS Account ID where the S3 bucket and target role exist." type = string default = "222222222222" # <-- Replace with the ID of Account B } variable "data_bucket_name" { description = "The name of the S3 bucket in Account B." type = string default = "the-bucket-name-from-account-b-output" # <-- Replace with the output from the Account B deployment } # The "EKS Pod Identity Role" (Primary Role) in Account A. resource "aws_iam_role" "primary_role" { name = "eks-pod-identity-primary-role" # This role must trust the EKS Pod Identity service with both actions. assume_role_policy = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Principal = { Service = "pods.eks.amazonaws.com" }, Action = [ "sts:AssumeRole", "sts:TagSession" ] }, ] }) } # Policy granting the Primary Role permission to assume the Target Role. resource "aws_iam_policy" "assume_target_role_policy" { name = "allow-assume-s3-reader-target-role" policy = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Action = ["sts:AssumeRole", "sts:TagSession"], Resource = "arn:aws:iam::${var.account_b_id}:role/s3-reader-target-role" } ] }) } resource "aws_iam_role_policy_attachment" "primary_role_attachment" { role = aws_iam_role.primary_role.name policy_arn = aws_iam_policy.assume_target_role_policy.arn } resource "aws_eks_pod_identity_association" "cross_account" { cluster_name = var.eks_cluster_name namespace = "my-app-namespace" service_account = "my-app-sa" role_arn = aws_iam_role.primary_role.arn target_role_arn = "arn:aws:iam::${var.account_b_id}:role/s3-reader-target-role" } resource "kubernetes_namespace" "app" { metadata { name = "my-app-namespace" } } resource "kubernetes_service_account" "app" { metadata { name = "my-app-sa" namespace = kubernetes_namespace.app.metadata[0].name } } resource "kubernetes_pod" "s3_access_test" { metadata { name = "s3-access-test-pod" namespace = kubernetes_namespace.app.metadata[0].name } spec { service_account_name = kubernetes_service_account.app.metadata[0].name container { name = "aws-cli" image = "amazon/aws-cli:latest" command = [ "/bin/sh", "-c", <<-EOT echo "--- Waiting 10 seconds before first attempt... ---" sleep 10 for i in {1..5}; do echo "--- AWS CLI attempt #$i ---" aws s3 ls "s3://${var.data_bucket_name}/" && exit 0 if [ $i -lt 5 ]; then echo "--- Command failed. Retrying in 3 seconds... ---" sleep 3 fi done echo "--- Command failed after 5 attempts. ---" exit 1 EOT ] } restart_policy = "Never" } depends_on = [aws_eks_pod_identity_association.cross_account] }
Step 3: Configure Account B (Resource Account)
In the account-b
folder, create a file named main.tf
. This file will contain the S3 bucket and the target IAM role.
# eks-cross-account/account-b/main.tf provider "aws" { region = var.region # Ensure your CLI is authenticated to Account B } variable "region" { description = "The AWS region for the resources." type = string default = "us-east-1" } variable "account_a_id" { description = "The AWS Account ID where the EKS cluster resides." type = string default = "111111111111" # <-- Replace with the ID of Account A } resource "random_pet" "bucket_suffix" { length = 2 } resource "aws_s3_bucket" "data_bucket" { bucket = "my-cross-account-data-bucket-${random_pet.bucket_suffix.id}" } resource "aws_iam_role" "target_role" { name = "s3-reader-target-role" assume_role_policy = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Principal = { AWS = "arn:aws:iam::${var.account_a_id}:role/eks-pod-identity-primary-role" }, Action = ["sts:AssumeRole"] }, { Effect = "Allow", Principal = { AWS = "arn:aws:iam::${var.account_a_id}:role/eks-pod-identity-primary-role" }, Action = ["sts:TagSession"] } ] }) } resource "aws_iam_policy" "s3_read_policy" { name = "s3-read-only-for-target-role" policy = jsonencode({ Version = "2012-10-17", Statement = [ { Action = ["s3:GetObject", "s3:ListBucket"], Effect = "Allow", Resource = [ aws_s3_bucket.data_bucket.arn, "${aws_s3_bucket.data_bucket.arn}/*" ] } ] }) } resource "aws_iam_role_policy_attachment" "target_role_attachment" { role = aws_iam_role.target_role.name policy_arn = aws_iam_policy.s3_read_policy.arn } output "data_bucket_name" { description = "The name of the cross-account S3 data bucket." value = aws_s3_bucket.data_bucket.bucket }
Step 4: Deployment and Validation
To avoid IAM validation errors, you must deploy these resources in a specific, multi-step order. Before running the commands, be sure to edit the default
values in the variable blocks of your .tf
files.
- Initialize and Create the Primary Role: Navigate to your
account-a
directory. Initialize Terraform and then use thetarget
flag to create only the primary role.cd eks-cross-account/account-a terraform init terraform apply -target=aws_iam_role.primary_role
- Initialize and Deploy Account B: Navigate to the
account-b
directory. Initialize and runterraform apply
.cd ../account-b terraform init terraform apply
Note thedata_bucket_name
from the output. - Deploy the Remainder of Account A: Return to the
account-a
directory and run a fullterraform apply
to create the rest of the resources. You will need to pass in thedata_bucket_name
you just received.cd ../account-a terraform apply -var="data_bucket_name=the-bucket-name-from-output"
After deploying all steps, check the pod’s logs with kubectl logs s3-access-test-pod -n my-app-namespace
. A successful output listing the contents of your S3 bucket confirms the entire chain is working.
Conclusion: The Final Piece of the Puzzle
Across this three-part series, we’ve built a complete, modern framework for EKS IAM.
- In Part 1, we secured individual applications with least-privilege roles, moving away from risky static credentials.
- In Part 2, we extended that unified model to cluster add-ons, simplifying operational management.
- And now in Part 3, we have solved the complex cross-account access problem.
This latest enhancement is the final piece of the puzzle, transforming EKS Pod Identity into a comprehensive solution for virtually any permissions scenario. By abstracting away the complexity of role chaining, AWS empowers you to build secure, scalable, and maintainable multi-account EKS architectures. Adopting this unified model is the definitive standard for robust and simplified IAM in Amazon EKS.