# Bitbucket Pipelines OIDC: the step token explained

> What oidc: true gives a Bitbucket step: the BITBUCKET_STEP_OIDC_TOKEN, the workspace issuer, audiences, the sub claim and how to pin a trust policy to it.

URL: https://crunu.com/blog/bitbucket-pipelines-oidc-step-token
Language: en
Updated: 2026-10-30

Bitbucket Pipelines hands a step a signed JWT when that step has `oidc: true`. The token arrives in the variable `$BITBUCKET_STEP_OIDC_TOKEN`, is issued by an identity provider that belongs to your workspace, and names the repository, branch, step and, for deployment steps, the deployment environment. AWS, Google Cloud, Vault or Crunu check its signature against the workspace's public keys, compare its claims with a policy you wrote, and hand back short-lived access. No cloud key needs to sit in repository variables.

This post walks through the token itself: how to turn it on, which claims it carries, how audiences work, how to pin an AWS trust policy to the `sub` claim, what Bitbucket's third-party secret providers need, and how the same token binds a Crunu service token to one repository, branch and deployment. For GitHub Actions and GitLab CI, and for every cloud, see the guide to [CI/CD secrets with OIDC](https://crunu.com/guides/ci-cd-secrets-with-oidc).

## Turning the token on

OIDC is a property of a step. This step follows Atlassian's AWS example ([Atlassian: deploy on AWS using OIDC](https://support.atlassian.com/bitbucket-cloud/docs/deploy-on-aws-using-bitbucket-pipelines-openid-connect/)):

```yaml
image: amazon/aws-cli

pipelines:
  default:
    - step:
        oidc: true
        script:
          - export AWS_REGION=us-west-2
          - export AWS_ROLE_ARN=arn:aws:iam::123456789012:role/oidc-demo
          - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
          - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
          - aws s3 ls
```

Bitbucket generates a separate token for each step that asks for one. Other steps in the same pipeline don't get it unless they ask for it too. That is the first control you have: put `oidc: true` only on the steps that talk to a cloud or a vault.

The script writes the JWT to a file because the AWS CLI reads a web identity token from the path in `AWS_WEB_IDENTITY_TOKEN_FILE`. The file lives in the build directory for the rest of the step, so don't copy it into an artifact or a cache.

## What the token says about the step

Atlassian's example payload lists these claims: `sub`, `aud`, `iss`, `exp`, `iat`, `workspaceUuid`, `repositoryUuid`, `pipelineUuid`, `stepUuid`, `branchName` and `deploymentEnvironment`. Three of them carry most of the weight in a policy.

- **`repositoryUuid`** identifies the repository with its UUID, in braces, for example `{1de489be-…}`. A UUID survives a rename of the repository, unlike a slug.
- **`branchName`** is the branch the pipeline runs on.
- **`sub`** packs the identity into one string: `{REPOSITORY_UUID}[:{ENVIRONMENT_UUID}]:{STEP_UUID}`. The environment part "shows up only if the step is assigned to a deployment environment."

That last detail matters. A deployment step to production has the production environment's UUID inside `sub`; an ordinary test step doesn't. Atlassian calls this "very important for crafting strict policies per environment."

The values you need for a policy are in **Repository settings → Pipelines → OpenID Connect**: the identity provider URL, the audience, and the UUIDs of the workspace, repository and deployment environments ([Atlassian: integrate Pipelines with resource servers using OIDC](https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/)).

## One issuer per workspace

GitHub and GitLab.com each have one shared issuer by default. Bitbucket has one per workspace:

```text
https://api.bitbucket.org/2.0/workspaces/<workspace>/pipelines-config/identity/oidc
```

The discovery document and public keys hang off that address. Two consequences follow. First, the issuer alone already limits a policy to your workspace, so a token from another company's workspace fails before any claim is compared. Second, every repository in your workspace shares that issuer, so a policy that checks only the issuer and the default audience accepts a token from any of your repositories. Atlassian's AWS page says so plainly: the basic setup lets "any repository under the workspace" assume the role.

