# GitLab CI OIDC without Premium: id_tokens on the Free tier

> The secrets keyword needs GitLab Premium, but id_tokens works on Free. How to exchange a job's ID token for AWS, Google Cloud, Azure or Crunu by hand.

URL: https://crunu.com/blog/gitlab-ci-oidc-without-premium
Language: en
Updated: 2026-10-30

Yes, you can use OIDC from GitLab CI on the Free tier. The `secrets:` keyword, which fetches values from Vault, cloud secret managers or GitLab's own Secrets Manager for you, is listed for Premium and Ultimate only. But `id_tokens`, which gives a job a signed JWT, is available on Free, Premium and Ultimate. With an ID token, the job can exchange the JWT for short-lived credentials itself: one command for AWS, two HTTP calls for Google Cloud, one `az login` for Azure. The cloud's trust policy decides which projects and branches may do that.

This post shows the split between the two features, the exchange for each cloud, how to keep tokens narrow, and how the same JWT binds a Crunu service token to a project and branch. The guide to [CI/CD secrets with OIDC](https://crunu.com/guides/ci-cd-secrets-with-oidc) compares all CI systems and clouds.

## What's Premium and what isn't

GitLab's documentation marks the tiers on each feature:

- **`secrets:`** is "Tier: Premium, Ultimate". It lets a job name secrets to "retrieve from an external secrets provider" and receive them as CI/CD variables, with integrations for HashiCorp Vault, Google Cloud Secret Manager, Azure Key Vault and AWS Secrets Manager ([GitLab: external secrets](https://docs.gitlab.com/ci/secrets/)). GitLab Secrets Manager, built on OpenBao and generally available since GitLab 19.5, is also Premium and Ultimate.
- **`id_tokens`** is "Tier: Free, Premium, Ultimate" ([GitLab: OIDC authentication using ID tokens](https://docs.gitlab.com/ci/secrets/id_token_authentication/)). The `secrets:` integrations use ID tokens under the hood, and GitLab notes you can "use ID tokens to manually authenticate with any secrets provider that supports OIDC authentication with JSON web tokens (JWT)."

So on Free you lose the declarative syntax, not the security model. The cloud still checks a GitLab-signed token, and no long-lived cloud key needs to sit in CI/CD variables.

The old predefined token is gone. GitLab's AWS page warns that "`CI_JOB_JWT_V2` was removed in GitLab 17.0"; pipelines that used it need `id_tokens` now ([GitLab: OIDC in AWS](https://docs.gitlab.com/ci/cloud_services/aws/)).

## Declaring an ID token

A job asks for a token by name, with an audience:

```yaml
deploy:
  id_tokens:
    AWS_ID_TOKEN:
      aud: sts.amazonaws.com
  script:
    - ./deploy.sh
```

GitLab puts the JWT in the variable `AWS_ID_TOKEN` for that job. The default subject is `project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}`, and the token expires "at the job's timeout if specified, or 5 minutes if no timeout is specified."

## AWS: one command

Register GitLab as an OIDC provider in IAM, create a role whose trust policy names your project and branch, then exchange the token in the job. This is GitLab's example, with the audience set to `sts.amazonaws.com`:

```yaml
assume role:
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: sts.amazonaws.com
  script:
    - >
      aws_sts_output=$(aws sts assume-role-with-web-identity
      --role-arn ${ROLE_ARN}
      --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
      --web-identity-token ${GITLAB_OIDC_TOKEN}
      --duration-seconds 3600
      --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)
    - aws sts get-caller-identity
```

GitLab's documentation says the audience "typically matches the audience value configured in your IAM OIDC identity provider (often `sts.amazonaws.com` or your GitLab instance URL)", and notes that using the GitLab URL "might work" but "is semantically misleading", because the audience should represent the service that accepts the token.

For the trust policy, GitLab recommends pinning stable IDs as well as the path. On GitLab.com, AWS accepts `namespace_id` and `project_id` as condition keys, and `project_id` "remains the same for the entire lifetime of the project, including across group renames, project renames, and project transfers." On GitLab Self-Managed and Dedicated, only `sub` and `aud` are supported as AWS condition keys.

```json
"Condition": {
  "StringEquals": {
    "gitlab.com:sub": "project_path:kiteline/web-app:ref_type:branch:ref:main",
    "gitlab.com:aud": "sts.amazonaws.com",
    "gitlab.com:project_id": "67890"
  }
}
```

Kiteline is a fictional company; use your own project path and ID.

*Figure: Diagram of a GitLab CI job signing in to AWS STS with OIDC. Step 1: the job asks the GitLab CI issuer for an ID token. Step 2: the issuer signs a JWT naming the repository kiteline/web-app and the branch main. Step 3: the job presents the JWT to AWS STS, which checks the issuer, the audience, the repository and the branch. Step 4: AWS STS returns short-lived credentials, and no long-lived cloud key is stored in CI.*

## Google Cloud: two HTTP calls

Google Cloud uses Workload Identity Federation: a workload identity pool with a provider for GitLab, and a service account the pool may impersonate. GitLab's documentation shows the exchange with `curl`: the job first sends its ID token to `https://sts.googleapis.com/v1/token` with a token-exchange grant and gets a federated token, then calls the IAM Credentials API's `generateAccessToken` for the service account and gets an OAuth access token ([GitLab: OIDC with GCP Workload Identity Federation](https://docs.gitlab.com/ci/cloud_services/google_cloud/)). The result can be passed to `gcloud` "by setting the environment variable `CLOUDSDK_AUTH_ACCESS_TOKEN`."

The restriction to a project and branch lives in the provider's attribute condition and in which principals may impersonate the service account.

## Azure: one login

For Azure, you create an Entra ID application with a federated identity credential that names the GitLab subject. The job then signs in with the token:

```yaml
auth:
  image: mcr.microsoft.com/azure-cli:latest
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  script:
    - az login --service-principal -u $AZURE_CLIENT_ID -t $AZURE_TENANT_ID --federated-token $GITLAB_OIDC_TOKEN
    - az account show
```

That example comes from GitLab's Azure page; the audience must match the one in the federated credential ([GitLab: OIDC in Azure](https://docs.gitlab.com/ci/cloud_services/azure/)).

## Self-managed GitLab: the issuer has to be reachable

Every service that checks a GitLab ID token fetches GitLab's public signing keys from the issuer, which is the address of your GitLab instance. For AWS, GitLab's documentation says the provider URL "must be publicly accessible." For a private instance it describes a workaround, introduced in GitLab 18.1, that copies the OpenID configuration and public keys to a public location such as an S3 bucket, with a warning that a badly secured bucket "could lead to the takeover of any cloud accounts associated with this OpenID Connect identity."

The same applies to Crunu: on a self-managed GitLab, the binding's issuer is your instance's address, and the Crunu server must be able to fetch that instance's keys through OIDC discovery to check the JWT. If it can't reach them, the sign-in fails rather than skipping the check.

## Keep each token narrow

Three habits apply whatever the cloud.

- **One audience per service.** GitLab explains that services "can be configured to reject tokens that do not have an `aud` claim matching their bound audience", and to "use this functionality to reduce the number of services with which a token can authenticate." A job that talks to AWS and to Crunu should declare two tokens with two audiences, not one shared one.
- **Only the jobs that need it.** `id_tokens` is per job. Don't put it in `default:` unless every job really needs cloud access.
- **Branch and environment in the trust policy.** A role that deploys production should require `ref:main`, and on GitLab.com you can add `ref_protected` too. A policy that matches only the project path accepts any branch anyone with push access can create.

The [OIDC policy builder](https://crunu.com/tools/oidc-policy-builder) drafts these policies, and the glossary entry on [JWT](https://crunu.com/learn/jwt) explains the token itself.

## When Premium's `secrets:` is worth it

The manual exchange is a few lines of script per cloud, and scripts drift. On Premium or Ultimate, `secrets:` turns them into configuration: a job names the secret, and GitLab fetches it and exposes it as a file-type CI/CD variable by default, so it isn't printed by accident in an environment dump. If you already run Vault or a cloud secret manager and have many projects, that consistency is the real benefit. On Free, put the exchange in one shared template that every project includes, and review it like code.

## The same JWT, bound to a Crunu service token

Crunu uses the GitLab ID token in the same way: as proof of which job is calling. A pipeline signs in to Crunu with a service token stored in `CRUNU_TOKEN`. When that token has a **CI binding (OIDC)**, the server accepts it only together with a JWT from GitLab whose claims match the binding's rules, and a replayed JWT is refused. A typical rule for production: provider GitLab, `project_path: kiteline/web-app`, `ref: main`, `ref_type: branch`, `environment: production`, with the Crunu server address as the audience.

```yaml
deploy:
  image: node:24
  environment: production
  id_tokens:
    CRUNU_ID_TOKEN:
      aud: https://app.crunu.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script:
    - crunu run --env web-app/production -- npm run deploy
```

`crunu` reads `CRUNU_ID_TOKEN` by itself. This works on GitLab Free, because it uses `id_tokens`, not `secrets:`. The Crunu template file includes a `.crunu-oidc` job that installs a pinned `crunu`, checks the SHA-256 of the installer and the binary, and declares the ID token for you; the [GitLab CI integration](https://crunu.com/integrations/gitlab-ci) and the [GitLab CI docs](https://crunu.com/docs/ci/gitlab-ci) cover installation.

*Figure: Diagram of a GitLab CI job signing in to Crunu with OIDC. Step 1: the job asks the GitLab CI issuer for an ID token. Step 2: the issuer signs a JWT naming the repository kiteline/web-app and the branch main. Step 3: the job presents the JWT to Crunu, which checks the issuer, the audience, the repository and the branch. Step 4: Crunu rejects a replayed token, then returns ciphertext and the environment key sealed to the token; the values are decrypted in memory on the runner, and the CRUNU_TOKEN stored in CI is useless without the JWT.*

A copied `CRUNU_TOKEN` then doesn't work from a laptop or another project, because it needs a fresh JWT from that project's pipeline on that branch. The values it decrypts are still visible to the job's own commands, so keep each token's grant to the environments its job needs.

## Checklist

- [ ] Replace `CI_JOB_JWT_V2` with `id_tokens` in every pipeline
- [ ] One ID token per service, each with its own `aud`
- [ ] Trust policies require the project and the branch, plus `project_id` on GitLab.com
- [ ] `id_tokens` only on the jobs that need it
- [ ] No long-lived cloud keys left in CI/CD variables after the switch
- [ ] Crunu tokens that read production have a CI binding

## Sources

- [Use external secrets in CI/CD, GitLab Docs](https://docs.gitlab.com/ci/secrets/), checked October 30, 2026.
- [OpenID Connect (OIDC) authentication using ID tokens, GitLab Docs](https://docs.gitlab.com/ci/secrets/id_token_authentication/), checked October 30, 2026.
- [Configure OpenID Connect in AWS to retrieve temporary credentials, GitLab Docs](https://docs.gitlab.com/ci/cloud_services/aws/), checked October 30, 2026.
- [Configure OpenID Connect with GCP Workload Identity Federation, GitLab Docs](https://docs.gitlab.com/ci/cloud_services/google_cloud/), checked October 30, 2026.
- [Configure OpenID Connect in Azure to retrieve temporary credentials, GitLab Docs](https://docs.gitlab.com/ci/cloud_services/azure/), checked October 30, 2026.
