Series · 3 partsWhen Access Checks FailPart 1 · You are here
  1. 1The Pipeline Has No Secret. It Still Has a Cloud Identity.You are here
  2. 2The Signature Is Valid. The Token Still Belongs Somewhere Else.
  3. 3The Request Stayed Server-Side. The Credential Did Not.

CI/CD OIDC trust in 60 seconds

The best CI/CD security change most teams make is deleting the long-lived cloud key from their repository and replacing it with OpenID Connect. The workflow requests a short-lived identity token, the cloud exchanges it for temporary credentials, and there is no secret to rotate, leak, or find in an old build log.

That is a real improvement. It also creates a new failure mode that looks clean in every secret scanner: the credential is gone, but the right to mint one is broader than anyone realised.

OIDC does not remove trust from the pipeline. It moves trust into claims and policy conditions. If those conditions say “any workflow in this organisation” when the intended meaning was “the reviewed deployment job on the protected production branch,” the pipeline has become a cloud identity provider with an organisation-wide enrolment policy.

The secret was never the authorisation boundary

A static access key collapses authentication and authorisation into one string: whoever has the value can use the principal. OIDC separates them. GitHub authenticates the workflow and issues a signed token; AWS decides whether the claims in that token are allowed to assume a specific role.

The second decision is the load-bearing one.

For GitHub Actions, the AWS trust policy normally evaluates two claims:

  • aud identifies the intended token exchange, commonly sts.amazonaws.com when using the official AWS credentials action.
  • sub identifies the repository context that requested the token: repository, branch, tag, pull request, or environment, depending on the job.

The role’s permission policy answers what the session may do. Its trust policy answers which workflow may become that session. A least-privilege permission policy attached to a production role does not compensate for a trust policy that lets the wrong repository assume it. Least privilege begins one document earlier than most reviews start.

