publish.yml uploads a package. deploy.yml assumes a cloud role and rolls production. Both live in the same repository, and the cloud side trusts org/repo as the identity behind GitHub Actions.
That setup feels segmented because the workflows do different jobs. GitHub's OIDC flow does not preserve that segmentation. Any job with id-token: write can ask GitHub for an ID token with an audience chosen at runtime. If your cloud trust policy keys on repository identity alone, a compromised publish job can mint a token for the deployment audience and walk into deploy privileges.
A recent post on ENOSUCHBLOG shows the design gap cleanly: GitHub lets the job choose aud, while GitLab asks you to declare it in the job config up front https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints . The argument here is narrow: GitHub Actions makes the minting decision broad, so you need to scope trust by workflow, and often by environment. A repository is too wide for that job.
That fan-out is the story.
GitHub makes audience a job decision
The shape from the source is small and important.
GitLab asks you to declare the audience in the job definition:
my-job:
id_tokens:
PYPI_ID_TOKEN:
aud: pypi
script: |
do-something.sh --id-token "${PYPI_ID_TOKEN}"
GitHub gives the job a minting permission, then the job picks the audience at runtime:
my-job:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- run: |
curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi"
That difference decides the risk model. In OIDC, aud tells a relying party which service should honor the token. PyPI should accept tokens minted for PyPI. A cloud security token service should accept tokens minted for that service. That check limits replay across services.
GitHub's design hands that choice to the workload. Once you grant id-token: write, you did not approve one audience. You approved the ability to ask.
The source is blunt about the consequence: jobs with id-token: write often run third-party code, and vulnerable or malicious code can request new ID tokens for audiences it should not have. Even if every relying party checks aud correctly, that defense gets weaker when the attacker can select the audience in the first place.
Repo trust makes workflows co-equal
This is where many teams end up broader than they think. They separate workflows in Git, then collapse them again in cloud trust.
Trusted Publishing already had to solve this. According to the source, its machine identity includes the workflow name specifically to stop aws-deploy.yml from impersonating pypi-publish.yml by requesting aud: pypi https://blog.yossarian.net/2026/08/10/github-actions-needs-oidc-audience-constraints . That is a strong real-world example because it shows the platform designers ran into the same boundary problem and pushed identity below the repository.
Many other integrations still want org/repo as the whole principal. The source's wording is useful here: workflows become co-equal when issuing ID tokens. If your cloud trust policy says, in effect, "any valid token from this repository may assume this role," then every OIDC-bearing workflow in that repo shares the same trust surface.
This flow keeps the pivot in one picture.
sequenceDiagram
participant P as publish.yml
participant GH as GitHub OIDC
participant C as Cloud trust
participant D as Deploy API
P->>GH: Request token aud=sts.amazonaws.com
GH-->>P: ID token for org/repo
P->>C: Exchange token for cloud creds
C-->>P: Deploy credentials
P->>D: Use deploy privileges
Nothing in that path required access to deploy.yml. The publish job only needed id-token: write and a trust policy that treated repository identity as sufficient.
That is why a repository works poorly as the security boundary here. It is a source-control boundary. OIDC trust wants a narrower principal.
Workflow identity is the control point you own
GitHub does not give you a per-job audience allowlist today. Your control point sits on the relying party side, which for most teams means the cloud trust policy, federated credential, or workload identity configuration.
The practical move is boring: create more identities, each with less reach.
For most repos, that means splitting at least these paths:
package publishing
artifact signing
infrastructure change
application deployment
Production deserves a separate principal again. If your platform can bind trust to an environment attribute, use it. If it cannot, split the production path into a distinct workflow so the workflow itself becomes the enforceable boundary.
The policy syntax differs across AWS, Azure, and GCP. The shape does not. Match the narrowest claims your platform lets you enforce, and prefer structured claims over one composite subject string. I would avoid leaning too hard on sub if you have better options. The source calls out a real reason: delimiter-heavy sub formats have caused security bugs when attacker-controlled parts of identity interact badly with parsing rules. Separate identity parts are easier to review and harder to misread.
A good target state looks like this:
one cloud principal per workflow purpose
a separate principal for production
audience checks kept in place for the target service
no trust rule that makes every workflow in the repo interchangeable
That gives you a boundary you can explain in one sentence during a review. "This role exists for deploy.yml in production." You cannot say that cleanly when the policy says only org/repo.
Treat the mint as the risky permission
Least privilege in Actions starts at the mint. id-token: write lets the job create fresh credentials on demand.
Teams often focus on what a token can do after issuance. GitHub's runtime audience choice means you also need to focus on who can mint one, from which workflow, after running which code. A release workflow that installs dependencies, invokes third-party actions, and then mints a token carries more trust than its YAML name suggests.
That changes the review order.
First, remove id-token: write from every job that does not federate with anything. Second, examine every remaining job as a credential minting surface. Third, make sure the relying party will only honor tokens from the intended workflow and environment.
A bad package, a compromised action, or a shell injection bug inside publish.yml does not need a bug in deploy.yml. It only needs a repo-scoped trust rule waiting on the other side.
GitHub should add audience constraints
The product fix is clear enough. The source proposes a syntax in this shape:
permissions:
id-token: [ pypi ]
If GitHub supported that model, a job meant to publish to PyPI could not turn around and request aud: sts.amazonaws.com without failing. I think that is the right direction. Some services may want dynamic audience values, and GitHub can keep id-token: write as an explicit escape hatch for those cases.
You still need a local policy now. GitHub shipping audience constraints later will not shrink your current blast radius. Your cloud trust policy can do that today.
Each workflow that can mint an OIDC token deserves its own cloud identity, and production deserves a separate one again. If your trust policy still says only org/repo, you accepted cross-workflow pivot risk inside that repository.
Steal this
Inventory every job with id-token: write.
Write down the intended relying party and audience for each one.
Create a separate cloud identity for each workflow purpose: publish, sign, deploy, infra-update.
Give production its own identity. Use an environment-scoped condition if your platform supports it. Otherwise split production into its own workflow.
Delete trust rules that treat all workflows in org/repo as the same principal.