# CI/CD secrets with OIDC: GitHub, GitLab, Bitbucket

> How CI jobs reach AWS, Google Cloud, Azure and your vault with short-lived OIDC tokens instead of stored keys, and which claims to pin so a leaked token fails.

URL: https://crunu.com/guides/ci-cd-secrets-with-oidc
Language: en
Updated: 2026-10-30

Let the CI provider issue each job a short-lived signed token, and have AWS, Google Cloud, Azure or your vault accept that token only for one repository, branch and deployment environment. Nothing long-lived then sits in CI settings, and a token copied from a log is useless outside that job.

Stop storing cloud keys and long-lived tokens in CI settings. Instead, let the CI provider issue each job a short-lived token signed with its own key, and configure AWS, Google Cloud, Azure or your vault to accept that token only for a specific repository, branch and deployment environment. The job exchanges the token for temporary access, nothing reusable is stored, and a token copied out of a build log fails anywhere else. This guide covers how that exchange works, the claims GitHub Actions, GitLab CI and Bitbucket Pipelines put in their tokens, a working trust setup for each major cloud, and how to apply the same idea to secrets that aren't cloud keys.

## How OIDC works in a CI job

OpenID Connect is a standard for one party to vouch for an identity with a signed token. In CI the provider vouches for the job. The exchange has four steps.

1. **The job asks for a token.** GitHub Actions issues one only to jobs that have the `id-token: write` permission. GitLab issues one for each entry under `id_tokens` in the job. Bitbucket issues one to steps with OIDC turned on.
2. **The provider signs a JWT.** The token names its issuer, its audience, and a subject, plus claims that describe the job: the repository, the branch or tag, the deployment environment, the pipeline.
3. **The job presents the JWT.** The receiving service fetches the provider's public keys, checks the signature and expiry, and compares the claims with its own rules.
4. **The service grants short-lived access.** A cloud returns temporary credentials. A vault returns the secrets the rule allows.

*Figure: Diagram of a GitHub Actions job signing in to AWS STS with OIDC. Step 1: the job asks the GitHub Actions 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.*

The tokens are short-lived by design. GitLab, for example, sets the expiry to the job's timeout, or five minutes if none is set, per its [ID token documentation](https://docs.gitlab.com/ci/secrets/id_token_authentication/). Because nothing in CI settings can be reused later, the leak that stored keys allow, a value copied once and used for months, goes away.

What OIDC doesn't change is what a legitimate job can do. That comes up again at the end of the guide.

## The claims each CI puts in its token

Trust rules are only as good as the claims they check. These are the ones you will use.

|                          | GitHub Actions                                                             | GitLab CI                                                         | Bitbucket Pipelines                                                 |
| ------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
| How the job gets a token | `permissions: id-token: write`                                             | `id_tokens:` with an `aud` per token                              | `oidc: true` on the step; token in `BITBUCKET_STEP_OIDC_TOKEN`      |
| Issuer                   | `https://token.actions.githubusercontent.com`                              | the GitLab instance, `https://gitlab.com` on GitLab.com           | per workspace, shown under the repository's OpenID Connect settings |
| Default audience         | the URL of the repository owner                                            | the instance's domain, unless you set `aud`                       | the workspace's audience, unless you add your own                   |
| Default subject          | `repo:ORG/REPO:ref:refs/heads/BRANCH`, or `…:environment:NAME`             | `project_path:GROUP/PROJECT:ref_type:branch:ref:BRANCH`           | set by Bitbucket; pin the UUID claims rather than the subject       |
| Claims worth pinning     | `repository`, `ref`, `environment`, `repository_id`, `repository_owner_id` | `project_path`, `ref`, `ref_type`, `ref_protected`, `environment` | repository UUID, branch, deployment environment UUID                |

A few details from each provider's documentation change how you write rules.

