Learn GitLab CI/CD - Passwordless Cloud Authentication Using OIDC (OpenID Connect)
Episode 13 of 21

Learn GitLab CI/CD - Passwordless Cloud Authentication Using OIDC (OpenID Connect)

Cloud credentials stored permanently in CI/CD variables are a time bomb waiting to explode. This episode explains how to replace them with OpenID Connect — the pipeline requests a short-lived token directly from GitLab to authenticate itself to AWS, GCP, and Azure without storing a single long-term secret.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In the previous episode 12 we covered native DevSecOps — including Secret Detection catching credentials leaked into repositories. But there's one credential source that isn't visible in the repository: CI/CD variables permanently storing cloud access keys. In episode 5 we covered storing variables with the Masked and Protected flags — but those only slow down theft, not prevent it.

The old pattern for authenticating to the cloud from a pipeline always ends up like this: someone creates a dedicated IAM user, leaves its access key attached to the CI/CD settings, and that key lives for months. A single key like that is enough to tempt an attacker — and when it leaks, the whole cloud account is at stake. It's exactly like giving one house key valid for life to everyone passing by: convenient, but once lost, every door opens.

This episode introduces a far safer approach: passwordless cloud authentication using OIDC (OpenID Connect). The pipeline no longer stores permanent credentials — it requests a short-lived token directly from GitLab for each pipeline, and that token is only valid for minutes. Let's break it down.

Main Discussion

The Risks of Long-Lived Keys in CI/CD Variables

Before discussing the solution, let's be honest about the problem. Storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as permanent CI/CD variables brings three major risks:

  1. An extremely long exposure window. Keys are valid for weeks to months. Anyone who gets access once — via logs, exfiltration, or a malicious contributor — can use that key for months undetected.
  2. No context. An IAM key carries no information about which pipeline used it. There's no audit trail linking an API call to a specific job or commit.
  3. Revocation is expensive. Revoking a key scattered across many projects means editing every project one by one — and one is likely to be missed.

Warning

Never store long-lived cloud credentials — especially the AWS Secret Access Key — as permanent CI/CD variables. Masking only hides the value in logs; it doesn't prevent theft from inside a runner or from access that's already leaked. OIDC removes the need for those permanent credentials entirely.

How OIDC Works in GitLab

OIDC is a protocol that's already everywhere — you use it every time you log into a website with "Sign in with Google". In the CI/CD context, the flow looks like this:

  1. You define id_tokens in a job.
  2. When the job runs, the GitLab server issues a JWT (JSON Web Token) containing claims about that job — project path, branch, commit SHA, and the aud value you set.
  3. The job sends this JWT to the cloud provider (AWS, GCP, or Azure).
  4. The cloud provider verifies the JWT's signature against GitLab's public keys (JWKS) and checks claims like aud and project path.
  5. If valid, the provider returns temporary credentials that only live for a few minutes to a few hours.

The key is the JWT: this token is short-lived, bound to a specific pipeline context, and never stored anywhere. GitLab acts as the identity provider — just like Google or Okta, but specifically for pipelines.

Configuring id_tokens in .gitlab-ci.yml

The configuration is just an id_tokens block at the job, pipeline, or default level. The important value is aud — the audience that must match what's configured in the cloud provider.

Defining an OIDC token in a job
deploy-to-aws:
  stage: deploy
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  script:
    - echo "JWT tersedia di GITLAB_OIDC_TOKEN"
    - echo "${#GITLAB_OIDC_TOKEN} karakter token"

Once defined, the JWT is accessible as the shell variable GITLAB_OIDC_TOKEN inside the job — available only to the job that defines it. This is what gets sent to the cloud provider to exchange for temporary credentials. Note: if the variable name ends in _FILE (e.g. GITLAB_OIDC_TOKEN_FILE), GitLab writes the token to a file and that variable holds its path — useful for tools that need a file-form token.

Tip

The aud value is free-form, but make sure it's consistent between GitLab and the cloud provider. Many teams use https://gitlab.com for GitLab.com or their self-hosted instance URL. Some providers use aud to distinguish which pipelines are allowed to use which role.

AWS Integration: STS AssumeRoleWithWebIdentity

On AWS, the flow uses an IAM Role + the Security Token Service (STS). You create an IAM role, configure GitLab as a web identity provider in IAM, then the pipeline exchanges its JWT for that role's credentials.

The AWS role trust policy (condensed) ensures only tokens from the correct GitLab project can use the role:

