GitLab: pipeline authentication to AWS using OIDC

GitLab Pipelines provide the ability to define a build workflow, which per your requirements may include calls to AWS infrastructure.

However, for those AWS calls to work you need to establish an IAM identity.   In the past, you would have to rely on AWS access keys in CI/CD variables, or applying an IAM role to the compute of a self-managed GitLab Runner.

A better way is to use the GitLab OIDC integration, which generates a signed JWT token that can be validated by the AWS STS and exchanged for short-lived credentials when the pipeline runs.

 

Overview

As a prerequisite, a trusted IAM Identity Provider is created on the AWS side for gitlab.com and retrieves the root CA certificate thumbprint that is used for verification of signed JWT coming from GitLab.

An IAM role that governs the permissions on a set of AWS resources is created and lists this Identity Provider as its principal.  This role defines conditionals on the incoming JWT claim such as audience, repository, and branch.

Then when a GitLab pipeline is run, a JWT token signed by GitLab is injected into the job context.  This is sent to the AWS STS (Security Token Service) for authentication.  In OIDC terms, GitLab is the Identity Provider and AWS is the Relying Party.

If the JWT claims match the IAM role conditions AND is signed by GitLab, short-lived AWS credentials are provided back to the GitLab pipeline action.

This set of short-lived credentials can then be used to invoke AWS service calls, limited by the policy permissions of the assumed IAM role.

 

AWS infrastructure

As a prerequisite, we need to create the AWS resources that support this OIDC federated trust:

  • AWS Identity Provider
  • IAM Role
  • IAM Policy

This could be done manually or via AWS CLI calls, but we will use Terraform in this article.  Here is the full terraform definitions for the snippets below.

Create Identity Provider

The aws_iam_openid_connect_provider is used to create the AWS Identity Provider.

data "tls_certificate" "gitlab" {
  url = "https://gitlab.com/oauth/discovery/keys"
}

resource "aws_iam_openid_connect_provider" "gitlab" {
  url             = "https://gitlab.com"
  client_id_list  = ["https://sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.gitlab.certificates.0.sha1_fingerprint]
}

The ‘client_id_list’ is the ‘Audience’ identifier. This is a unique identifier (not an actual URL target), and should be the URL of the entity evaluating the JWT, “https://sts.amazonaws.com”.

Create IAM Role

Create an IAM role that uses the newly created Identity Provider as a principal, and allows AssumeRoleWithWebIdentity when the audience and GitLab repository project path match.

resource "aws_iam_role" "gitlab_ci_readwrite" {
  name = "gitlab-oidc-role-s3-readwrite"

  # IAM 'trust relationship' tab - who can assume this role
  assume_role_policy = jsonencode({
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${aws_iam_openid_connect_provider.gitlab.arn}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "gitlab.com:aud": "https://sts.amazonaws.com"
        },
        "StringLike": {
          "gitlab.com:sub": [
            "project_path:gitlab-pipeline7091038/aws-auth/gitlab-pipeline-aws-oidc-auth:ref_type:branch:ref:*""
          ]
        }
      }
    }
  ]
  })

} # aws_iam_role

The ‘StringLike’ and wildcard “*” usage allows for any branch in this repository to assume the IAM role. The value “gitlab-pipeline7091038/aws-auth/gitlab-pipeline-aws-oidc-auth” is clearly the path to my example repository and needs to be changed to match your environment.

Associate IAM policy with role

Assign the IAM role policy that defines which AWS resources and permissions are being granted to the IAM role.  In this case, we grant full privileges to S3 storage.

resource "aws_iam_role_policy" "gitlab_readwrite_policy" {
  name = "gitlab_oidc_readwrite_policy"
  role = aws_iam_role.gitlab_ci_readwrite.id

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": "*"
        }
    ]
  })

} # aws_iam_role_policy

Running example terraform

Use the Terraform scripts from my GitLab example project.

git clone https://gitlab.com/gitlab-pipeline7091038/aws-auth/gitlab-pipeline-aws-oidc-auth.git
cd gitlab-pipeline-aws-oidc-auth/aws-infra

# manually change terraform.tfvars to match your GitLab repository

# establish aws credentials
aws login

# create AWS resources described above (Identity Provider, IAM)
terraform init
terraform plan
# note the ouput IAM role ARN, which is needed by workflow
terraform apply

GitLab Workflow

