IAM roles with Kubernetes service accounts using EKS OIDC provider

2 minute read

In this post, I’m going to describe how to setup the OIDC provider in AWS and use IAM roles on Kubernetes service accounts. This will allow your services to access AWS resources using roles instead of access keys. No need for user accounts or key rotations! I’m going to go through all the Terraform code, but it can be referenced here in the EKS module. This doesn’t cover setting up an EKS cluster; however, you can see how to do that here.

After creating your EKS cluster, setting up the OIDC provider is easy and after creating this, you can locate in on the IAM console page. Look for providers.

Terraform resources

1data "tls_certificate" "cluster" {
2  url = aws_eks_cluster.cluster.identity[0].oidc[0].issuer
3}
4
5resource "aws_iam_openid_connect_provider" "cluster" {
6  client_id_list  = ["sts.amazonaws.com"]
7  thumbprint_list = [data.tls_certificate.cluster.certificates[0].sha1_fingerprint]
8  url             = data.tls_certificate.cluster.url
9}

Next I’m going to through an example app that uses a role with the OIDC provider. Here the OIDC provider is allowed to assume the role and a basic policy that allows access to an s3 bucket.

data.tf

1data "aws_caller_identity" "current" {}

iam.tf

 1locals {
 2  irsa_oidc_provider_url = replace(var.irsa_oidc_provider_arn, "/^(.*provider/)/", "")
 3  account_id             = data.aws_caller_identity.current.account_id
 4}
 5
 6data "aws_iam_policy_document" "assume_role" {
 7  statement {
 8    effect  = "Allow"
 9    actions = ["sts:AssumeRoleWithWebIdentity"]
10
11    principals {
12      type        = "Federated"
13      identifiers = [var.irsa_oidc_provider_arn]
14    }
15    condition {
16      test     = "StringEquals"
17      variable = "${local.irsa_oidc_provider_url}:sub"
18      values   = ["system:serviceaccount:${var.env}:${var.name}"]
19    }
20    condition {
21      test     = "StringEquals"
22      variable = "${local.irsa_oidc_provider_url}:aud"
23      values   = ["sts.amazonaws.com"]
24    }
25  }
26}
27
28resource "aws_iam_role" "role" {
29  name               = "${var.name}-${var.env}"
30  assume_role_policy = data.aws_iam_policy_document.assume_role.json
31  managed_policy_arns = [
32    aws_iam_policy.role.arn
33  ]
34  tags = {
35    service   = var.name
36  }
37}
38
39resource "aws_iam_policy" "role" {
40  name = "${var.name}-${var.env}"
41  policy = jsonencode({
42    "Version" : "2012-10-17",
43    "Statement" : [
44      {
45        Effect : "Allow",
46        Action : [
47          "s3:GetObject",
48          "s3:ListBucket",
49        ],
50        Resource : [
51          "${module.s3_bucket.arn}/*",
52          module.s3_bucket.arn
53        ]
54      }
55    ]
56  })
57}

Demo

The next step is to tell our Kubernetes service account to use this specific role when accessing AWS resources by adding an annotation with the ARN of the role we just created.

 1apiVersion: v1
 2kind: ServiceAccount
 3metadata:
 4  name: test-app
 5  namespace: sandbox
 6  labels:
 7    helm.sh/chart: test-app-0.1.0
 8    app.kubernetes.io/version: "1.0.0"
 9    app.kubernetes.io/managed-by: Helm
10    app.kubernetes.io/name: test-app
11    app.kubernetes.io/instance: test-app
12  annotations:
13    eks.amazonaws.com/role-arn: arn:aws:iam::<account_id>:role/test-app-sandbox
14automountServiceAccountToken: true