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...