Bitbucket Pipelines repositories commonly hold AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as secured repository or workspace variables, which are long-lived keys available to every step that can read them. Bitbucket Cloud can instead issue an OpenID Connect token to each step, which AWS STS exchanges for temporary credentials for a role whose trust policy names the workspace, the repository and, ideally, the deployment environment. This page is the runbook: collect the identifiers, create the provider and role, update bitbucket-pipelines.yml, test, roll back and delete the keys. It is for DevOps and platform engineers who administer Bitbucket workspaces and AWS accounts. For the wider platform hardening picture see the Bitbucket Security Guide; this page focuses on the migration.
This is an independent guide from NHI Mgmt Group, based on Atlassian's and AWS's public documentation as checked in November 2026. It is not sponsored or reviewed by Atlassian or AWS. Confirm settings against the current vendor documentation before changing production.
Key takeaways
- Without a
subcondition, the whole workspace is trusted. Atlassian states that by default any repository in the workspace can assume the role, and so can anyone holding a token. The issuer and audience are per workspace, so they do not narrow anything inside it. - The subject is built from UUIDs, not names. It takes the form
{REPOSITORY_UUID}[:{ENVIRONMENT_UUID}]:{STEP_UUID}. Renaming a repository does not break or transfer the trust, which is safer than name-based subjects. - Bind production roles to a deployment environment. The environment UUID only appears in the subject when the step is a deployment step, so a trust policy that requires it cannot be met by an ordinary branch or pull request build.
- The token is a file you write yourself. The step exposes
BITBUCKET_STEP_OIDC_TOKEN; you write it to the path inAWS_WEB_IDENTITY_TOKEN_FILEand setAWS_ROLE_ARN. Never echo it into logs or save it as an artifact. - Finish by deleting keys. The migration is done only when the IAM user's keys are deleted and the secured variables removed at repository, deployment and workspace level.
When you need this
- AWS keys sit in secured variables. Workspace-level variables are the widest exposure: every repository in the workspace receives them. Secured variables are masked in logs, but any step can still use or exfiltrate them. A researcher's demonstration of a Bitbucket pipeline leading to server takeover shows how pipeline configuration and secrets become an attack path.
- One IAM user serves many repositories. CloudTrail cannot tell which repository made a call. Federated sessions record the repository and step UUIDs.
- You are already reviewing Atlassian credentials. If you are moving off app passwords, do this at the same time. See Atlassian App Passwords to API Tokens.
Before you start
| Item | Where to find it | Example form |
|---|---|---|
| Identity provider URL | Repository settings, Pipelines, OpenID Connect | https://api.bitbucket.org/2.0/workspaces/<WORKSPACE>/pipelines-config/identity/oidc |
| Audience | Same settings page | ari:cloud:bitbucket::workspace/<WORKSPACE_UUID> |
| Repository UUID | Same settings page, or $BITBUCKET_REPO_UUID in any build | {xxxxxxxx-...} |
| Deployment environment UUID | Same settings page, or $BITBUCKET_DEPLOYMENT_ENVIRONMENT_UUID in a deployment step | {xxxxxxxx-...} |
| AWS permissions | IAM | Rights to create an OIDC identity provider and roles |
| Bitbucket permissions | Workspace or repository admin | To manage variables, deployment environments and their restrictions |
| Permissions plan | CloudTrail history of the existing IAM user | The exact actions and resources each pipeline uses |
Bitbucket shows UUIDs wrapped in curly braces. Copy the identifiers exactly as the OpenID Connect settings page shows them, including the braces, and confirm the final subject from a real token or CloudTrail event before tightening the policy.
Choosing the subject condition
| Scope | sub condition (StringLike) | Use it for | Watch out for |
|---|---|---|---|
| Workspace | No sub condition | Never for write roles | Every repository, present and future, can assume the role |
| One repository | {REPO_UUID}:* | Read-only or test roles | Every branch and pull request build in that repository qualifies |
| One repository, one environment | {REPO_UUID}:{ENV_UUID}:* | Production and staging deploy roles | Only as strong as the environment's deployment restrictions |
Atlassian's own example for deployment steps uses {REPO_UUID}*:{ENVIRONMENT_UUID}:*. The step UUID changes every run, so a wildcard on the final segment is unavoidable; that is why the environment segment, not the step, carries the production boundary.
Step-by-step
- Inventory first (read-only). List repositories, deployment environments and the workspace with AWS variables, and record which IAM user each key belongs to. In AWS, check for an existing provider with
aws iam list-open-id-connect-providers; there is one provider per URL per account, and one Bitbucket provider covers the whole workspace. - Create the OIDC identity provider.
AWS verifies the issuer certificate against its trusted root CA library and uses a thumbprint only as a fallback. Atlassian's AWS page publishes a thumbprint for manual setups; you do not normally need to supply one.aws iam create-open-id-connect-provider \ --url https://api.bitbucket.org/2.0/workspaces/<WORKSPACE>/pipelines-config/identity/oidc \ --client-id-list ari:cloud:bitbucket::workspace/<WORKSPACE_UUID> - Write the trust policy for a production environment. Save as
trust.json:
The condition key prefix is the provider URL without{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/api.bitbucket.org/2.0/workspaces/<WORKSPACE>/pipelines-config/identity/oidc"}, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "api.bitbucket.org/2.0/workspaces/<WORKSPACE>/pipelines-config/identity/oidc:aud": "ari:cloud:bitbucket::workspace/<WORKSPACE_UUID>" }, "StringLike": { "api.bitbucket.org/2.0/workspaces/<WORKSPACE>/pipelines-config/identity/oidc:sub": "{<REPO_UUID>}:{<ENV_UUID>}:*" } } }] }https://, path included. AWS maps generic OIDC providers toaud,sub,amr,emailandoaudonly, so repository and environment scoping must live insub. - Create the role and attach permissions.
Build the permissions from what the pipeline actually calls. See the AWS IAM Policies and Roles Guide.aws iam create-role --role-name bbp-payments-api-prod-deploy \ --assume-role-policy-document file://trust.json --max-session-duration 3600 aws iam put-role-policy --role-name bbp-payments-api-prod-deploy \ --policy-name deploy --policy-document file://deploy-permissions.json - Restrict the deployment environment in Bitbucket. Limit which branches can deploy to the environment and, where your plan allows it, which users can run deployments. Bitbucket only releases an environment's variables to a step when the branch and admin permission checks pass, and manual steps can only be triggered by users with write access.
- Update
bitbucket-pipelines.yml.
The AWS CLI and SDKs readpipelines: branches: main: - step: name: Deploy to production deployment: production oidc: true image: amazon/aws-cli script: - export AWS_REGION=eu-west-2 - export AWS_ROLE_ARN=arn:aws:iam::<ACCOUNT_ID>:role/bbp-payments-api-prod-deploy - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token - aws sts get-caller-identityAWS_ROLE_ARNandAWS_WEB_IDENTITY_TOKEN_FILEand callAssumeRoleWithWebIdentitythemselves. Atlassian's documentation is inconsistent about whether OIDC is on by default, so setoidc: trueon steps that need it andoidc: falseon steps that do not, such as pull request builds. - Pilot on staging. Create a staging role bound to the staging environment UUID and run it first. Keep the old variables in place but unused by the federated step until both environments deploy cleanly.
Verify it worked
- Step output:
aws sts get-caller-identityreturns anassumed-roleARN for the new role, not an IAM user. - CloudTrail: find
AssumeRoleWithWebIdentitywithuserIdentity.typeWebIdentityUser.responseElements.subjectFromWebIdentityTokenshows the repository, environment and step UUIDs, andresponseElements.audiencethe workspace audience. Global STS endpoint calls are logged in us-east-1. - Negative tests: run the same step from a feature branch without
deployment: production, and from another repository in the workspace. Both must be denied. - Token hygiene: confirm the token file is not listed under
artifactsand that no script prints it.
| CloudTrail field | What it tells you for Bitbucket |
|---|---|
userIdentity.identityProvider | The workspace's Bitbucket provider ARN |
responseElements.subjectFromWebIdentityToken | Repository UUID, environment UUID (deployment steps) and step UUID |
requestParameters.roleSessionName | Session name chosen by the CLI or SDK; match on the step UUID from the subject instead |
sourceIPAddress | Egress address of the build, useful for anomaly review |
additionalEventData.identityProviderConnectionVerificationMethod | IAMTrustStore or Thumbprint |
Rollback and common errors
| Symptom | Likely cause | Fix |
|---|---|---|
| "Not authorized to perform sts:AssumeRoleWithWebIdentity" | Subject mismatch: braces missing, environment UUID from another repository, or step not a deployment step | Compare subjectFromWebIdentityToken from the failed event with the policy pattern |
BITBUCKET_STEP_OIDC_TOKEN is empty | OIDC not enabled for that step | Add oidc: true to the step |
| Audience or provider error | Provider created with a different URL or client ID, or values copied from another workspace | Recreate the provider with the values from the OpenID Connect settings page |
| CLI still uses old keys | Static AWS_ACCESS_KEY_ID variables still present take precedence in the credential chain | Remove or rename the static variables for that environment |
Rollback: during the pilot, revert the YAML change to restore key-based login; nothing in AWS needs undoing. After cut-over, deactivate the old keys (aws iam update-access-key --status Inactive), wait a full release cycle, then delete the keys and the secured variables at every level. See How to rotate an AWS IAM access key without downtime. To withdraw access, edit or delete the role's trust policy; sessions already issued last until they expire.
Decisions: environments, runners and IP conditions
| Decision | Option A | Option B | Guidance |
|---|---|---|---|
| Role scope | One role per repository | One role per repository and environment | Use B for anything that writes to production; A is acceptable for read-only checks |
| Build infrastructure | Atlassian-hosted steps | Self-hosted runners (runs-on) | OIDC works on both; on self-hosted runners, a non-ephemeral host can retain token files, so clean the workspace after each step |
| Source IP condition | None | aws:SourceIp on the trust policy | Atlassian does not recommend IP ranges as a security control: standard 1x and 2x steps run from broad AWS ranges, and a smaller fixed set needs atlassian-ip-ranges: true on 4x or larger steps. Treat IP conditions as defence in depth only |
| Accounts | One role in a shared deploy account | One role per target account | Per-account roles keep blast radius small; small teams may start with one account and split later |
Small organisations with a handful of repositories can manage roles by hand. Larger ones should create provider and roles from a dedicated infrastructure pipeline whose own role is not trusted by ordinary repositories, so a compromised repository cannot rewrite the trust that governs it. The same pattern for other platforms is in How to set up workload identity federation for GitLab CI to Azure.
Hardening and monitoring afterwards
- Disable OIDC where unused: set
oidc: falseon pull request and fork-triggered steps. - Protect the environments: branch restrictions on production deployment environments are part of the trust boundary.
- Alert on drift:
UpdateAssumeRolePolicyonbbp-*roles,CreateOpenIDConnectProvideroutside change windows, andAssumeRoleWithWebIdentitywhose subject contains a repository UUID not on your list. - Remove workspace-level AWS variables: they are inherited by every repository.
| Metric | Target |
|---|---|
| Secured variables holding AWS secret keys (all levels) | Zero |
Bitbucket-trusted roles without a sub condition | Zero |
| Production deploy roles bound to an environment UUID | All |
| Federated sessions from unknown repository UUIDs | Zero; investigate every one |
| Trust policy changes without a change record | Zero |
Migrating a whole workspace
- Rank repositories by exposure. Start with repositories whose keys reach production or hold IAM write rights, then shared workspace-level keys, then the long tail of read-only keys.
- Create the provider once per account. Because the issuer and audience are per workspace, one provider in each AWS account serves every repository; only roles are per repository and environment.
- Template the roles. Generate trust policies from a list of repository and environment UUIDs held in code, so every new repository gets a reviewed role rather than a copied key.
- Track the old IAM users. For each user, record the repositories that used it and its key last-used date. A user is ready for deletion only when every consumer has moved and the key has shown no use for a full release cycle.
- Close the door. Once migration is complete, use an SCP or a detective control to flag any new IAM user access key created for CI, so the old pattern does not creep back. See How to find unused AWS IAM roles and access keys.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| MITRE ATT&CK | T1552.001 Credentials In Files; T1078.004 Cloud Accounts; T1098.003 Additional Cloud Roles | Exposed static keys; abuse of valid cloud roles; tampering with role trust |
| NIST SP 800-53 Rev. 5 | IA-5, IA-5(7), AC-6, AU-6 | Authenticator management, no embedded static authenticators, least privilege, log review |
| NIST CSF 2.0 | PR.AA-01, PR.AA-05, DE.CM-03 | Credential management, least privilege, monitoring |
| OWASP NHI Top 10 2025 | NHI6 Insecure Cloud Deployment Configurations; NHI7 Long-Lived Secrets | Static CI keys and loose OIDC conditions |
Practitioner checklist
- List every repository, deployment and workspace variable that holds AWS keys.
- Create one Bitbucket OIDC provider per workspace in each target AWS account.
- Pin
audwithStringEqualsand always include asubcondition. - Bind production roles to repository UUID plus deployment environment UUID.
- Restrict which branches can deploy to production environments.
- Set
oidc: trueonly on steps that need AWS access. - Run positive and negative tests and check the subject in CloudTrail.
- Deactivate, then delete, old keys and secured variables at every level.
Standards and references
- Atlassian: Deploy on AWS using Bitbucket Pipelines OpenID Connect
- Atlassian: Integrate Pipelines with resource servers using OIDC
- Atlassian: Step options
- Atlassian: Variables and secrets
- AWS: Create an OpenID Connect identity provider in IAM
- AWS: IAM and AWS STS condition context keys
- AWS: Logging IAM and AWS STS API calls with AWS CloudTrail
- OWASP: Non-Human Identities Top 10
Related NHI Mgmt Group resources: CI/CD Pipeline Identity Security Guide · How to set up workload identity federation for GitHub Actions to AWS · How to federate Terraform Cloud to AWS · How to Audit CI/CD Secrets Scope · Multi-Cloud Workload Identity Troubleshooting Guide