Setup cross-account IAM permissions for EKS using OIDC and IRSA

Using your cluster’s OIDC provider simplifies cross-account permissions compared to the older, chained AssumeRole method. We hope our example of applying this in an AWS GovCloud scenario helps with your own projects.

You can use your EKS cluster’s OIDC provider to easily support cross-account permissions using the familiar IAM Roles for Service Accounts (IRSA) pattern.

At Pelotech, we recently had a client with a use case that required cross account permissions for their EKS cluster. The AWS Blog has a great post with an example about this scenario, and here we will share another example of how we used this pattern to connect external-dns running in a GovCloud EKS cluster to a public account’s AWS Route53 service. As always, we’ll use Terraform to provision the necessary AWS resources.


The Scenario

We have an EKS cluster running in a private AWS GovCloud account with endpoints that need to be accessible from the public internet. Since AWS GovCloud Route53 does not support public hosted zones, we need to grant our cluster’s external-dns service with the necessary permissions to create entries in our public AWS account’s Route53. Another key consideration is that GovCloud doesn’t not support cross account IAM roles, so using the OIDC approach is our best option compared to other solutions like long lived keys.


Step 1: Set up the OIDC provider:

Our public account needs to register the OIDC provider from the GovCloud cluster to allow cross account assume role operations. We do this by creating an aws_iam_openid_connect_provider resource in the public account with the URL and thumbprint of our GovCloud account’s cluster OIDC provider. Follow these instructions for creating the cluster OIDC provider if it doesn’t already exist. To obtain the provider’s thumbprint, follow these steps from AWS.

resource "aws_iam_openid_connect_provider" "gov_eks_oidc" {
  provider        = aws.public_account
  url             = "<< OIDC issuer URL of govcloud EKS cluster >>"
  thumbprint_list = ["<< Thumbprint of EKS OIDC issuer >>"]
  client_id_list  = ["sts.amazonaws.com"]
}

Step 2: Create IAM role with trust relationship

Create the IAM role in the public account which external-dns will assume. Note that we are using the OIDC provider from Step 1 in the OIDC claims.

resource "aws_iam_role" "external_dns_assumed_role" {
  provider = aws.public_account
  name = "external-dns-assumed-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.gov_eks_oidc.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition : {
          StringEquals : {
            "${trimprefix(aws_iam_openid_connect_provider.gov_eks_oidc.url, "https://")}:aud" : "sts.amazonaws.com"
            "${trimprefix(aws_iam_openid_connect_provider.gov_eks_oidc.url, "https://")}:sub" : "system:serviceaccount:external-dns:external-dns-controller" # This needs to match the external-dns namespace/SA name in the gov cluster
          }
        }
      }
    ]
  })
}

Step 3: Create and attach IAM policy with Route53 Permissions

Create the policy with the necessary Route53 permissions and attach it to the role which external-dns will assume:

resource "aws_iam_role_policy" "external_dns_policy" {
  provider = aws.public_accout
  role   = aws_iam_role.external_dns_assumed_role.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = ["route53:ChangeResourceRecordSets"]
        Effect   = "Allow"
        Resource = "arn:aws:route53:::hostedzone/*"
      },
      {
        Action = ["route53:ListHostedZones", "route53:ListResourceRecordSets"]
        Effect   = "Allow"
        Resource = "*"
      }
    ]
  })
}

Step 4: Add IRSA Annotation to external-dns Service Account

Now all we have to do is add the usual IRSA annotations to the external-dns Service Account, but this time note that we can directly use the account ID of the public account:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns-controller
  namespace: external-dns
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<< PUBLIC_ACCOUNT_ID >>:role/external-dns-assumed-role
    eks.amazonaws.com/sts-regional-endpoints: "true"


Summary

Using your cluster’s OIDC provider makes it easier to grant cross-account permissions to resources in your cluster compared with the older approach of using chained AssumeRole operations. We hope that our example of deploying this approach to a common scenario when using AWS GovCloud helps you in your own projects.

With the Pelotech crew, we’re on the next versions of dev concepts which allow for complete self-hosted apps, deployments which are based on high-availability, high-scalability, and simple blue/green style deployments. Hit me up if you’re interested in more info about it!

Sean Morton is a Lead Engineer at http://www.pelotech.com, a group that helps organizations improve their dev practices and culture.

— — —

Here is the full code from this example for your convenience:

Public account Terraform:

resource "aws_iam_openid_connect_provider" "gov_eks_oidc" {
  provider        = aws.public_account
  url             = "<< OIDC issuer URL of govcloud EKS cluster >>"
  thumbprint_list = ["<< Thumbprint of EKS OIDC issuer >>"]
  client_id_list  = ["sts.amazonaws.com"]
}

resource "aws_iam_role" "external_dns_assumed_role" {
  provider = aws.public_account
  name = "external-dns-assumed-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.gov_eks_oidc.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition : {
          StringEquals : {
            "${trimprefix(aws_iam_openid_connect_provider.gov_eks_oidc.url, "https://")}:aud" : "sts.amazonaws.com"
            "${trimprefix(aws_iam_openid_connect_provider.gov_eks_oidc.url, "https://")}:sub" : "system:serviceaccount:external-dns:external-dns-controller" # This needs to match the external-dns namespace/SA name in the gov cluster
          }
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "external_dns_policy" {
  provider = aws.public_accout
  role   = aws_iam_role.external_dns_assumed_role.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = ["route53:ChangeResourceRecordSets"]
        Effect   = "Allow"
        Resource = "arn:aws:route53:::hostedzone/*"
      },
      {
        Action = ["route53:ListHostedZones", "route53:ListResourceRecordSets"]
        Effect   = "Allow"
        Resource = "*"
      }
    ]
  })
}

GovCloud EKS Service Account:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns-controller
  namespace: external-dns
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<< PUBLIC_ACCOUNT_ID >>:role/external-dns-assumed-role
    eks.amazonaws.com/sts-regional-endpoints: "true"

Let’s Get Started

Ready to tackle your challenges and cut unnecessary costs?
Let’s talk about the right solutions for your business.
Contact us