Creating service account IAM roles on EKS
Tags: eks, kubernetes, iam, relax
Published: 2020-12-17 14:22:32

---
title: Creating service account IAM roles on EKS
published: true
description: Automate setup of service account roles on EKS
tags: eks, kubernetes, iam, relax
//cover_image: https://direct_url_to_image.jpg
---

As of the time of this writing AWS still seems to be working on their not so streamlined IAM for Service Accounts feature.

Its not difficult to get running but you have to jump through quite a few hoops especially when it comes to the command line.

If you do everything with `eksctl` or the AWS Console, then its really easy.  If you didn't use `eksctl` to build your cluster and would like to make things simple and script it... well that's a different story.

## What needs to happen

- Create an OpenIDConnect Provider in IAM based off your EKS cluster's issuer URL.  This requires the following.
    - EKS cluster's issuer URL
    - ClientID string: "sts.amazonaws.com"
    - EKS cluster's domain footprint
- Role for attaching to a service account.  This requires the following.
    - IAM Policy(s)
    - trust relationship document
- Create/Annotate your pod's service account

## Whats the problem

Some of the requirements listed above are not so straightforward to obtain or setup.  In particular two of them come to mind.

- EKS cluster's domain footprint
- Role trust relationship document

lets take a look at these.

### Domain Footprint

This was the biggest issue I faced... and purely because the [documentation for obtaining this](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html) in anyway other than the AWS Console is incorrect.  In particular (2) in the linked document returns the following error.

```json
{"message":"Missing Authentication Token"}
```

