
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.
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.
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"]
}
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
}
}
}
]
})
}
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 = "*"
}
]
})
}
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"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:
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 = "*"
}
]
})
}
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"