IAM trust policy for GitLab OIDC
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/gitlab.com"
      },
      "Action": "sts:TagSession",
      "Condition": {
        "StringEquals": {
          "gitlab.com:aud": "https://gitlab.com"
        }
      }
    }
  ]
}

Inside the job, the pipeline exchanges the JWT for role credentials using the AWS CLI:

Deploy job with AWS STS
deploy:
  image: amazon/aws-cli:latest
  stage: deploy
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  script:
    - aws sts assume-role-with-web-identity \
        --role-arn arn:aws:iam::123456789012:role/gitlab-deployer \
        --role-session-name gitlab-pipeline \
        --web-identity-token "$GITLAB_OIDC_TOKEN" \
        --duration-seconds 3600

The temporary credentials from this assume-role can be exported to the environment and used by subsequent AWS commands, then die on their own after 1 hour. No permanent access key is stored in GitLab.

GCP Integration: Workload Identity Federation

Google Cloud uses Workload Identity Federation for the same purpose. The concept: you create a workload identity pool and a provider that trusts GitLab as an identity provider, then connect it to a service account. Because GCP requires the token in file form, the job uses the id_tokens variable convention ending in _FILE — GitLab writes the JWT to a file, and the variable holds that file's path.

Creating temporary credentials from a JWT
gcloud iam workload-identity-pools create-cred-config \
  projects/123456789/locations/global/workloadIdentityPools/ci-pool/providers/gitlab-provider \
  --service-account=gitlab-deployer@my-project.iam.gserviceaccount.com \
  --subject-token-type=urn:ietf:params:oauth:token-type:jwt \
  --credential-source-file="$GITLAB_OIDC_TOKEN_FILE" \
  --output-file="$CI_PROJECT_DIR/gcp-credential.json"
 
gcloud auth login --cred-file="$CI_PROJECT_DIR/gcp-credential.json"

The common flow: gcloud ... create-cred-config produces a temporary credential file from the JWT, then gcloud auth login --cred-file uses it. The result — GCP access without any stored service account key.

Azure Integration: Federated Identity Credentials

Azure uses federated identity credentials in Azure AD. You create an app registration or user-assigned managed identity, add a federation credential that trusts GitLab, then the pipeline logs in with that JWT token:

Logging into Azure with a federated token
az login \
  --service-principal \
  --username "$AZURE_CLIENT_ID" \
  --tenant "$AZURE_TENANT_ID" \
  --federated-token "$GITLAB_OIDC_TOKEN"
ProviderMechanismCredentials stored
AWSIAM Role + STS AssumeRoleWithWebIdentityNone
GCPWorkload Identity FederationNone
AzureFederated Identity CredentialsNone

The same pattern, different language: define id_tokens in GitLab, then exchange the JWT for temporary credentials from each provider.

Common OIDC Mistakes

MistakeSymptomSolution
Forgetting to restrict claims in the trust policyOther projects can use the roleTighten aud, project path, and branch in conditions
Using the deprecated $CI_JOB_JWTTokens expire unexpectedlyUse id_tokens with an explicit aud
Still storing an access key as a fallbackLong-lived key remainsRemove the key and use full OIDC
Token too permissiveWider access than neededUse minimal roles/scopes per job
Not testing rotationDeployment suddenly fails when keys rotateOIDC has no static keys; verify periodically

Important

OIDC eliminates static credentials, but it's not without configuration. The final security lies in the trust policy — restrict the provider to trust only the correct projects, branches, and environments. A valid token is still useless if the trust policy conditions are properly narrowed.

Closing

In this episode we covered the risks of storing long-lived cloud credentials in CI/CD variables — long exposure windows, no context, and expensive revocation; how OIDC works in GitLab with id_tokens issuing short-lived JWTs; and integration with the three major cloud providers — AWS via STS AssumeRoleWithWebIdentity, GCP via Workload Identity Federation, and Azure via federated identity credentials — all without storing a single permanent secret.

The core of this episode: credentials that are never stored can't be stolen. With OIDC, the pipeline proves its identity via a one-shot, short-lived, context-bound token, replacing the permanent keys that have been the biggest weak point.

In the next episode 14 we'll manage what gets deployed to that cloud: Environments, Deployments & Manual Approval Gates — declaring environments, deployment history in the GitLab UI, manual approval gates for production, protected environments, and Review Apps. See you there!

Learn GitLab CI/CD - Passwordless Cloud Authentication Using OIDC (OpenID Connect) | Learn GitLab CI/CD