external-dns: AWS cross account access with OIDC provider in EKS & externalDNS not working

Hello to everyone,

I hope this is the right place to ask my question. If not please point me to the correct place or documentation.

We are in the process of setting up an AWS EKS cluster that uses the OIDC provider for IAM access control to AWS resources. So far this works fine for the ALB ingresss controler. Now we want to also add externalDNS to create DNS records in Route53 for zones hosted in another AWS account.

We have a MGMT AWS account (ID: 222233334444) that is hosting the Route53 DNS zones and a test stage AWS account (ID: 999988887777) where we run the EKS cluster. Now we want to setup externalDNS for a cross account access to add/change the DNS entries hosted in the MGMT account. See below for the config files and error message we get.

Please let me know if I missed something or you need more information.

Michael

====== config details ====== When I try to setup externalDNS I get the following error:

deployment.apps/external-dns created
serviceaccount/external-dns created
clusterrole.rbac.authorization.k8s.io/external-dns created
clusterrolebinding.rbac.authorization.k8s.io/external-dns-viewer created

kubectl logs -n kube-system external-dns-XYZ
time="2020-05-28T16:14:23Z" level=info msg="config: {Master: KubeConfig: RequestTimeout:30s IstioIngressGatewayServices:[] ContourLoadBalancerService:heptio-contour/contour SkipperRouteGroupVersion:zalando.org/v1 Sources:[service ingress] Namespace: AnnotationFilter: FQDNTemplate: CombineFQDNAndAnnotation:false IgnoreHostnameAnnotation:false Compatibility: PublishInternal:false PublishHostIP:false AlwaysPublishNotReadyAddresses:false ConnectorSourceServer:localhost:8080 Provider:aws GoogleProject: GoogleBatchChangeSize:1000 GoogleBatchChangeInterval:1s DomainFilter:[test.local] ExcludeDomains:[] ZoneIDFilter:[] AlibabaCloudConfigFile:/etc/kubernetes/alibaba-cloud.json AlibabaCloudZoneType: AWSZoneType:private AWSZoneTagFilter:[] AWSAssumeRole:\"arn:aws:iam::222233334444:role/externalDNSAccessRoute53CrossAccountTest\" AWSBatchChangeSize:1000 AWSBatchChangeInterval:1s AWSEvaluateTargetHealth:true AWSAPIRetries:3 AWSPreferCNAME:false AzureConfigFile:/etc/kubernetes/azure.json AzureResourceGroup: AzureSubscriptionID: AzureUserAssignedIdentityClientID: CloudflareProxied:false CloudflareZonesPerPage:50 CoreDNSPrefix:/skydns/ RcodezeroTXTEncrypt:false AkamaiServiceConsumerDomain: AkamaiClientToken: AkamaiClientSecret: AkamaiAccessToken: InfobloxGridHost: InfobloxWapiPort:443 InfobloxWapiUsername:admin InfobloxWapiPassword: InfobloxWapiVersion:2.3.1 InfobloxSSLVerify:true InfobloxView: InfobloxMaxResults:0 DynCustomerName: DynUsername: DynPassword: DynMinTTLSeconds:0 OCIConfigFile:/etc/kubernetes/oci.yaml InMemoryZones:[] OVHEndpoint:ovh-eu PDNSServer:http://localhost:8081 PDNSAPIKey: PDNSTLSEnabled:false TLSCA: TLSClientCert: TLSClientCertKey: Policy:upsert-only Registry:txt TXTOwnerID:\"test account\" TXTPrefix: Interval:1m0s Once:false DryRun:false UpdateEvents:false LogFormat:text MetricsAddress::7979 LogLevel:debug TXTCacheInterval:0s ExoscaleEndpoint:https://api.exoscale.ch/dns ExoscaleAPIKey: ExoscaleAPISecret: CRDSourceAPIVersion:externaldns.k8s.io/v1alpha1 CRDSourceKind:DNSEndpoint ServiceTypeFilter:[] CFAPIEndpoint: CFUsername: CFPassword: RFC2136Host: RFC2136Port:0 RFC2136Zone: RFC2136Insecure:false RFC2136TSIGKeyName: RFC2136TSIGSecret: RFC2136TSIGSecretAlg: RFC2136TAXFR:false RFC2136MinTTL:0s NS1Endpoint: NS1IgnoreSSL:false TransIPAccountName: TransIPPrivateKeyFile:}"
time="2020-05-28T16:14:23Z" level=info msg="Instantiating new Kubernetes client"
time="2020-05-28T16:14:23Z" level=debug msg="kubeMaster: "
time="2020-05-28T16:14:23Z" level=debug msg="kubeConfig: "
time="2020-05-28T16:14:23Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2020-05-28T16:14:23Z" level=info msg="Created Kubernetes client https://172.20.0.1:443"
time="2020-05-28T16:14:25Z" level=info msg="Assuming role: \"arn:aws:iam::222233334444:role/externalDNSAccessRoute53CrossAccountTest\""
time="2020-05-28T16:14:26Z" level=error msg="AccessDenied: User: arn:aws:sts::999988887777:assumed-role/externalDNSEKSrole/${ID} is not authorized to perform: sts:AssumeRole on resource: \"arn:aws:iam::222233334444:role/externalDNSAccessRoute53CrossAccountTest\"\n\tstatus code: 403, request id: ${REQUESt_ID}"

