Johannes Eiglsperger

Zhuilu Suspension Bridge in Taroko Gorge

Use OpenID Connect to securely authenticate GitLab CI/CD with AWS

4 min read

Have you rotated your access keys today? If not, it is time to make your life easier and your GitLab pipelines more secure with GitLab’s built-in OpenID Connect (OIDC) provider.

Click here if you are using GitHub instead of GitLab.

Access keys are risky - they are long-lived secrets that can easily fall into the wrong hands (especially in public repositories). Creating, rotating, storing and revoking access keys can be time-consuming and error-prone. With OIDC, your pipelines request short-lived credentials from AWS at runtime using GitLab’s built-in OIDC provider.

sequenceDiagram
    participant Pipeline as GitLab
Pipeline participant IdP as GitLab
OIDC Provider participant IAM as AWS IAM IdP --> IAM: OIDC trust relationship Pipeline ->>+ IdP: Token request IdP -->>- Pipeline: Token Pipeline ->>+ IAM: Token,
IAM role ARN IAM -->>- Pipeline: Temporary credentials

Set up AWS IAM resources

You need two resources:

  1. AWS::IAM::OIDCProvider prepares AWS to use GitLab’s built-in OIDC provider.
  2. AWS::IAM::Role creates a trust relationship with the OIDC provider and grants access to your cloud resources using policies.

While you can create these resources manually using the AWS Console, many engineers prefer an Infrastructure-as-Code tool like AWS CDK or Terraform.

Both AWS CDK and Terraform provide excellent ways to define your cloud resources in code, making the code reusable and maintainable.

Terraform

This is how it can look like with Terraform:

resource "aws_iam_openid_connect_provider" "gitlab" {
  url             = "https://gitlab.com"
  client_id_list  = ["https://gitlab.com"]
}

resource "aws_iam_role" "gitlab_oidc_role" {
  name               = "GitLabOidcRole"
  assume_role_policy = data.aws_iam_policy_document.gitlab_oidc_role_trust_relationship.json
}

data "aws_iam_policy_document" "gitlab_oidc_role_trust_relationship" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    effect  = "Allow"
    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.gitlab.arn]
    }
    condition {
      test     = "StringEq"
      variable = "https://gitlab.com:aud"
      values   = "https://gitlab.com"
    }
    condition {
      test     = "StringLike"
      variable = "https://gitlab.com:sub"
      // Replace with your GitLab project path:
      values   = "project_path:<YOUR_GITLAB_GROUP>/<YOUR_PROJECT>:ref_type:*:ref:*"
    }
  }
}

resource "aws_iam_role_policy_attachment" "gitlab_oidc_s3_full_access" {
  role       = aws_iam_role.gitlab_oidc_role.name
  // Replace with the policies your workflow needs:
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}

AWS CDK for TypeScript

This is how it can look like with AWS CDK for TypeScript:

import { type App, Stack, type StackProps } from 'aws-cdk-lib';
import {
    OpenIdConnectProvider,
    Role,
    WebIdentityPrincipal,
    PolicyDocument,
    PolicyStatement
} from 'aws-cdk-lib/aws-iam';

class OidcIamRoleStack extends Stack {
    constructor(scope: App, id: string, props?: StackProps) {
        super(scope, id, props);

        const oidcProvider = new OpenIdConnectProvider(this, 'GitLabOidcProvider', {
            url: 'https://gitlab.com',
            clientIds: ['https://gitlab.com']
        });

        new Role(this, 'GitLabOidcRole', {
            assumedBy: new WebIdentityPrincipal(
                oidcProvider.openIdConnectProviderArn,
                {
                    StringEquals: {
                        'gitlab.com:aud': 'https://gitlab.com'
                    },
                    StringLike: {
                        // Replace with your GitLab project path:
                        'gitlab.com:sub': 'project_path:<YOUR_GITLAB_GROUP>/<YOUR_PROJECT>:ref_type:*:ref:*'
                    }
                }
            ),
            roleName: 'GitLabOidcRole',
            inlinePolicies: {
                GitLabCIPolicy: new PolicyDocument({
                    statements: [
                        // Replace with the policy statements your pipeline needs:
                        new PolicyStatement({
                            actions: ['s3:*'],
                            resources: ['arn:aws:s3:::<YOUR_BUCKET_NAME>/*']
                        })
                    ]
                })
            }
        });
    }
}

Self-hosted GitLab instances

If you are using a self-hosted GitLab instance, replace gitlab.com with the hostname of your instance. Note that the following paths must be accessible from the public Internet:

  • /.well-known/openid-configuration
  • The jwks_uri as described by /.well-known/openid-configuration

Apply further access restrictions

Following the principle of least privilege, you can further restrict the token subject (gitlab.com:sub). For example, you can restrict access to the main branch: project_path:<YOUR_GITLAB_GROUP>/<YOUR_PROJECT>:ref_type:branch:ref:main.

Deploy both IAM resources and note the IAM role ARN for the next step.

Update your GitLab CI/CD configuration

Now that you have the IAM role, update your GitLab CI/CD configuration (.gitlab-ci.yml) to use it with GitLab’s built-in AWS integration.

deploy:
  image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  environment:
    AWS_REGION: <YOUR_REGION> # Replace with your AWS region
  before_script:
    - >
      aws_sts_output=$(aws sts assume-role-with-web-identity
        --role-arn "<YOUR_ROLE_ARN>" # Replace with the IAM role ARN
        --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
        --web-identity-token "${GITLAB_OIDC_TOKEN}"
        --duration-seconds "${CI_JOB_TIMEOUT}"
        --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
        --output text)
    - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $aws_sts_output)
  script:
    - aws s3 sync ./public s3://<YOUR_BUCKET_NAME>

With OIDC, you are embracing a secure and flexible way to manage AWS access in your GitLab CI/CD pipelines. Now you can stop worrying about rotating your pipeline’s access keys and start focusing on what matters: Shipping great features.

Common problems

Error: Not authorized to perform sts:AssumeRoleWithWebIdentity

  • Is your assume role policy correct?
    • Does your token subject (sub) condition allow specific branches or tags?
    • Are you using wildcards and your condition function is StringEq instead of StrinkLike?

Error: Couldn't retrieve verification key from your identity provider

  • Is your self-hosted GitLab server accessible from the public Internet?
  • Does your GitLab server respond within 5 seconds?
  • Do you have an ingress controller or a web application firewall, which limits the number of requests to your GitLab server?
  • Is your GitLab server using either a self-signed TLS certificate or a certificate from an uncommon certificate authority?
Header image source: Balon Greyjoy via Wikimedia Commons, CC0