On the GitLab side, now we need to craft a GitLab workflow definition that get the GitLab JWT injected into an action, does the OIDC exchange with AWS, and uses the returned short-lived credentials to make general AWS API calls.

Template job for AWS authentication

By creating a template job in our .gitlab-ci.yml (job beginning with period), we can easily reuse this logic in multiple actions.

# template job to capture boilerplate steps of calling AWS api
.aws-login:
  image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: $OIDC_AUDIENCE
  before_script: |
    mkdir -p ~/.aws
    echo "${GITLAB_OIDC_TOKEN}" > /tmp/web_identity_token
    echo -e "[default]\nregion=${AWS_REGION}\nrole_arn=${AWS_ROLE_ARN}\nweb_identity_token_file=/tmp/web_identity_token" > ~/.aws/config

The template above has the GitLab runner inject a JWT token with a specific OIDC_AUDIENCE into the job context, which then allows us to create a basic AWS configuration file at  “~/.aws/config”.  This file location and format are what is expected by the AWS CLI.

Job that uses AWS authentication

An job can then extend this template like below to enable AWS authentication, and then make AWS CLI calls to create S3 buckets, upload files to S3, etc.

test-aws:
  stage: build
  extends: .aws-login # include template that takes care of AWS auth
  script: |
    aws s3 mb s3://$S3_BUCKET_NAME --region $AWS_REGION || true # create s3 bucket
    echo "pipeline: $CI_PIPELINE_ID job: $CI_JOB_NAME date: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" > test.txt
    aws s3 cp test.txt s3://$S3_BUCKET_NAME/test.txt
    echo "just UPLOADED test.txt to s3 bucket $S3_BUCKET_NAME"

The AWS CLI picks up the “~/.aws/config” file, because this is its known default location.

Here is the full .gitlab-ci.yml example.

Create workflow variables

There are several variables used in the workflow definition above that need to be set at the GitLab repository/group level.  Setting them as variables (instead of hardcoding into the workflow) allows more flexibility.

In GitLab, navigate to Settings > CI/CD > Variables, “Add variable”.

Create the following variables:

  • AWS_REGION = us-east-1 or any other region where you want S3 bucket created
  • AWS_ROLE_ARN = arn:aws:iam::xxxxxxx:role/gitlab-oidc-role-s3-readwrite (role ARN to be assumed)
  • OIDC_AUDIENCE = https://sts.amazonaws.com

Invoke workflow

You can invoke the example GitLab workflow either by pushing to the repository, or navigating to Build > Pipelines, and “New pipeline”.

The invoked jobs will exercise the JWT token injection, OIDC exchange,  then use the short-lived credentials to make calls to S3 as a test.

Navigating to Amazon S3 >Buckets will show the new bucket and “test.txt” content just uploaded by the GitLab job.

 

 

REFERENCES

GitLab, Configure OpenID Connect in AWS

GitLab docs, Configure a conditional role with OIDC claims

GitLab docs, OpenID Connect (OIDC) Authentication using ID Tokens

AWS, Setting up OpenID Connect with GitLab CI/CD

AWS create-open-id-connect-provider

Firefly, Why use OIDC for secure GitLab CI/CD

Configure OpenID Connect (OIDC) between AWS and GitLab

github docs, openid-connect explanation

GitLab docs, Use OpenID Connect as auth provider

GitLab docs, Test OIDC/OAuth with your client app

GitLab issues #17073, AWS has pre-loaded CA for GitLab trust

GitLab repo example of terraform code to create OIDC and IAM policy and document

Calvine Otieno, Terraform Pipeline with Gitlab CI and OpenID Connect

Yakuphan, Integrating GitLab CI/CD Pipelines with AWS using OIDC

Dhruv Mavani, GitLAB CI/CD Pipeline for AWS EKS deployment with AWS OIDC (custom gitlab runner)

Gav Harris, Secure CI with GitLab and HashiCorp Vault

Secure your Terraform deployment on AWS with GitLab-CI and Vault (pipeline side)

 

 

 

 

NOTES

gitlab discovery endpoint
https://gitlab.com/.well-known/openid-configuration

get public key location

curl https://gitlab.com/.well-known/openid-configuration -s | jq .jwks_uri -r
# get hex encoded sha1 hash of cert
echo quit | openssl s_client -connect gitlab.com:443 2>/dev/null | openssl x509 -noout -fingerprint | cut -d= -f2 | sed -e s/://g | tr '[:upper:]' '[:lower:]'