I tried for sometime to accomplish (2) but I eventually gave up and searched for another way.  In my search I came across an [issue in terraforms-provider-aws](https://github.com/hashicorp/terraform-provider-aws/issues/10104#issuecomment-659892848) That gave me the clue I needed and I was finally able to move on after writing a function to not only grab the footprint but create the IAM provider if not exists.

```bash
function create_provider_if_not_exists() {
    local PROVIDER_LIST PROVIDER_ARN THUMBPRINT
    PROVIDER_LIST=$(aws iam list-open-id-connect-providers --query OpenIDConnectProviderList --output text)
    PROVIDER_ARN=$(for PROVIDER in ${PROVIDER_LIST}; do
        if [[ "${PROVIDER}" == *"${2}"* ]]; then
            echo "${PROVIDER}"
            return
        fi
    done)
    if ! [ "${PROVIDER_ARN}" ]; then
        FOOTPRINT=$(echo | openssl s_client -servername "${3}" -showcerts -connect "${3}":443 2>&- \
            | tac | sed -n '/-----END CERTIFICATE-----/,/-----BEGIN CERTIFICATE-----/p; /-----BEGIN CERTIFICATE-----/q' \
            | tac | openssl x509 -fingerprint -sha1 -noout \
            | sed 's/://g' | awk -F= '{print tolower($2)}')
        aws iam create-open-id-connect-provider \
            --url "${1}" \
            --client-id-list "sts.amazonaws.com" \
            --thumbprint-list "${FOOTPRINT}" \
            --query OpenIDConnectProviderArn
    fi
}
```

### Role Trust Relationship

This wasn't particularly hard, just annoying if not automated.  I used [AWS documentation here](https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html) and created a function to create and destroy the required document.

```bash
function create_trust_relationship_file() {
    local TRUST_RELATIONSHIP
    read -r -d '' TRUST_RELATIONSHIP <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${1}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${1#*/}:sub": "system:serviceaccount:${3}:${2}"
        }
      }
    }
  ]
}
EOF
    echo "${TRUST_RELATIONSHIP}" > trust.json
}
```

```bash
function delete_trust_relationship_file() {
    rm -f trust.json
}
```

## Complete Script

With that finished I just needed to fill in the blanks and got the following which creates required resources and gives you the service annotation to add to your service account.

```bash
function create_eks_pod_role() {
    local CLUSTER_NAME ROLE_NAME ROLE_DESCRIPTION CONTAINER_POLICY_ARN SERVICE_ACCOUNT_NAME
    local ISSUER_ADDRESS ISSUER_ID ISSUER_DOMAIN PROVIDER_ARN ROLE_ARN NAMESPACE
    function usage() {
        echo "Outputs rendered files for specified chart into current directory.
           ${FUNCNAME[0]} -
                   [-h] print this help
                   -c   Name of the EKS Cluster
                   -r   Name of the IAM role to create
                   -d   Description for IAM role
                   -p   IAM policy ARN of to attach to the role
                   -s   Name of the service account
                   -n   Namespace of the service account";
    }

    function check_policy_exists() {
        local POLICY_LIST POLICY
        POLICY_LIST="$(aws iam list-policies --query Policies[].Arn --output text)"
        for POLICY in ${POLICY_LIST}; do
            if [[ "${POLICY}" == "${1}" ]]; then
                return 0
            fi
        done
        echo "INVALID: your policy arn seems to be invalid please verify"
        return 1
    }

    function create_provider_if_not_exists() {
        local PROVIDER_LIST PROVIDER_ARN THUMBPRINT
        PROVIDER_LIST=$(aws iam list-open-id-connect-providers --query OpenIDConnectProviderList --output text)
        PROVIDER_ARN=$(for PROVIDER in ${PROVIDER_LIST}; do
            if [[ "${PROVIDER}" == *"${2}"* ]]; then
                echo "${PROVIDER}"
                return
            fi
        done)
        if ! [ "${PROVIDER_ARN}" ]; then
            FOOTPRINT=$(echo | openssl s_client -servername "${3}" -showcerts -connect "${3}":443 2>&- \
                | tac | sed -n '/-----END CERTIFICATE-----/,/-----BEGIN CERTIFICATE-----/p; /-----BEGIN CERTIFICATE-----/q' \
                | tac | openssl x509 -fingerprint -sha1 -noout \
                | sed 's/://g' | awk -F= '{print tolower($2)}')
            aws iam create-open-id-connect-provider \
                --url "${1}" \
                --client-id-list "sts.amazonaws.com" \
                --thumbprint-list "${FOOTPRINT}" \
                --query OpenIDConnectProviderArn
        else
            echo "${PROVIDER_ARN}"
        fi
    }

    function create_trust_relationship_file() {
        local TRUST_RELATIONSHIP
        read -r -d '' TRUST_RELATIONSHIP <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${1}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${1#*/}:sub": "system:serviceaccount:${3}:${2}"
        }
      }
    }
  ]
}
EOF
        echo "${TRUST_RELATIONSHIP}" > trust.json
    }

    function delete_trust_relationship_file() {
        rm -f trust.json
    }

    # Set options
    while getopts ':c:r:d:p:s:n:' OPTION; do
        case "$OPTION" in
            c)
                CLUSTER_NAME="${OPTARG}"
            ;;
            r)
                ROLE_NAME="${OPTARG}"
            ;;
            d)
                ROLE_DESCRIPTION="${OPTARG}"
            ;;
            p)
                CONTAINER_POLICY_ARN="${OPTARG}"
            ;;
            s)
                SERVICE_ACCOUNT_NAME="${OPTARG}"
            ;;
            n)
                NAMESPACE="${OPTARG}"
            ;;
            *)
                usage;
                return 1;
            ;;
        esac;
    done;

    [ "${CLUSTER_NAME}" ] && \
    [ "${ROLE_NAME}" ] && \
    [ "${ROLE_DESCRIPTION}" ] && \
    [ "${CONTAINER_POLICY_ARN}" ] && \
    [ "${SERVICE_ACCOUNT_NAME}" ] && \
    [ "${NAMESPACE}" ] \
    || { usage; return 1; }

    if ! check_policy_exists "${CONTAINER_POLICY_ARN}"; then
        return 1
    fi

    ISSUER_ADDRESS=$(aws eks describe-cluster --name "${CLUSTER_NAME}" --query "cluster.identity.oidc.issuer" --output text)
    ISSUER_ID="${ISSUER_ADDRESS#https://}"
    ISSUER_DOMAIN="${ISSUER_ID%%/*}"
    PROVIDER_ARN=$(create_provider_if_not_exists "${ISSUER_ADDRESS}" "${ISSUER_ID}" "${ISSUER_DOMAIN}")
    create_trust_relationship_file "${PROVIDER_ARN}" "${SERVICE_ACCOUNT_NAME}" "${NAMESPACE}"
    ROLE_ARN="$(aws iam create-role --role-name "${ROLE_NAME}" \
                        --assume-role-policy-document file://trust.json \
                        --description "${ROLE_DESCRIPTION}" \
                        --output text --query Role.Arn)"
    delete_trust_relationship_file
    aws iam attach-role-policy --role-name "${ROLE_NAME}" --policy-arn="${CONTAINER_POLICY_ARN}"
    echo "コンテナーのサービスアカウントの annotations に下記を追加"
    echo "    eks.amazonaws.com/role-arn: ${ROLE_ARN}"
}
```

Some example output:

```
$ AWS_DEFAULT_REGION=us-east-1 create_eks_pod_role -c TestCluster -r loki-s3-role-test-2 -d "test role for loki s3 integration" -p arn:aws:iam::aws:policy/AmazonS3FullAccess -s a9t-loki -n monitoring-system
コンテナーのサービスアカウントの annotations に下記を追加
    eks.amazonaws.com/role-arn: arn:aws:iam::636082426924:role/loki-s3-role-test-2

```

Don't mind the Japanese it just states that the below is to be added to the annotations of your pods service account.

## Conclusion

In conclusion, it was kind of a pain to create this script.. but its definitely simplifies my work-flow when creating roles for my pods.  Feel free to use it if you like, and don't hesitate to let me know if you have any questions or concerns!