IAM Role for the test account:


Trust relationships
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::999988887777:oidc-provider/oidc.eks.eu-central-1.amazonaws.com/id/${OIDC_ID}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.eu-central-1.amazonaws.com/id/${OIDC_ID}:sub": "system:serviceaccount:kube-system:external-dns",
          "oidc.eks.eu-central-1.amazonaws.com/id/${OIDC_ID}:aud": "sts.amazonaws.com"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::999988887777:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Attached IAM policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": [
                "arn:aws:iam::222233334444:role/externalDNSAccessRoute53CrossAccountTest
            ]
        }
    ]
}

IAM Role for the MGMT account:


Trust relationships
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::999988887777:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {}
    }
  ]
}

Attached IAM policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "route53:ChangeResourceRecordSets"
            ],
            "Resource": [
                "arn:aws:route53:::hostedzone/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "route53:ListHostedZones",
                "route53:ListResourceRecordSets"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

Kubernetes YAML configuration for externalDNS:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      automountServiceAccountToken: true
      containers:
      - name: external-dns
        image: registry.opensource.zalan.do/teapot/external-dns:v0.7.1
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=test.local
        - --provider=aws
        - --aws-assume-role="arn:aws:iam::222233334444:role/externalDNSAccessRoute53CrossAccountTest"
        - --policy=upsert-only
        - --aws-zone-type=private
        - --registry=txt
        - --txt-owner-id="test account"
        - --log-level=debug
      securityContext:
        fsGroup: 65534
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::999988887777:role/externalDNSEKSrole
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns
rules:
- apiGroups: [""]
  resources: ["services","endpoints","pods"]
  verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
  resources: ["ingresses"]
  verbs: ["get","watch","list"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["list","watch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: external-dns-viewer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns
subjects:
- kind: ServiceAccount
  name: external-dns
  namespace: kube-system

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 8
  • Comments: 27 (2 by maintainers)

Most upvoted comments

I tested it in our setup and it seems to work fine. Here’s what I did.

Let’s assume we want to run ExternalDNS in account A managing records in account B. We want to use the IAM role in account A arn:aws:iam::A:role/external-dns to assume the IAM role in account B arn:aws:iam::B:role/external-dns-cross-account.

Head over to account B (the target account) and create the IAM role you want to assume. In the following this target IAM role is denoted with the ARN arn:aws:iam::B:role/external-dns-cross-account.

Then go back to AWS account A and annotate ExternalDNS’ ServiceAccount in cluster A with:

eks.amazonaws.com/role-arn: arn:aws:iam::A:role/external-dns

Note, this should already be the case in your setup.

Then start ExternalDNS in cluster A with:

- --aws-assume-role=arn:aws:iam::B:role/external-dns-cross-account

For the IAM role for ExternalDNS in cluster A, add the permission to assume the role in account B:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::B:role/external-dns-cross-account"
        }
    ]
}

Normally this role would have Route53 permissions to manage DNS records in account A but that wasn’t needed. For the OIDC stuff to work you need to keep the Trust Relationship to the OIDC provider and the StringsEqual stuff. This should already be there and can be left unchanged.