AUTHENTICATED WORKFLOW → CLAIMS → TRUST DECISION → CLOUD SESSION workflow run id-token: write may request JWT GitHub OIDC aud + sub + ref signed claims role trust CONDITIONS actual boundary STS session short-lived CLOUD IDENTITY THE SAME TOKEN ISSUER — TWO VERY DIFFERENT TRUST SURFACES repo:ORG/* any matching repository may become the role large, changing identity population repo + protected environment named source, reviewed deployment boundary small, intentional identity population
The OIDC provider proves where a token came from. The role trust policy decides whether that source is entitled to become a cloud identity. Short-lived credentials reduce shelf life; precise claim conditions reduce who can mint them.

One wildcard changes the unit of compromise

AWS refuses a new GitHub trust policy whose sub condition is absent, null, or only a bare wildcard. That guardrail is useful, but it does not make every accepted pattern narrow. This condition is syntactically constrained and operationally enormous:

{
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
  },
  "StringLike": {
    "token.actions.githubusercontent.com:sub": "repo:example-org/*"
  }
}

It means the security boundary is no longer one deployment repository. It is every current and future repository whose subject matches that organisation pattern, plus every actor and workflow path that can cause one of those repositories to request a token.

The mistake is easy to make because the policy reads like central governance: “our GitHub organisation may deploy.” But organisations contain archived projects, migration tooling, internal experiments, forks, and repositories with very different branch protections. A single low-sensitivity repository becomes relevant to production the moment its workflow can satisfy the same sub pattern as the deployment repository.

The reliable boundary is the smallest stable claim set the provider and cloud can both enforce: exact repository identity, the expected audience, and either a protected branch or a protected GitHub environment. When an environment appears in the job, the default subject shape changes to include that environment rather than the branch. That is not cosmetic; the trust policy and the repository’s environment protection rules now form one control.

Test the issuer’s vocabulary before writing policy

Do not begin by copying a trust-policy example. Begin by observing the claims produced by each workflow context you intend to distinguish: production environment, main branch, pull-request job, tag release, and reusable workflow.

In an authorised test repository, request the token only long enough to decode its header and claims locally. Do not log the encoded token or exchange it for a production role.

permissions:
  contents: read
  id-token: write

jobs:
  inspect-claims:
    runs-on: ubuntu-latest
    environment: oidc-lab
    steps:
      - uses: actions/github-script@v8
        with:
          script: |
            const token = await core.getIDToken('sts.amazonaws.com')
            const claims = JSON.parse(Buffer.from(token.split('.')[1], 'base64url'))
            core.info(JSON.stringify({ aud: claims.aud, sub: claims.sub, ref: claims.ref }))

Use a non-privileged lab role whose permission policy allows only sts:GetCallerIdentity. The positive case should assume it from the intended environment. The negative cases should fail from a feature branch, a pull request, a second repository, and a job that does not reference the protected environment. If one of those succeeds, the trust policy is broader than its author thinks.

Do not print the full token. It is a bearer credential for its short lifetime, and a log is still a disclosure even when the value expires quickly.

Names are not always the most durable identity

Repository names made OIDC policy readable, but names can be renamed, transferred, and in some systems reused. GitHub’s current OIDC reference documents an immutable subject format for newer or opted-in repositories that includes owner and repository IDs. Older repositories may still emit the previous name-based form until migrated.

That transition creates a review question most examples omit: which subject format does this repository actually issue today? A policy written for the new shape will deny an old repository. A migration performed without updating the cloud trust will break deployment. A fallback wildcard added during the outage can quietly preserve both formats forever and leave the role broader than before.

Treat subject-format migration like an identity migration: capture the current claims, stage an exact new condition, run positive and negative controls, then remove the old form. Compatibility should be a measured window, not a permanent wildcard.

Evidence matrix

SignalWhat it provesNegative controlDefender verification
Observed aud and sub from the intended protected jobThe issuer vocabulary and exact deployment identity are knownCollect claims from a pull request, feature branch, and second repository; each should differ in the expected fieldCompare live claims with the trust-policy operators and values
Lab role assumption succeeds only from the approved environmentThe claim conditions enforce the intended source boundaryAttempt the same role from every near-neighbour context and require denialCorrelate AssumeRoleWithWebIdentity events with repository, subject, role, and session name
Organisation or repository wildcard matches more identities than documentedThe unit of compromise is larger than the deployment repositoryReplace it with exact conditions and confirm legitimate deployment still worksEnumerate every repository and workflow that can satisfy each wildcard pattern
Environment protection blocks an unreviewed deployment jobRepository controls and cloud trust compose into one boundaryRemove reviewer/branch eligibility in the lab and confirm the token can no longer satisfy the production pathReview environment reviewers, deployment branches, administrators, and bypass permissions
Immutable subject migration accepts the new form and denies the retired formCompatibility did not leave a second permanent trust routePresent a token using the old subject shape after the cutover and require denialTrack subject format, repository ID, owner ID, and policy migration date

The pattern I keep seeing

The team celebrates deleting the cloud secret, closes the credential-rotation ticket, and never assigns ownership of the trust policy that replaced it. Platform engineering owns the workflow. Cloud security owns IAM. Repository administrators own environments. The identity exists only when all three systems agree, so no single owner sees the complete enrolment rule.

That is why the durable finding is not “OIDC is misconfigured.” It is: “this production role accepts tokens from these additional workflow contexts, and this one condition is where the boundary expands.” The path should be as concrete as any other identity escalation path.

What to hand the defenders

The resolved trust population. Translate each StringLike pattern into the repositories, branches, tags, pull-request jobs, and environments it actually matches today.

A claim contract. Record the expected issuer, audience, subject format, repository identity, and deployment context. Test that contract after repository transfers, environment changes, or subject-format migrations.

The negative cases. A successful production deployment proves availability. It does not prove exclusivity. Keep automated denial tests for the nearest untrusted contexts, because those are what detect a trust policy becoming broad again.

The pipeline no longer stores a cloud credential. It stores something more important: the policy that decides who may ask for one.

Sources & freshness

How current is this note?

Sources checkedApril 16, 2026

The latest source-review, content-update, or publication date is shown.

ReviewAuthor review complete

The author completed a technical review. This does not, by itself, claim lab reproduction.