Skip to content
Vladimir on TwitterVladimir on LinkedIn

IAM Roles Anywhere

I have recently used AWS feature called “IAM Roles Anywhere” in order to access AWS services from another cloud. In this post I’ll outline the process and the experience.

Baseline: IAM Roles for Service Accounts

If you’re running Kubernetes inside AWS, you can use IAM Roles for Service Accounts, where you annotate your Kubernetes service accounts with the desired role, and magic happens.

For example, your K8S service account might look like this

apiVersion: v1
kind: ServiceAccount
metadata:
  name: ray
  namespace: ray
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::111111111111:role/ray

And the IAM role might have the following trust policy to allow access from K8S:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111111111111:oidc-provider/oidc.eks.eu-central-1.amazonaws.com/id/NNN"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.eu-central-1.amazonaws.com/id/NNN:sub": "system:serviceaccount:ray:ray"
        }
      }
    }
  ]  
}   

With this definitions, any pod using your service account automatically assume the specified IAM role&mdashvery easy and convenient.

Authorization for external workloads

If your workload run outside of AWS, the most common choice is to create a new user, give it the necessary permissions, and generate access key. However, if that key is compromised, an attacker can use the key with no contraints.

IAM Roles Anywhere aims to improve things. You need to have a public key CA, and you configure AWS to trust that CA. You then issue a X.509 certificate for your workload. There is an API call that allows to request session tokens for a specific IAM role, and if that API call is signed by the client certificate, IAM will accept it and issue the session token that can be used in further API calls.

IAM Roles Anywhere Walktrough

Create a Private CA

To speed up things, I created AWS Private CA. While it costs 400 dollars per months, if you never used it before, the first 30 days are free. I have created a new CA using appropriate names for company and business units, and used ECDSA P384 key algorith and SHA256 ECDSA signature algorithm. When you create a new CA, it has “Pending certificate” state, you must explicitly select “Get CA Certificate” from the actions menu. Finally, record the ARN for the created CA, as we’ll need it below.

Configure IAM Roles Anywhere

In the console, you need to create a “trust anchor” from your private CA, and then a “profile” that is associated with the desired IAM Role. This is well described in the documentation.

Create a client certificate.

Creating a client certificate is just a set of CLI commands

$ openssl ecparam -out ec_key.pem -name secp384r1 -genkey
$ openssl req -out csr.pem -new -key ec_key.pem -nodes -keyout private-key.pem
... input names for certificate ...
$ aws acm-pca issue-certificate \
      --certificate-authority ...CA ARN... \
      --csr fileb://csr.pem \
      --signing-algorithm "SHA256WITHECDSA" \
      --validity Value=365,Type="DAYS"
... outputs certificate ARN ...
aws acm-pca get-certificate \
      --certificate-arn ...Certificate ARN...
      --certificate-authority-arn ...CA ARN... | \
      jq -r '.Certificate' > certificate.pem

Check authentication with the certificate

The AWS SDK does not directly support using IAM Roles Anywhere. You need to run a separate utility, aws_signing_helper that will obtain session token. The utility can be downloaded from the releases page.

Then, run

./aws_signing_helper credential-process \
    --certificate certificate.pem --private-key ec_key.pem \
    --trust-anchor-arn  ...Trust Anchor ARN... \
    --profile-arn ...IAM Anywhere Profile... \
    --role-arn ...Role ARN...

If everything is well, the above should output JSON that can be used for authentication.

Kubernetes setup

Finally, we’re ready to configure Kubernetes workloads.

Create a secret with the certificate:

kubectl -n ray create secret generic iam-ra \
    --from-file=certificate.pem=./certificate.pem --from-file=key.pem=./private-key.pem

Then, deploy the following, admittedly huge, pod definition:

apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-config
  namespace: ray
data:
  config: |
    [default]
    region = eu-central-1
    credential_process = /root/shared/aws_signing_helper credential-process
      --certificate /root/.certificates/certificate.pem
      --private-key /root/.certificates/key.pem
      --trust-anchor-arn ....
      --profile-arn ....
      --role-arn ....
---
apiVersion: v1
kind: Pod
metadata:
  name: checker
  namespace: ray
spec:
  containers:
  - name: app-container
    image: public.ecr.aws/aws-cli/aws-cli:2.22.32
    command: ['sh', '-c']
    args: ['aws sts get-caller-identity; echo The app is running! && sleep 3600']
    imagePullPolicy: Always
    volumeMounts:
    - name: certificates
      mountPath: /root/.certificates
      readOnly: true
    - name: aws-config
      mountPath: /root/.aws/config
      subPath: config
      readOnly: true
    - name: shared
      mountPath: /root/shared
      readOnly: true
  initContainers:
   - name: aws-credentials-setup
     image: amazon/aws-cli:2.22.32
     command:
     - /bin/sh
     - -c
     - |
       # Install rolesanywhere-credential-process
       curl -o /root/shared/aws_signing_helper https://rolesanywhere.amazonaws.com/releases/1.0.6/X86_64/Linux/aws_signing_helper
       chmod +x /root/shared/aws_signing_helper
     volumeMounts:
     - name: shared
       mountPath: /root/shared
       readOnly: false
  volumes:
    - name: aws-config
      configMap:
        name: aws-config
    - name: certificates
      secret:
        secretName: iam-ra
        items:
        - key: certificate.pem
          path: certificate.pem
        - key: key.pem
          path: key.pem
    - name: shared
      emptyDir: {}

Here is what we do above:

It should be noted that we use a specific version of the credentials helper, as the latest version of the aws-cli docker image and the helper are not compatible.

If everything is OK, the pod will report its newly obtained identity.

Conclusions

While the path above led us to some success, I would say that overall the developer experience here is very convoluted

The last point is the most critical—if you have 100 pods, editing them all to add additional volumes or init containers is very complicated, especially as some pods will be created programmatically.

The overall conclusion is that:

Hope this helps, and let me know if you have any comments.