Then go to the IAM role in account B that you created at the beginning and that you want to assume. As a reminder this is denoted as ARN arn:aws:iam::B:role/external-dns-cross-account.

Give it permission to manage DNS records just like the IAM role for ExternalDNS in account A normally has, e.g.:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "route53:*",
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

Then add a Trust Relation to the IAM role in account A with:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::A:role/external-dns"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Note that I removed the Trust Relationship to The identity provider(s) ec2.amazonaws.com as well. It works either way and I guess it’s better to just keep it then.

Note, the IAM role in account B can be a “plain” IAM role. It must have permissions to modify DNS records as well as a Trust Relationship back to account A. If you want to assume another ServiceAccount’s IAM role, such as ExternalDNS’ role in a different account, then the role will also have the Trust Relationship to the OIDC provider. Both cases worked fine for me.

In my setup the target account has its own Kubernetes cluster with its own OIDC provider. I’m certain that this doesn’t interfere with the experiment but who knows. Unfortunately, I can’t delete it at the moment without causing some disruption.

To summarize you basically need to:

  • Make sure that IAM role A works fine with your OIDC setup within account A
  • add a permission to IAM role A to allow to assume IAM role B
  • add a trust relationship on IAM role B stating IAM role A
  • configure ExternalDNS’ ServiceAccount to use IAM role A
  • start ExternalDNS and tell it to --aws-assume-role IAM role B

This was tested with v0.7.1 and v0.7.2.

Let me know if that helped you or if you still get stuck.

In my case I had to remove this part, because if it was non-empty it seemed to skip IRSA mechanism and ignored annotation placed on the K8s service account - --aws-assume-role="arn:aws:iam::222233334444:role/externalDNSAccessRoute53CrossAccountTest"

I deploy external-dns with chart version 5.4.15, app 0.10.1. I am deploying using Helm. Specifically I had to remove Helm param: aws.assumeRoleArn but leave serviceAccount.annotations.eks.amazonaws...

At the end, if I describe the external-dns Deployment object, it doesnt have any ARN set in any param. ARN of the role is set only on the service account

Just in case anyone comes across this issue trying to do the same thing. I found a way easier way to do this. Following this guide I was able to get it to work with ODIC and without needing roles on the AWS account with the EKS Cluster. See: https://aws.amazon.com/blogs/containers/cross-account-iam-roles-for-kubernetes-service-accounts/

I have it completely working in terraform with 2 copies of external-dns running, one for each account. You don’t even need to use the assume role option in external-dns.

To add my 5 cents here.

I have done exactly the same steps as @linki described and run into the problem anyway (access denied, cannot assume role, as above). The cause was that I have provided the role ARN to be assumed between quotation marks (e.g. --aws-assume-role=“arn:aws:iam::123456789012:role/role-to-be-assumed”). So basically, there is an error in args in the comment above, which cause the problem @michazt reported.

After removing the quotation masks, everything works fine.

That doc pretty much covers it: https://aws.amazon.com/ru/blogs/containers/cross-account-iam-roles-for-kubernetes-service-accounts/

Pay attention to:

  1. Create an IAM OIDC provider in the shared_content account. The Provider URL corresponds to OpenID Connect provider URL from the EKS cluster in the developer account

To add my 5 cents here.

I have done exactly the same steps as @linki described and run into the problem anyway (access denied, cannot assume role, as above). The cause was that I have provided the role ARN to be assumed between quotation marks (e.g. --aws-assume-role=“arn:aws:iam::123456789012:role/role-to-be-assumed”). So basically, there is an error in args in the comment above, which cause the problem @michazt reported.

After removing the quotation masks, everything works fine.

we just spent 4 hours on this and this was the issue.

@linki the ALB basically has the same IAM setup. The difference between the ALB ingress controller and externalDNS is that externalDNS needs to do a cross account access to create the DNS record. The resources the ALB creates are in the same AWS account.

From the various documentations and hints I have read externalDNS can do a cross account access or can use the OIDC provider. But I never found something where people use both. That’s why I think there is a problem with the combination cross account access and OIDC provider. Or we simply do not understand how to set this up.

As AWS is promoting the OIDC provider for EKS more and more people will use this to authenticate a Kubernetes Cluster against AWS IAM. And I think it will be helpful for others to have a working example in the docs for this.