GitHub Actions needs OIDC audience constraints

woodruffw1 pts0 comments

GitHub Actions needs OIDC audience constraints

ENOSUCHBLOG

Programming, philosophy, pedaling.

Home

Tags

Series

Favorites

Archive

Main Site

TILs

GitHub Actions needs OIDC audience constraints

Aug 10, 2026

Tags:

dear-github,

oss,

security

TL;DR : GitHub Actions should allow end-users to express audience constraints, to<br>make it harder for an attacker to pivot across services that use independent OIDC-bearing jobs.<br>They could do this with relatively small syntax tweak, although the backend implications are<br>probably nontrivial.

Like many CI/CD providers, GitHub Actions provides verifiable machine identities1<br>via OpenID Connect (OIDC).

These are awesome for a lot of reasons, not least of which is that they allow<br>workflows running on GitHub Actions to federate with other (third-party) services<br>without GitHub having to intermediate and pre-bless every interaction.

This is the backbone of how both Trusted Publishing and Sigstore work:<br>an individual workflows on GitHub Actions presents its machine identity<br>(via an OIDC token) to an external service, which then authenticates and<br>for some purpose (uploading to PyPI or signing artifacts, respectively).

Unfortunately, GitHub’s mechanism for exposing OIDC tokens in workflows<br>contains a significant weakness, one that (in my opinion) will present an<br>increasingly serious security risk over time. This post is about that weakness.

CI/CD and OIDC#

At the core of all “OIDC in CI/CD” implementations is some mechanism<br>that allows the workflow (pipeline definition, etc.) to request or<br>otherwise be pre-loaded with an OIDC identity.

Here’s how that looks in GitLab CI/CD:

my-job:<br>id_tokens:<br>PYPI_ID_TOKEN:<br>aud: pypi<br>script: |<br>do-something.sh --id-token "${PYPI_ID_TOKEN}"

and here’s the equivalent in GitHub Actions:

10<br>11<br>my-job:<br>runs-on: ubuntu-latest<br>permissions:<br>id-token: write<br>steps:<br>run: |<br>resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \<br>"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")<br>oidc_token=$(jq '.value'

do-something.sh --token "${oidc_token}"

The difference between these two is small but important: GitLab requires<br>the OIDC audience (the aud) to be declared up-front and statically ,<br>while GitHub requires the workflow to dynamically request a token<br>with an audience selected at runtime (the audience parameter<br>in the HTTP request).

Why does this matter?#

First, a very quick foray into OIDC.

Under the hood, OIDC is (mostly) just OAuth 2.0, and OIDC identity<br>tokens are just JSON Web Tokens (JWTs), with some additional2 constrains<br>on the claims they should express.

The most important claim in an OIDC ID token is arguably sub, since it identifies<br>the principal (the “subject”)3.

However, the second most important claim is aud, for the audience.<br>The audience is critical because it constrains who honors the token :<br>services that accept ID tokens should only do so when they recognize<br>the audience as matching theirs.

In other words: the aud claim prevents an ID token that’s intentionally<br>been issued for a specific service from being stolen by the attacker<br>and mis-applied to another service.

This is intended as a defense-in-depth: even if an attacker manages to exfiltrate<br>an OIDC credential, they should not be able to pivot to other services it.

It’s a flimsy defense but one that’s generally effective4, unless you<br>give the attacker the ability to control the aud claim as well.

Unfortunately, that’s exactly what GitHub Actions enables5: id-token: write gives the<br>job (or entire workflow) the ability to mint any ID token it pleases, with any<br>audience.

This matters a great deal in a world (our world) where jobs that are given id-token: write<br>also run a lot of third-party code: any vulnerability (or malware) in that code<br>has the potential to ask for new ID tokens for audiences that it isn’t supposed<br>to have access to.

We thought about this problem when designing Trusted Publishing, and came to the conclusion<br>that the machine identity that Trusted Publishing uses must include the workflow name,<br>preventing an attacker from impersonating pypi-publish.yml by inducing an ID token<br>from aws-deploy.yml with aud: pypi.

However, this constraint is not suitable for all possible use cases: many integrations<br>want to use just the org/repo slug as a sufficient identity, meaning that all<br>workflows are effectively co-equal when issuing ID tokens.

What should GitHub do?#

Add constraints! Ideally, something like this:

my-job:<br>runs-on: ubuntu-latest<br>permissions:<br>id-token: [pypi]

The basic idea here is to constrain what audiences the job<br>can request ID tokens for. In the example above, attempting to request<br>a token for aud: sts.amazonaws.com would cause an error, preventing a job<br>that’s intended only for publishing to PyPI from serving as a pivot to<br>AWS.

There are, of course, some potential downsides to this. For example,<br>some services might (inadvisedly) require dynamic information<br>in their audience, meaning that a value can’t be...

oidc github token audience actions pypi

Related Articles