## Audiences: the default and your own

The default audience has the form `ari:cloud:bitbucket::workspace/<workspace-uuid>`. It is the same for every step in the workspace, and every service that trusts it accepts the same token. If one service logs incoming tokens or is compromised, a token it received is valid at the others until it expires.

Since February 2026, a step can declare several audiences of its own, per step or for the whole file, and one token carries all of them ([Atlassian: multiple audiences](https://www.atlassian.com/blog/bitbucket/bitbucket-pipelines-oidc-now-supports-multiple-audiences)):

```yaml
pipelines:
  default:
    - step:
        oidc:
          audiences:
            - https://app.crunu.com
        script:
          - ./deploy.sh
```

The limits are 10 audiences per step or file and 150 characters per audience name. Atlassian also states the rule that makes audiences useful: resource servers "must check that their identifier appears in the `aud` claim" and refuse the token if it doesn't.

Notice the difference from GitLab, where a job declares one ID token per audience. In Bitbucket a step gets one token with every audience in it, so each service named there could replay it at the others. If a step needs AWS and another service, consider two steps with one audience each.

## AWS: pin the trust policy to `sub`

Register the workspace's identity provider URL in IAM with your audience as the client ID, then create a role. Atlassian notes that AWS lets a trust policy use only a set of claims, which is why its examples match on `sub` with `StringLike`. To allow one repository:

```json
"Condition": {
  "StringLike": {
    "api.bitbucket.org/2.0/workspaces/kiteline/pipelines-config/identity/oidc:sub": "{REPO_UUID}:*"
  }
}
```

To allow only deployment steps to one environment, include the environment's UUID:

```json
"Condition": {
  "StringLike": {
    "api.bitbucket.org/2.0/workspaces/kiteline/pipelines-config/identity/oidc:sub": "{REPO_UUID}*:{ENVIRONMENT_UUID}:*"
  }
}
```

Kiteline is a fictional company; use your own workspace and UUIDs, braces included. The branch isn't part of `sub`, so for AWS the deployment environment is the practical way to keep a feature branch away from production. Atlassian also documents an IP-range condition limited to Bitbucket Pipelines' addresses as an extra layer.

*Figure: Diagram of a Bitbucket Pipelines job signing in to AWS STS 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 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 [OIDC policy builder](https://crunu.com/tools/oidc-policy-builder) drafts trust policies like these, and [what is a JWT](https://crunu.com/learn/jwt) explains the token format.

## Third-party secret providers: what they need

Since December 2024, Bitbucket can inject variables from an outside secret store into a step without storing them in Bitbucket ([Atlassian: third-party secrets in Pipelines](https://www.atlassian.com/blog/bitbucket/third-party-secrets-are-now-supported-bitbucket-pipelines)). The flow is built on the same step token:

1. You set a **Secret provider endpoint** for the workspace, or `SECRET_PROVIDER_URI` on a self-hosted runner.
2. The step declares `oidc: true`, with the comment "Required to get the token to pass to the Secret Provider".
3. At run time the runner calls your endpoint "including the OIDC token in the `Authorization` header."
4. The endpoint validates the token against the workspace's keys and issuer, picks the secrets that the claims allow, and answers with "a flat JSON key-value map, where both keys and values are strings."

The part that's easy to miss: the endpoint is yours to build. Atlassian describes it as "an integration that serves as a bridge between your specific secret store and the runner" and points to a demo. On self-hosted runners the request goes from the runner, so values stay inside your network. Bitbucket says logs "automatically mask these variables", which helps with accidental `echo` but not with a value that's been encoded or split; [masked CI variables still leak](https://crunu.com/blog/masked-ci-variables-still-leak) covers where masking stops.

So the provider gives you declarative injection, and in return you run and secure a service whose only job is to turn a Bitbucket JWT into secrets. It needs the same claim checks as an AWS trust policy, written in your own code.

## Binding a Crunu service token to the step

Crunu reads the same token. A pipeline signs in with a [service token](https://crunu.com/learn/service-token) stored in `CRUNU_TOKEN`; when that token has a **CI binding (OIDC)**, the server accepts it only together with a Bitbucket JWT whose claims match the binding. The rule for Bitbucket takes the workspace name and UUID, the repository UUID, optionally a branch (`main` or a list of patterns), optionally the deployment environment's UUID, and the audience. The issuer is the workspace's own, and UUIDs are compared without braces and regardless of case ([Crunu docs: OIDC binding](https://crunu.com/docs/ci/oidc-binding)).

Unlike AWS, the Crunu rule matches `branchName` directly, so a production token can require both `main` and the production deployment environment.

```yaml
pipelines:
  branches:
    main:
      - step:
          name: Deploy
          deployment: production
          oidc:
            audiences:
              - https://app.crunu.com
          script:
            - curl -sSf "$CRUNU_INSTALL_URL" -o /tmp/crunu-install.sh
            - echo "$CRUNU_INSTALL_SHA256  /tmp/crunu-install.sh" | sha256sum -c -
            - sh /tmp/crunu-install.sh --version "$CRUNU_VERSION" --sha256 "$CRUNU_BINARY_SHA256" --user-install
            - crunu run --env web-app/production -- npm run deploy
```

`crunu` finds `$BITBUCKET_STEP_OIDC_TOKEN` by itself. Use the server address as the audience, `https://app.crunu.com` in the cloud or your own server's address, so the token is made for Crunu and nothing else. The default workspace audience also works, but then every service in the workspace receives a token Crunu would accept.

Two Bitbucket specifics are handled on the server side. Bitbucket tokens have no `jti` claim, so the server recognizes a replay by a fingerprint of the token's signed part and raises an alert; `crunu` therefore signs in once per step and keeps that session. And because the whole step shares one token, the binding is only as narrow as the step: a step that also calls AWS hands the same JWT to AWS. Setup details, including the `crunu run` pipe, are in the [Bitbucket Pipelines docs](https://crunu.com/docs/ci/bitbucket-pipelines) and on the [Bitbucket Pipelines integration](https://crunu.com/integrations/bitbucket-pipelines) page.

*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.*

A `CRUNU_TOKEN` copied out of a log then fails from a laptop or another repository, because it needs a fresh JWT from this repository's step. Within the step, the decrypted values are still visible to the command and anything it runs, so grant the token only the environments that step needs.

## Checklist

- [ ] `oidc: true` only on steps that call a cloud, a vault or Crunu
- [ ] A custom audience per service instead of the shared workspace audience
- [ ] No more audiences on a step than that step uses
- [ ] AWS trust policies match the repository UUID in `sub`, plus the environment UUID for production
- [ ] Production deployments go through a restricted deployment environment
- [ ] Crunu tokens that read production have a CI binding with repository, branch and deployment environment
- [ ] No long-lived cloud keys left in repository variables after the switch

## Sources

- [Deploy on AWS using Bitbucket Pipelines OpenID Connect, Atlassian Support](https://support.atlassian.com/bitbucket-cloud/docs/deploy-on-aws-using-bitbucket-pipelines-openid-connect/), checked October 30, 2026.
- [Integrate Pipelines with resource servers using OIDC, Atlassian Support](https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/), checked October 30, 2026.
- [Bitbucket Pipelines OIDC now supports multiple audiences, Atlassian blog](https://www.atlassian.com/blog/bitbucket/bitbucket-pipelines-oidc-now-supports-multiple-audiences), checked October 30, 2026.
- [Third-party secrets are now supported in Bitbucket Pipelines, Atlassian blog](https://www.atlassian.com/blog/bitbucket/third-party-secrets-are-now-supported-bitbucket-pipelines), checked October 30, 2026.
