GitHub Actions to cdk deploy (OIDC role to CDK bootstrap roles)

The handoff between a GitHub Actions OIDC role and the CDK bootstrap roles — the OIDC role only needs to assume cdk-<qualifier>-* roles (and read the bootstrap version), same-account and cross-account variants, why a missing sts:AssumeRole silently falls back to the CI role's own permissions, the workflow, and how to verify.

markdownpatternv4D5D8-JB1Tstage: deploytrust: officialservice: cdkservice: iamservice: stsinterface: iacinterface: cliGitHub Actions CDK deployOIDC CDKcdk deploy CI role permissionssts:AssumeRole cdk-hnb659fdscross-account CDK pipelinecdk bootstrap --trust CI accountProceeding anyway CIconfigure-aws-credentials cdkleast privilege CDK CI role

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

GitHub Actions to cdk deploy

The GitHub OIDC role should hold almost no permissions of its own: it only needs to assume the CDK bootstrap roles. The bootstrap roles do the publishing and deploying, and CloudFormation's execution role creates the resources. Set up the OIDC provider and trust policy first (guides/iam/ci-oidc-github.md).

The chain

GitHub job ──AssumeRoleWithWebIdentity──▶ gha-deploy (your OIDC role)
gha-deploy ──AssumeRole──▶ cdk-<q>-file-publishing-role / image-publishing-role  (upload assets)
gha-deploy ──AssumeRole──▶ cdk-<q>-lookup-role                                   (context lookups, if any)
gha-deploy ──AssumeRole──▶ cdk-<q>-deploy-role ──PassRole──▶ cdk-<q>-cfn-exec-role ◀── CloudFormation

Same account

The bootstrap roles trust the account ("AWS": "<account-id>"). An account principal in a trust policy delegates the decision to IAM, so the OIDC role's own policy must allow the assume:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AssumeCdkBootstrapRoles",
    "Effect": "Allow",
    "Action": ["sts:AssumeRole", "sts:TagSession"],
    "Resource": "arn:aws:iam::<account-id>:role/cdk-hnb659fds-*"
  }]
}
  • Without this policy, nothing fails at first. The CLI warns … could not be used to assume '<role-arn>', but are for the right account. Proceeding anyway. and tries with the OIDC role's own credentials. If you gave that role broad permissions "to make it work", every resource is now created as the CI role, bypassing your scoped execution policy. Keep the OIDC role narrow, so that this mistake fails loudly.
  • Don't pass an external ID. The bootstrap roles deny AssumeRole calls that carry one (DenyExternalId defaults to true).
  • With a custom qualifier, replace hnb659fds in the ARN.

Cross-account (tools account deploys to prod)

  1. In prod, bootstrap with --trust <tools-account> (keep any accounts already trusted) and an explicit --cloudformation-execution-policies (guides/cdk/bootstrap-and-deploy.md).
  2. In tools, the OIDC role's policy allows sts:AssumeRole (and sts:TagSession) on arn:aws:iam::<prod-account>:role/cdk-hnb659fds-*.
  3. The stack's env.account is prod. If either half is missing, the CLI stops with Could not assume role in target account … (reference/cdk/errors.md). Unlike same-account, there's no fallback.

Workflow

permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production        # trust policy sub: repo:<org>/<repo>:environment:production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: npm ci
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::<tools-or-same-account>:role/gha-deploy
          aws-region: <region>
          role-session-name: gha-${{ github.run_id }}
      - run: npx aws-cdk deploy --all --require-approval never

Run --require-approval never only behind something that reviews changes, such as a protected GitHub environment or a separate --method prepare-change-set job.

The workflow above is assembled from the referenced docs and wasn't run here. The action versions for checkout and setup-node are UNVERIFIED; pin the ones your organization approves.

Known-good and known-bad

Known-good:

  • OIDC role trust: pinned to the repository and environment.
  • OIDC role policy: only sts:AssumeRole + sts:TagSession on cdk-<q>-* in the target account.
  • Target account: bootstrapped with --trust for the tools account (or same-account) and a scoped --cloudformation-execution-policies.
Known-badSymptom
OIDC role has AdministratorAccess but no sts:AssumeRole on cdk-* (same account)Deploys "work", with Proceeding anyway in the log. Resources are created by the CI role, bypassing the scoped execution policy
OIDC role is narrow and lacks sts:AssumeRole on cdk-*Same-account fallback, then failures on S3 asset upload (forbidden: null) or CloudFormation calls
Target bootstrapped without --trust <tools-account>, or a later bootstrap dropped itCould not assume role in target account …
Trust policy uses StringLike repo:<org>/<repo>:*Works, but any branch or PR in the repo can deploy: a silent security bug
Workflow lacks id-token: writeThe credentials step fails before any CDK call

Which side is wrong:

Where it failsSide
The configure-aws-credentials stepGitHub ↔ OIDC role: trust policy sub/aud, provider, id-token: write (guides/iam/ci-oidc-github.md)
cdk deploy before any stack event, with assume or bootstrap messagesOIDC role ↔ bootstrap roles: role policy, --trust, qualifier (reference/cdk/errors.md)
Stack events (CREATE_FAILED)Execution role ↔ resources: execution policies, service quotas (guides/cloudformation/fix-failed-stack.md)

Verify

  • The deploy log has no "Proceeding anyway" line.
  • In CloudTrail, sessionContext.sessionIssuer.arn on CreateChangeSet / ExecuteChangeSet is cdk-<q>-deploy-role-<account>-<region>, not gha-deploy. Resource-creating events show cdk-<q>-cfn-exec-role-… with invokedBy set to CloudFormation. To walk back to the GitHub run, follow the AssumeRole events to the AssumeRoleWithWebIdentity event whose session name is gha-<run id> (reference/cloudtrail/event-records.md).
  • Push from a branch or environment the OIDC trust policy doesn't allow: the credentials step must fail.

Read this document as markdown — the same bytes an agent receives.