Secure GCP auth in Bitbucket Pipelines

This is post two in my Bitbucket Woes series. Post one covered the mental-model gap between GitHub Actions and Bitbucket Pipelines.

This one is about a real pain point and cost of the gap: the Pipes ecosystem is much smaller than the Actions marketplace, and I’ve found that that gap is most painful around cloud authentication.

Bitbucket does support OIDC for keyless auth to GCP, AWS, and Azure but you BYOG - bring your own glue. What are DevOps folks but fond of glue and pipes though? Here’s what that looks like for GCP Workload Identity Federation.

The elephant (key) in the room Link to heading

A.k.a. Are the long-lived, dangerous static secrets in the room with us right now?

Service-account JSON keys are convenient and hella dangerous. They’re long-lived, generally sitting in your CI system’s secret store, and a leak by virtue(or sin) of not having protection against committing secrets(oof), exfiltration by supply chain attack(more and more common) , surfaced in build logs, etc, causes a credential out in the world that can take real action in your cloud space.

Workload Identity Federation fixes this by having CI present a short-lived OIDC token signed by the CI provider, GCP verifies the token against a configured Identity Pool/Provider, GCP hands back a short-lived access token (or impersonates a service account, more probably on this later).

GitHub Actions makes this almost trivial. Bitbucket Pipelines supports the flow, but you don’t get a one-line action — you write a handful of shell commands. I am sorry.

What GitHub Actions does for you Link to heading

In an Actions workflow, the entire CI side of WIF is roughly:

permissions:
  id-token: write # let the runner mint an OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: "projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider"
          service_account: "ci-deployer@my-project.iam.gserviceaccount.com"

      - run: gcloud storage ls gs://my-bucket

That’s it! The google-github-actions/auth action is a beautiful friend that handles all sorts of stuff like getting the OIDC token scoped to your project’s audience, calling STS and getting a federated access token, setting env vars, and more. All in one uses:!

The same thing in Bitbucket Pipelines Link to heading

Bitbucket exposes the building block via. oidc: true on a step minting an OIDC token and placing its value in the $BITBUCKET_STEP_OIDC_TOKEN environment variable. From there, you gotta get out your homemade glue. Everyone loves homemade glue.

Step 1 — GCP-side setup (one-time) Link to heading

You configure this in GCP, not in Bitbucket.

# Variables
PROJECT_ID="my-project"
PROJECT_NUMBER="123456789"
POOL_ID="bitbucket-pool"
PROVIDER_ID="bitbucket-provider"
WORKSPACE_UUID="{your-bitbucket-workspace-uuid}"   # from Bitbucket workspace settings
SA_EMAIL="ci-deployer@${PROJECT_ID}.iam.gserviceaccount.com"

# Create the Workload Identity Pool
gcloud iam workload-identity-pools create "${POOL_ID}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="Bitbucket Pipelines"

# Create the OIDC provider inside that pool
gcloud iam workload-identity-pools providers create-oidc "${PROVIDER_ID}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="${POOL_ID}" \
  --display-name="Bitbucket OIDC" \
  --issuer-uri="https://api.bitbucket.org/2.0/workspaces/${WORKSPACE_UUID}/pipelines-config/identity/oidc" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository_uuid=assertion.repositoryUuid,attribute.workspace_uuid=assertion.workspaceUuid" \
  --attribute-condition="attribute.workspace_uuid == '${WORKSPACE_UUID}'"

# Allow your Bitbucket workspace's identities to impersonate the service account
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.workspace_uuid/${WORKSPACE_UUID}"

A few subtleties worth calling out:

  • The issuer URI is per-workspace. Bitbucket exposes the OIDC discovery document at a workspace-scoped URL, not a Bitbucket-wide one. You can get your workspace UUID from Workspace settings → OpenID Connect in the Bitbucket UI.
  • The attribute condition is your security boundary. Without --attribute-condition, any Bitbucket workspace would work to ask this provider for a token. So scope your attribute condition to your workspace UUID, or better yet a repository or repositories UUIDs in prod.
  • workloadIdentityUser binds which identities can impersonate the SA. The principalSet:// member here means that any identity from your workspace can impersonate ci-deployer. Preferably bind on attribute.repository_uuid/<repo-uuid> instead.

Step 2 — Bitbucket-side YAML and (s)hell Link to heading

This is the part where you write the glue.

For readers who don’t have a working example to start from, here’s a reference shape using gcloud’s built-in credential config. This is the shorter of the two common approaches, and the one I like to use:

image: google/cloud-sdk:slim

pipelines:
  branches:
    main:
      - step:
          name: Deploy to GCP via WIF
          oidc: true # makes $BITBUCKET_STEP_OIDC_TOKEN available
          script:
            # 1. Write the OIDC token to a file gcloud can read
            - echo "$BITBUCKET_STEP_OIDC_TOKEN" > /tmp/oidc-token.txt

            # 2. Create a credential config that points at the WIF provider
            - |
              gcloud iam workload-identity-pools create-cred-config \
                "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}" \
                --service-account="${SA_EMAIL}" \
                --output-file=/tmp/credential-config.json \
                --credential-source-file=/tmp/oidc-token.txt              

            # 3. Tell gcloud (and Google client libraries) to use it
            - export GOOGLE_APPLICATION_CREDENTIALS=/tmp/credential-config.json
            - gcloud auth login --cred-file=/tmp/credential-config.json --quiet

            # 4. Verify, then do real work
            - gcloud auth list
            - gsutil ls gs://${BUCKET}

GCP_PROJECT_NUMBER, POOL_ID, PROVIDER_ID, SA_EMAIL, and BUCKET are repos or deployment variables. The only real secret in this flow is the OIDC token, which Bitbucket mints for each step.

There’s a couple of gotchas that can trip you up here:

  • Double-check the workspace UUID — Bitbucket UUIDs include the curly braces ({abc-123-...}) and the provider config needs them. Weird Bitbucket formatting might be a theme in these posts.

  • The SA needs roles/iam.workloadIdentityUser granted to the principalSet. The error message names the SA, not the binding, in GCP’s tradition of somewhat misleading or cryptic error messages.

  • The pool/provider path uses the numeric project number, not the project ID string. Lots of things use the latter, so it’s an easy mistake to make.

What’s worth verifying against current docs Link to heading

OIDC, WIF, and gcloud’s credential-helper commands have all evolved. Before copy-pasting, cross-check against:

YMMV. The shape should be good, make sure the syntax and all that are right.

Previous in series: Coming from GitHub Actions? Here’s what Bitbucket Pipelines actually feels like

Next in series: The Bitbucket API UUID Slog