**GitHub changed its default subject in 2026.** Per the [GitHub OIDC reference](https://docs.github.com/en/actions/reference/security/oidc), repositories created after July 15, 2026 use an immutable default subject that includes both the owner ID and the repository ID, such as `repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main`. Older repositories keep the previous format unless the organization or repository opts in, and renames and transfers after that date also move to the new format. A trust policy that matches `repo:octo-org/octo-repo:…` silently stops matching for a new repository. The IDs exist for a reason: unlike names, they can't be reused by someone who registers a deleted name.

**GitHub's subject depends on the trigger.** If a job references an environment, the subject uses the environment. A pull request without an environment gets `repo:ORG/REPO:pull_request`. Only otherwise does it use the branch or tag. A rule that expects `ref:refs/heads/main` will not match a job that deploys to the `production` environment, and that is a feature: it lets you tie production access to an environment with protection rules.

**GitLab lets you choose the audience per token.** Declare one token per receiving service with its own `aud`, so a token minted for your cloud can't be replayed against your vault. ID tokens are listed for GitLab's Free, Premium and Ultimate tiers.

**Bitbucket shares one default audience across the workspace.** Every service that trusts the workspace audience accepts the same token. Add your own under `oidc: audiences:` in the step; [Atlassian's guide](https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/) allows up to 10 audiences of up to 150 characters each.

## GitHub Actions to AWS

Example. Kiteline is a fictional company. The examples in this guide use its repository `kiteline/web-app`.

AWS trusts GitHub through an IAM OIDC identity provider for `token.actions.githubusercontent.com` with the audience `sts.amazonaws.com`, and a role whose trust policy says which subjects may assume it. The policy below, adapted from the [AWS IAM guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html), allows only the `main` branch of one repository:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::012345678910:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:kiteline/web-app:ref:refs/heads/main"
        }
      }
    }
  ]
}
```

For a repository created after July 15, 2026, the subject includes the IDs, so the same condition reads `repo:kiteline@OWNER_ID/web-app@REPO_ID:ref:refs/heads/main` with your numeric IDs. When AWS sees GitHub as the trusted principal, it refuses to save a trust policy whose `sub` condition is missing or is only a wildcard. It also warns that without a specific organization or repository in `sub`, workflows outside your control could assume the role.

The workflow requests the token and lets the official action make the exchange. This follows the [configure-aws-credentials README](https://github.com/aws-actions/configure-aws-credentials), which uses `sts.amazonaws.com` as the audience by default:

```yaml
permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v6.3.0
        with:
          role-to-assume: arn:aws:iam::012345678910:role/web-app-deploy
          aws-region: eu-west-1
      - run: aws sts get-caller-identity
```

Two rules make this setup hold up. Give the role only the permissions the deploy needs, because any step of the job can use them. And for production, prefer an environment subject over a branch subject, then add required reviewers or branch limits to that environment in GitHub. The same AWS guide strongly recommends protection rules on any environment that an OIDC policy relies on.

## GitLab CI to Google Cloud

Google Cloud calls the mechanism Workload Identity Federation. You create a pool, add an OIDC provider for your GitLab issuer, map claims to attributes, and restrict which tokens the provider accepts with an attribute condition. The commands below follow [Google's guide for deployment pipelines](https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines):

```sh
gcloud iam workload-identity-pools create ci-pool \
  --location="global" \
  --display-name="CI pipelines"

gcloud iam workload-identity-pools providers create-oidc gitlab \
  --location="global" \
  --workload-identity-pool="ci-pool" \
  --issuer-uri="https://gitlab.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.project_id=assertion.project_id,attribute.namespace_id=assertion.namespace_id" \
  --attribute-condition="assertion.namespace_id=='GROUP_ID' && assertion.environment=='production'"
```

`GROUP_ID` is the numeric ID of your GitLab group. Mapping `google.subject` to `sub` keeps the project path and Git ref in the principal, so you can grant access per project and branch.

The attribute condition is not optional in practice. GitLab.com and GitHub use one issuer for every customer, so without a condition the provider accepts tokens from any project on the platform. Google's guide says you must restrict access to tokens from your own organization, and suggests numeric ID claims over names because IDs are unique and can't be reused.

Google then recommends granting roles directly to the federated principal rather than impersonating a service account, where the API you call allows it. The member string names the pool and an attribute, with the project number rather than the project ID:

```sh
gcloud storage buckets add-iam-policy-binding gs://kiteline-artifacts \
  --role=roles/storage.objectViewer \
  --member="principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/ci-pool/attribute.project_id/4242"
