Skip to content
3 min read Moustakime KIFIA

GitLab OIDC to AWS: the trust policy and the CI job

How we connected GitLab CI to AWS via OIDC, with a tight trust policy, one role for all projects, and the ACLSizePerRole limit we almost hit.

  • CI/CD
  • Auth
  • Operations
GitLab OIDC to AWS: the trust policy and the CI job

This topic can quickly become verbose because it mixes IAM, CI and security. This article does the opposite: show the minimal path that works, then say where it breaks — including the AWS limit we did not see coming.

The pattern in five lines

The core setup:

  • a GitLab OIDC provider registered in IAM
  • a trust policy with StringLike on gitlab.com:sub
  • an aud set to sts.amazonaws.com
  • assume-role-with-web-identity in the CI job
  • one shared role across multiple projects, with short sessions (1h)

The reader should be able to reconstruct the setup without reading a 40-page AWS guide.

The Terraform trust policy

# cicd.tf
locals {
  gitlab_build_subjects = [
    "project_path:keyson/cercly/*:ref_type:branch:ref:main",
    "project_path:keyson/cercly/*:ref_type:branch:ref:develop",
    "project_path:keyson/cercly/*:ref_type:tag:ref:*",
  ]
}

data "aws_iam_policy_document" "gitlab_build_assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.gitlab.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "gitlab.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "gitlab.com:sub"
      values   = local.gitlab_build_subjects
    }
  }
}

The StringLike with wildcard * covers all projects under keyson/cercly/ without enumerating each repo.

The CI job

# templates/docker-build.yml
.assume_aws_role: &assume_aws_role
  - >
    export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
    $(aws sts assume-role-with-web-identity
    --role-arn "$AWS_BUILD_ROLE_ARN"
    --role-session-name "gitlab-build-${CI_PROJECT_NAME}-${CI_PIPELINE_ID}"
    --web-identity-token "$CI_JOB_JWT_V2"
    --duration-seconds 3600
    --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]"
    --output text))

No static credentials. The OIDC token is provided by GitLab for each job via $CI_JOB_JWT_V2, exchanged for temporary AWS credentials valid for 1 hour.

The limit we did not see coming

We had initially listed each project explicitly in the trust policy:

gitlab_build_subjects = [
  "project_path:keyson/cercly/apis/operator-api:ref_type:branch:ref:main",
  "project_path:keyson/cercly/apis/platform-api:ref_type:branch:ref:main",
  "project_path:keyson/cercly/bff/member-bff:ref_type:branch:ref:main",
  # ... 8 more projects
]

When we added portal and mobile, the CI deploy failed with:

LimitExceeded: Cannot exceed quota for ACLSizePerRole: 2048

AWS limits the total size of trust policies to 2048 characters per role. With long project paths multiplied by 3 authorized branches (main, develop, tags), we hit the limit with 5-6 projects.

The fix: replace enumeration with a wildcard on the namespace:

gitlab_build_subjects = [
  "project_path:keyson/cercly/*:ref_type:branch:ref:main",
  "project_path:keyson/cercly/*:ref_type:branch:ref:develop",
  "project_path:keyson/cercly/*:ref_type:tag:ref:*",
]

Three lines instead of thirty. All current and future projects under keyson/cercly/ are covered. The * wildcard in StringLike is natively supported by IAM.

The only guard lost: a project outside the keyson/cercly/ namespace can no longer slip in by mistake. But it is the same GitLab namespace — if someone has access to the namespace, they have access to the projects.

The operational checklist

To add a new project:

  1. Create the GitLab project under keyson/cercly/
  2. Add the ECR repo in Terraform (var.ecr_services)
  3. Apply Terraform — the ECR repo and permissions are created
  4. Push a first image — it works immediately

No need to touch the trust policy for each new project.

In hindsight

A good OIDC integration is mostly judged by its readability when adding one more project. The enumeration version looked precise and secure. It was mostly fragile: each new project was an opportunity to forget a line, hit the limit, or plan a Terraform apply before the first push.

The wildcard is less explicit. It is more robust.

Keep reading

A few related articles to extend the topic and keep the internal linking strong.

5 min

A simple map of Cercly's SaaS architecture

A reading of Cercly's SaaS architecture focused on application boundaries: shared design system, separate frontends, dedicated BFFs, business APIs and shared infrastructure.

  • Architecture
  • Engineering
  • Product
Read the note