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).

image 12

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.

  1. 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).
  2. 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.
  3. 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 the eks-pod-identity-primary-role (the Primary Role) and, crucially, that a target_role_arn is also specified, pointing to the s3-reader-target-role in Account B.
  4. 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.
  5. 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.”
  6. 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).
  7. 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.
  8. The Secure Injection (Account A): The agent securely receives these final credentials and injects them into the application pod’s environment.
  9. 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.
image 13

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:

  1. Terraform Provider Version: You must use version 6.2.0 or higher of the Terraform AWS provider. This version introduced the target_role_arn argument, which is the correct and required way to configure the cross-account association in the aws_eks_pod_identity_association Terraform resource. Version 6.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.
  2. 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.
  3. Primary Role Trust Policy: The primary role (in Account A) must trust the pods.eks.amazonaws.com service principal with both sts:AssumeRole and sts:TagSession permissions. Missing sts:TagSession will cause the Pod Identity agent to fail.
  4. Target Role Trust Policy Format: While standard IAM policy allows combining actions in an array, we found that explicitly separating sts:AssumeRole and sts:TagSession into two distinct statements in the target role’s trust policy is more reliable and is thus the recommended approach.
  5. 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.
  6. 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.

  1. Initialize and Create the Primary Role: Navigate to your account-a directory. Initialize Terraform and then use the target flag to create only the primary role. cd eks-cross-account/account-a terraform init terraform apply -target=aws_iam_role.primary_role
  2. Initialize and Deploy Account B: Navigate to the account-b directory. Initialize and run terraform apply. cd ../account-b terraform init terraform apply Note the data_bucket_name from the output.
  3. Deploy the Remainder of Account A: Return to the account-a directory and run a full terraform apply to create the rest of the resources. You will need to pass in the data_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.

Leave a Comment