```

In the job, declare an ID token whose audience is the provider itself, write it to a file, and point the Google client libraries at a credential configuration that reads that file:

```yaml
deploy:
  environment: production
  id_tokens:
    WORKLOAD_IDENTITY_TOKEN:
      aud: https://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/ci-pool/providers/gitlab
  variables:
    GOOGLE_APPLICATION_CREDENTIALS: $CI_BUILDS_DIR/.workload_identity.wlconfig
  script:
    - echo "$WORKLOAD_IDENTITY_TOKEN" > "$CI_BUILDS_DIR/.workload_identity.jwt"
    - ./write-credential-config.sh
    - ./deploy.sh
```

The credential configuration is a small JSON file of type `external_account` that names the provider, the token file and Google's token endpoint; Google's guide shows it in full. Keep one ID token per receiving service, so the Google token and any vault token have different audiences.

*Figure: Diagram of a GitLab CI job signing in to Google Cloud 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 Google Cloud STS, which checks the issuer, the audience, the repository and the branch. Step 4: Google Cloud STS returns short-lived credentials, and no long-lived cloud key is stored in CI.*

## GitHub Actions to Azure

Azure trusts external tokens through a **federated identity credential** on a Microsoft Entra application or a user-assigned managed identity. A credential names an issuer, an exact subject and an audience. The example below is from Microsoft's [federated credential guide](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust):

```json
{
  "name": "web-app-production",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:kiteline/web-app:environment:production",
  "audiences": ["api://AzureADTokenExchange"]
}
```

Three constraints from that guide shape the design:

- **Exact matches only.** Wildcards aren't supported in any field, and pattern matching isn't supported for branches and tags. A subject that is slightly off is saved without error, and the exchange then fails without a clear message. For workflows that run on many branches, bind to an environment.
- **Up to 20 credentials** per application or managed identity. Plan one identity per deploy target rather than one per branch.
- **One audience,** with `api://AzureADTokenExchange` recommended.

