Cached at:
08/10/26, 02:59 PM
# GitHub Actions needs OIDC audience constraints
Source: [https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints)
## ENOSUCHBLOG
## *Programming, philosophy, pedaling\.*
- [Home](https://blog.yossarian.net/)
- [Tags](https://blog.yossarian.net/tags)
- [Series](https://blog.yossarian.net/series)
- [Favorites](https://blog.yossarian.net/favorites)
- [Archive](https://blog.yossarian.net/archive)
- [Main Site](https://yossarian.net/)
- [TILs](https://yossarian.net/til)
---
## *Aug 10, 2026*Tags:[dear\-github](https://blog.yossarian.net/tags#dear-github),[oss](https://blog.yossarian.net/tags#oss),[security](https://blog.yossarian.net/tags#security)
---
**TL;DR**: GitHub Actions should allow end\-users to express*audience constraints*, to make it harder for an attacker to pivot across services that use independent OIDC\-bearing jobs\. They could do this with relatively small syntax tweak, although the backend implications are probably nontrivial\.
Like many CI/CD providers, GitHub Actions provides verifiable machine identities[1](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#fn:ident)via[OpenID Connect](https://openid.net/developers/how-connect-works/)\(OIDC\)\.
These are awesome for a lot of reasons, not least of which is that they allow workflows running on GitHub Actions to federate with other \(third\-party\) services*without*GitHub having to intermediate and pre\-bless every interaction\.
This is the backbone of how both[Trusted Publishing](https://docs.pypi.org/trusted-publishers/)and[Sigstore](https://sigstore.dev/)work: an individual workflows on GitHub Actions presents its machine identity \(via an OIDC token\) to an external service, which then authenticates and for some purpose \(uploading to PyPI or signing artifacts, respectively\)\.
Unfortunately, GitHub’s*mechanism*for exposing OIDC tokens in workflows contains a significant weakness, one that \(in my opinion\) will present an increasingly serious security risk over time\. This post is about that weakness\.
## CI/CD and OIDC[\#](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#cicd-and-oidc)
At the core of all “OIDC in CI/CD” implementations is*some*mechanism that allows the workflow \(pipeline definition, etc\.\) to request or otherwise be pre\-loaded with an OIDC identity\.
Here’s how that looks in GitLab CI/CD:
```
1
2
3
4
5
6
my-job:
id_tokens:
PYPI_ID_TOKEN:
aud: pypi
script: |
do-something.sh --id-token "${PYPI_ID_TOKEN}"
```
and here’s the equivalent in GitHub Actions:
```
1
2
3
4
5
6
7
8
9
10
11
my-job:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
run: |
resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
oidc_token=$(jq '.value' <<< "${resp}")
do-something.sh --token "${oidc_token}"
```
The difference between these two is small but important: GitLab requires the*OIDC audience*\(the`aud`\) to be**declared up\-front and statically**, while GitHub requires the workflow to**dynamically request**a token with an audience selected at*runtime*\(the`audience`parameter in the HTTP request\)\.
## Why does this matter?[\#](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#why-does-this-matter)
First, a very quick foray into OIDC\.
Under the hood, OIDC is \(mostly\) just[OAuth 2\.0](https://oauth.net/2/), and OIDC identity tokens are just[JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519)\(JWTs\), with some additional[2](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#fn:stringent)constrains on the claims they should express\.
The most important claim in an OIDC ID token is arguably`sub`, since it identifies the principal \(the “subject”\)[3](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#fn:sub)\.
However, the*second*most important claim is`aud`, for the*audience*\. The audience is*critical*because it**constrains who honors the token**: services that accept ID tokens should only do so when they recognize the audience as matching theirs\.
In other words: the`aud`claim prevents an ID token that’s*intentionally*been issued for a specific service from being stolen by the attacker and*mis\-applied*to another service\.
This is intended as a defense\-in\-depth: even if an attacker manages to exfiltrate an OIDC credential, they should not be able to*pivot*to other services it\.

It’s a flimsy defense but one that’s*generally*effective[4](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#fn:effective),**unless**you give the attacker the ability to control the`aud`claim as well\.
Unfortunately, that’s exactly what GitHub Actions enables[5](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#fn:others):`id\-token: write`gives the job \(or entire workflow\) the ability to mint*any*ID token it pleases, with*any*audience\.
This matters a great deal in a world \(our world\) where jobs that are given`id\-token: write`*also*run a lot of third\-party code: any vulnerability \(or malware\) in that code has the potential to ask for*new*ID tokens for audiences that it isn’t*supposed*to have access to\.
We thought about this problem when designing Trusted Publishing, and came to the conclusion that the machine identity that Trusted Publishing uses*must*include the workflow name, preventing an attacker from impersonating`pypi\-publish\.yml`by inducing an ID token from`aws\-deploy\.yml`with`aud: pypi`\.
However, this constraint is not suitable for all possible use cases: many integrations want to use*just*the`org/repo`slug as a sufficient identity, meaning that all workflows are effectively co\-equal when issuing ID tokens\.
## What should GitHub do?[\#](https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints#what-should-github-do)
Add constraints\! Ideally, something like this:
```
1
2
3
4
my-job:
runs-on: ubuntu-latest
permissions:
id-token: [pypi]
```
The basic idea here is to*constrain*what audiences the job can request ID tokens for\. In the example above, attempting to request a token for`aud: sts\.amazonaws\.com`would cause an error, preventing a job that’s*intended*only for publishing to PyPI from serving as a pivot to AWS\.
There are, of course, some potential downsides to this\. For example, some services might \(inadvisedly\) require dynamic information in their audience, meaning that a value can’t be statically pre\-declared\. I think this is rare enough in practice to not be worth blocking a security improvement and, when they*do*occur, GitHub could continue to allow the`id\-token: write`form as a \(less secure\!\) catch\-all\.
---
---