The workflow uses `azure/login` with the IDs of the application, as in [Microsoft's GitHub Actions guide](https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect):

```yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```

The client, tenant and subscription IDs are identifiers, not credentials. Microsoft still suggests keeping them in secrets rather than in the workflow file.

## Bitbucket Pipelines to AWS

Bitbucket hands each OIDC-enabled step a token in `BITBUCKET_STEP_OIDC_TOKEN`. You register the workspace's identity provider URL and audience, copied from the repository's OpenID Connect settings, as an IAM OIDC provider, then assume a role with the token. Atlassian's example uses the AWS CLI directly:

```yaml
pipelines:
  default:
    - step:
        oidc: true
        image: amazon/aws-cli
        script:
          - aws sts assume-role-with-web-identity --role-arn arn:aws:iam::012345678910:role/web-app-build --role-session-name build-session --web-identity-token "$BITBUCKET_STEP_OIDC_TOKEN" --duration-seconds 1000
```

On the AWS side, restrict the role by repository and, for deploys, by deployment environment, using the repository and environment UUIDs from Bitbucket's settings. Atlassian's guide lists those restrictions, plus limiting access to Bitbucket Pipelines IP ranges, as options for the resource side. The blog post on [the Bitbucket step token](https://crunu.com/blog/bitbucket-pipelines-oidc-step-token) goes through the claims one by one.

## Self-managed CI and other issuers

Everything above assumes the hosted services. Self-managed installations change two things.

**The issuer is yours.** A self-managed GitLab instance signs tokens with its own domain as the issuer, so a cloud provider that trusts it trusts only your instance, not every GitLab.com customer. That removes the shared-issuer problem but not the need for a condition: every project on your instance can still request a token. Restrict by group or project ID as before.

**Defaults differ.** GitHub's rollout of the immutable subject format does not include GitHub Enterprise Server, per its OIDC reference, so subjects there keep the name-based format unless you customize them.

Other CI systems, and tools that run jobs outside a classic CI, can act as OIDC issuers too. The test for whether you can trust one is the same as for the big three: does it publish signing keys through OIDC discovery, does it put the repository or project in a claim you can pin, and can you set an audience per receiver? A vault or cloud that accepts a generic issuer should make you pin at least one such claim. Crunu, for example, requires a generic rule to pin a repository, project or owner without a `*` pattern, and checks the standard claims such as expiry and audience itself.

When a CI system can't issue tokens at all, the fallback is a stored credential with every other limit you can add: access to only the environments the job needs, a short lease, a usage limit, and an allowed network range. In Crunu those are settings on the service token, described in [Any CI with a service token](https://crunu.com/docs/ci/any-ci-with-a-service-token).

## OIDC for secrets that aren't cloud keys

Cloud trust covers cloud APIs. Most pipelines also need values that no cloud issues: a Stripe test key for integration tests, a database URL for migrations, an API token for a monitoring service, a signing key for release builds. Stored as CI variables, they bring back every problem OIDC removed.

There are two ways to keep them out of CI settings too.

**Store them in the cloud's secret manager and reach it with OIDC.** The job assumes a role, then reads the secret. This works. The trade-off is that the cloud service decrypts the value to return it, and anyone in the account with the right IAM permissions can read it as well. [Who can decrypt your secrets](https://crunu.com/guides/who-can-decrypt-your-secrets) compares that model with the alternatives.

**Have the vault itself accept the CI token.** Several vaults accept OIDC from CI. In Crunu, OIDC is a **CI binding** on a service token. The token is created in an admin's browser and shown once, and it is granted only the environments the pipeline needs. With a binding, the token signs in only together with a JWT from the CI provider that matches your rules, and each JWT can be used once:

| Provider            | Required in a rule                                                                                      | Optional limits                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| GitHub Actions      | repository, audience                                                                                    | ref, environment, workflow ref, repository ID, owner ID               |
| GitLab CI           | project path, audience                                                                                  | ref, ref type, protected ref, environment, namespace path, project ID |
| Bitbucket Pipelines | workspace, workspace UUID, repository UUID                                                              | branch, deployment environment UUID, audience                         |
| Any OIDC issuer     | an `https` issuer, an audience, at least one claim that pins a repository, project or owner without `*` | up to 32 claim constraints                                            |

*Figure: Example: Priya created the service token github-actions-web-app; the window shows it only once with ready variables for GitHub Actions. In the kiteline/web-app pipeline on main, crunu run gets web-app staging and deploys, and the token works only with GitHub’s OIDC token for that repository and branch.*

The agent picks up the job's token by itself on GitHub Actions, GitLab CI and Bitbucket Pipelines. On GitHub the job needs only the permission. On GitLab it declares a token with your Crunu server address as the audience:

```yaml
deploy:
  image: node:24
  environment: production
  id_tokens:
    CRUNU_ID_TOKEN:
      aud: https://app.crunu.com
  script:
    - crunu run --env web-app/production -- ./deploy.sh
```

On Bitbucket, add your server address as an audience of its own, so the token isn't the workspace-wide one:

```yaml
- step:
    name: Tests with staging variables
    oidc:
      audiences:
        - https://app.crunu.com
    script:
      - crunu run --env web-app/staging -- npm test
```

`crunu run` decrypts the environment in memory on the runner and starts the command with its variables. It never passes its own credentials, such as `CRUNU_TOKEN` or the ID token, to that command. The server stores only the token's public keys and ciphertext, so the values never pass through it in readable form. A replayed JWT is refused and raises an alert; for Bitbucket, whose tokens carry no `jti` claim, replays are recognized by a fingerprint of the token's signed part. See [Bind a service token to CI with OIDC](https://crunu.com/docs/ci/oidc-binding) for every field.

*Figure: Diagram of a Bitbucket Pipelines job signing in to Crunu with OIDC. Step 1: the job asks the Bitbucket Pipelines 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.*

## One identity per job, not per repository

The easy design is one role per repository that every job assumes. It is also the design that gives your test job the same access as your production deploy. OIDC makes finer identities cheap, so use them.

| Job                      | What it should get                            | Rule                                                                     |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------------------ |
| Tests on pull requests   | test or staging values, no cloud write access | repository plus `pull_request`, or a GitLab rule without `ref_protected` |
| Build on the main branch | permission to push an image or artifact       | repository plus the protected main branch                                |
| Deploy to staging        | staging deploy rights and staging values      | repository plus the `staging` environment                                |
| Deploy to production     | production deploy rights only                 | repository plus the `production` environment with required reviewers     |

Each row is a separate cloud role, Google principal set or Azure credential, and in a vault a separate service token with its own environments. Two things follow. A mistake in the test job can no longer reach production, because nothing it can present is accepted there. And your audit log shows which pipeline stage did what, instead of one shared identity for everything.

Keep the count manageable by naming identities after the job and environment, such as `web-app-deploy-production`, and by keeping the rules in the same repository as the workflow, reviewed together. Azure's limit of 20 federated credentials per identity is another reason to split by deploy target rather than accumulate branches on one identity.

## Writing rules that hold

Each item below is a way a rule can be looser than its author thought. Check yours against the list.

- **Wildcards across an organization.** `repo:kiteline/*` accepts every repository in the organization, including one an intern creates tomorrow. Name the repository.
- **Repository without branch or environment.** Any branch that can run a workflow, including one pushed by anyone with write access, then gets production. For production, bind to an environment with protection rules, or at least to a protected branch.
- **Pull requests.** A rule that accepts `pull_request` subjects accepts code that hasn't been reviewed yet. Give pull request jobs a separate identity with test access only.
- **Names instead of IDs.** A deleted organization or repository name can be registered again. Prefer `repository_id` and `repository_owner_id` on GitHub, project or namespace IDs on GitLab, and UUIDs on Bitbucket.
- **Shared audiences.** If two services accept the same audience, a token minted for one works against the other. Give each receiver its own.
- **Changing the subject format without updating policies.** GitHub recommends creating the matching condition at your cloud provider before customizing the subject template. The same applies to opting in to the immutable format.
- **Long-lived fallbacks.** An old access key left in CI "just in case" undoes the work. Delete it once the OIDC path works.

The [OIDC trust policy builder](https://crunu.com/tools/oidc-policy-builder) generates AWS, Google Cloud and Azure policies from a repository, branch and environment, in both GitHub subject formats.

## When a token is refused

A refused exchange usually means the rule and the token disagree about one claim. The error messages are terse, and Microsoft notes that a wrong subject on Azure fails without a clear error. Check these in order:

1. **The subject format.** Is the repository new enough to use GitHub's immutable format with IDs? Does the job reference an environment, so that the subject names the environment rather than the branch?
2. **The audience.** AWS expects `sts.amazonaws.com`, Azure recommends `api://AzureADTokenExchange`, Google's GitLab example uses the provider's own address, and your vault expects its server address. A GitLab job needs one `id_tokens` entry per audience.
3. **The issuer.** Compare it character by character, including a trailing slash. Microsoft's own examples show the GitHub issuer both with and without one, so match what your provider's token actually contains.
4. **The trigger.** A workflow started by a tag, a schedule or a manual run may produce a different subject from the branch push you tested.
5. **The permission.** On GitHub, a job without `id-token: write` gets no token at all, and reusable workflows called from another organization need the permission set explicitly in the caller.

To see what the job really sends, decode the token's claims in a debug run on a branch that has no production access, and never print the token itself: until it expires, the token is a credential. Compare the decoded `sub`, `aud` and `iss` with the rule, fix the rule, then remove the debug step.

## What OIDC does not protect

OIDC answers "is this the right job?" It doesn't answer "is this job doing the right thing?"

- **Code inside the job.** A compromised dependency, a malicious build script or a modified test runs inside the job that passed every check, and can use whatever credentials the job received. The protection is a narrow grant: the job gets only what its work needs.
- **Logs and artifacts.** A value printed by mistake ends up in logs, caches and artifacts. Masking replaces only exact strings that the CI knows about, so a value that was encoded or split along the way can slip through.
- **Job-wide variables.** Variables set for the whole job are visible to every step and process in it. Set credentials on the one step that uses them.
- **The runner itself.** On a runner you host, anyone with access to the machine sees what the job sees.

*Figure: Diagram of where a secret goes once it is in CI logs. CI provider’s log storage: kept for the retention period; it stays after you delete it. Log forwarding: your log platform keeps its own copy; it stays after you delete it. Artifacts and caches: files the job uploads. Everyone who can open the job: read access to the project is often enough. Deleting the copy doesn’t undo the leak, so the value has to be reissued at its provider.*

For production, one answer is to keep production secrets out of CI entirely. The [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) says to treat CI/CD tooling as a production environment, and that it is better still when the consumer of a secret retrieves it itself. With Crunu that means running the agent on the production machine, so the pipeline only ships code and restarts a service. [Giving CI only the secrets it needs](https://crunu.com/blog/ci-secrets-oidc-vs-long-lived-tokens) explains that setup and why it beats any token in CI.

## Moving a pipeline from stored keys to OIDC

1. List every credential stored in the pipeline's settings, with the job that uses it and what it can reach.
2. For each cloud, create the OIDC identity provider or workload identity pool and a role that only that pipeline uses.
3. Write the trust rule with the narrowest subject that works: repository plus branch, or repository plus deployment environment for production.
4. Change the job to request a token and exchange it, and run it on a branch that the rule doesn't allow, to confirm it is refused.
5. Move non-cloud secrets into a vault the job reaches with its OIDC token, and grant it only the environments the job needs.
6. Delete the stored keys from CI settings, then deactivate and delete them at the provider.
7. Record which identity each pipeline uses, so the next audit can check the rules against the list.

Step 4 is the one that catches mistakes. A rule that accepts a forbidden branch looks exactly like a correct rule until something proves otherwise.

For more depth on single topics, see [the OIDC subject claim](https://crunu.com/learn/oidc-subject-claim), [workload identity federation](https://crunu.com/learn/workload-identity-federation), [GitHub's new subject format and AWS trust policies](https://crunu.com/blog/github-oidc-subject-claim-format) and [GitLab CI OIDC without Premium](https://crunu.com/blog/gitlab-ci-oidc-without-premium). The CI pages for [GitHub Actions](https://crunu.com/integrations/github-actions), [GitLab CI](https://crunu.com/integrations/gitlab-ci) and [Bitbucket Pipelines](https://crunu.com/integrations/bitbucket-pipelines) show the Crunu side in each.

## FAQ

### What is OIDC in CI/CD, in one sentence?

The CI provider signs a short-lived token for each job that says which repository, branch and environment it runs for, and the cloud or vault checks that signature and those claims instead of asking for a stored password or key.

### Do I still need any secrets in CI if I use OIDC?

For cloud access, usually not: the job exchanges its token for temporary credentials. For other secrets, such as a Stripe key or a database URL, the job still needs a place to fetch them from. A vault that accepts the CI token, or a cloud secret manager reached through OIDC, keeps those out of CI settings too.

### Why does AWS say my GitHub workflow is not authorized to assume the role?

A likely cause is that the sub claim in the trust policy doesn't match the token. Repositories created on GitHub after July 15, 2026 use a subject format that includes owner and repository IDs, so a policy written for the old format no longer matches. Compare the policy with the claims your job actually receives.

### Is OIDC available on the free plans of GitLab and Bitbucket?

GitLab lists ID tokens on its Free, Premium and Ultimate tiers. Atlassian's guide to OIDC in Bitbucket Pipelines doesn't tie it to a plan. Check your own plan's limits before you depend on it.

### Can OIDC stop a malicious dependency in my build from stealing secrets?

No. OIDC proves which job is calling. A job that passes the checks receives its credentials, and any code running inside that job can use them. Keep each job's access as narrow as its work, and keep production secrets out of jobs that run on pull requests.

### How does Crunu use OIDC?

As a binding on a service token. With a CI binding, the token signs in only together with a JWT from GitHub Actions, GitLab CI, Bitbucket Pipelines or another OIDC issuer that matches your rules for repository, branch and environment. A replayed JWT is refused.

## Sources

- [OpenID Connect reference, GitHub Docs](https://docs.github.com/en/actions/reference/security/oidc) (accessed 2026-10-30)
- [Create a role for OpenID Connect federation, AWS IAM User Guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html) (accessed 2026-10-30)
- [configure-aws-credentials, AWS](https://github.com/aws-actions/configure-aws-credentials) (accessed 2026-10-30)
- [Workload Identity Federation with deployment pipelines, Google Cloud](https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines) (accessed 2026-10-30)
- [Create a trust relationship with an external identity provider, Microsoft Entra](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust) (accessed 2026-10-30)
- [Authenticate to Azure from GitHub Actions by OpenID Connect, Microsoft Learn](https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect) (accessed 2026-10-30)
- [OpenID Connect (OIDC) ID token authentication, GitLab Docs](https://docs.gitlab.com/ci/secrets/id_token_authentication/) (accessed 2026-10-30)
- [Integrate Pipelines with resource servers using OIDC, Atlassian Support](https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/) (accessed 2026-10-30)
- [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) (accessed 2026-10-30)
