For two years I have been repeating the same sentence to the teams I work with: "move to OIDC, remove your AWS_ACCESS_KEY_ID from GitHub secrets". It is still true. But after the May 2026 attacks, actions-cool/issues-helper, then Megalodon and its 5,561 boobytrapped repositories, I no longer phrase the advice the same way.
The problem is not OIDC. The problem is that OIDC has become an end point in the security narrative, when it should be a starting point. The two May attacks do not use exactly the same technique, but they exploit the same forgotten boundary: the CI/CD runner.
With actions-cool/issues-helper, the attacker hijacks action tags and gets an imposter commit executed that can read the memory of the Runner.Worker process. With Megalodon, he injects malicious GitHub Actions workflows directly into public repositories and collects environment variables, credential files, kubeconfigs, SSH keys and OIDC tokens.
In both cases the lesson is the same: OIDC removes static cloud credentials, but it does not make harmless the code that runs inside a job allowed to handle temporary credentials. This post is a correction of my own narrative; I am not going to stop recommending OIDC, I am spelling out what must be stacked around it for the argument to hold in 2026.
What does OIDC actually solve?
The historical scenario fits in one line: an AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY secret stored in GitHub Secrets, valid for months, copied by hand from one collaborator to another, forgotten in a fork, exfiltrated the day a third-party workflow runs with too many rights.
OIDC breaks that scheme. GitHub signs a JWT describing the identity of the job (repo, ref, workflow, job, environment). The target cloud verifies that signature, applies its trust policy, and returns temporary credentials valid for 15 minutes to 1 hour. No more long-lived credential to protect on the GitHub side.
That is exactly what I document in the guide OIDC GitHub Actions : authentification sans secrets and its cloud counterpart OIDC AWS, Azure, GCP. The implementation is simple, the posture improvement is real, and I am not walking back that recommendation.
What does OIDC not solve?
The temporary credential obtained through OIDC is not a secret stored durably in GitHub, but it does become exploitable data during the execution of the job: an environment variable (AWS_ACCESS_KEY_ID, AWS_SESSION_TOKEN exported by default by aws-actions/configure-aws-credentials), a credentials file, a token requested by a step, or an artifact reachable from the runner context depending on the action used.
Any code that runs during that window in the same runner sees that credential. A malicious third-party action, an injected workflow, a post-install script of an npm dependency, a read of /proc/<PID>/environ or /proc/<PID>/mem: all of them can extract this sensitive data.
That is exactly Megalodon's modus operandi: direct injection of a malicious workflow that collects the runner's environment variables, credential files, kubeconfigs, and the OIDC tokens requested from the job. A cloud token valid for 15 minutes is enough for an industrial attacker; he has the tooling to exfiltrate it and reuse it within the validity window.
The illusion to correct is therefore simple: "OIDC = no stealable credentials" is false. OIDC removes persistent credentials. It does not remove ephemeral credentials. Both families are stealable, but with very different risk windows.
Which scenario did I fail to see coming?
Picture a clean deploy.yml workflow, the kind I have written dozens of. Default permissions read-only. id-token: write set only on the deployment job. AWS trust policy requiring repo:my-org/my-repo:ref:refs/heads/main. aws-actions/configure-aws-credentials action pinned by SHA. On paper, this is what we call a hardened workflow.
jobs: deploy: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@<SHA> - uses: aws-actions/configure-aws-credentials@<SHA> with: role-to-assume: arn:aws:iam::123456789012:role/deploy aws-region: eu-west-3 - run: ./scripts/deploy.shThe problem stays invisible as long as no foreign step is inserted. The day an attacker injects a step before or after configure-aws-credentials, through a malicious commit on .github/workflows/, a third-party action compromised via its tag, or a dependency that runs during ./scripts/deploy.sh, he grabs either the GitHub JWT (before the exchange) or the temporary AWS credentials (after the exchange).
In both cases, the attacker has a 15 to 60 minute window to assume the deploy role from his own infrastructure. That is more than enough to automate an exfiltration or plant a cloud backdoor.
Which four layers must you stack around OIDC?
OIDC is necessary but not sufficient. Here are the four layers I now consider mandatory in any GitHub Actions architecture that touches production.
A strict cloud trust policy. The sub condition of the JWT must be constrained as much as possible: precise repo, precise branch or precise environment. Avoid broad StringLike and organization wildcards; prefer StringEquals on a precise sub whenever possible. It is the first barrier, and it is what prevents a forked workflow or a test branch from assuming the production role.
Minimal GitHub permissions. id-token: write set only on the job that needs it, never at workflow level. contents: read by default. See the guide Permissions GitHub Actions GITHUB_TOKEN for the details.
Egress control on the runner. This is the most forgotten layer. If a malicious payload cannot reach the attacker's infrastructure, the credential exfiltration fails. Harden-Runner in block mode or a firewall allow-list on a self-hosted runner do exactly this job. I detail the patterns in Hardening de l'environnement de build.
Strong workflow governance. The .github/workflows/ directory must be treated as production code: CODEOWNERS forcing a review by the security team, SHA pinning of third-party actions, strict branch protection. That is precisely the lesson of the attack on actions-cool/issues-helper which preceded Megalodon by a few hours.
None of these layers replaces the others. And none of them makes OIDC optional. They stack.
How do you harden an AWS trust policy after Megalodon?
This is the mistake I see most often in audits: an AWS trust policy written to "work everywhere in the organization", hence too broad to withstand a targeted attack.
The pattern to avoid:
{ "Condition": { "StringLike": { "token.actions.githubusercontent.com:sub": "repo:my-org/*" } }}The StringLike with a wildcard lets any repo in the organization assume the role. Any branch, any workflow, any environment. The day a peripheral repo of the organization is compromised, the attacker assumes the production role without forcing anything.
The pattern to aim for:
{ "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:environment:production" } }}StringEquals instead of StringLike. A sub limited to a precise repo and a precise environment, not a branch, because an attacker who pushes a commit to main remains legitimate, whereas an attacker who requests the assumption from the production environment must first pass that environment's approval gate. It is the chaining that hardens, not each link in isolation.
StringLike is not forbidden as such; GitHub's own documentation shows wildcard examples to allow several branches or environments of the same repo. The problem is the overly broad wildcard (repo:my-org/*). If StringLike is necessary, restrict it to a strict, documented and tested pattern.
The exact format of the sub claim depends on the trigger (branch, tag, environment, pull request). Check the documentation before pasting an example into your trust policy; a malformed sub blocks the deployment without a clear message.
Why are GitHub Environments the missing piece?
GitHub Environments remain underused. That is a shame, because they add exactly the piece missing from an OIDC workflow: a human gate before the token exchange.
A production environment can require:
- required reviewers (mandatory human approval before the job starts);
- a wait timer (forced delay before execution);
- allowed branches (only
maincan deploy toproduction); - secrets scoped to the environment, invisible to other jobs.
Combined with an AWS trust policy that requires environment:production in the sub claim, this gives a coherent defensive chain: no deployment without human approval, and no valid OIDC token without a validated environment. A protected environment would not have made Megalodon impossible, but it would have changed the dynamics; the injected workflow could no longer silently obtain a production token without going through an approval rule, a branch restriction or a visible deployment control.
It is the layer I forgot most often in my recommendations. I no longer forget it.
What did I change in my own advice?
Before May 2026, I said: "move to OIDC". Full stop, as if the absence of a static credential settled the matter.
Today, I say: "move to OIDC, AND enforce a protected environment, AND scope the trust policy to a precise sub, AND cut the runner's egress, AND protect .github/workflows/ with CODEOWNERS". Five requirements instead of one.
It is heavier to state. It is also more honest. OIDC remains the right foundation. But a foundation alone is not a house, and an attack like Megalodon reminds us that an industrial attacker does not stop at the most visible wall; he looks for the open window.
Which mistakes do I see most often in audits?
Four patterns come back in almost every GitHub Actions audit:
id-token: writeat workflow level instead of job level. Every job in the workflow can then request an OIDC token, even the ones that do not need it. It is the first thing to fix.- Trust policy in
StringLikerepo:my-org/*. Replace it withStringEqualson a precise sub, ideally with an environment. - No protected environment on the deployment job. Without a human gate, any commit that passes CI deploys to production with the cloud credentials.
- No egress control on the runners, neither
Harden-Runnernor a firewall allow-list. A payload that steals the credentials can exfiltrate them without any network constraint.
None of these mistakes is dramatic in isolation. All four together form exactly the playground that attacks in Megalodon's family expect.
What does this post not say?
This post does not say that OIDC is a bad practice.
On the contrary: OIDC remains preferable to static cloud credentials stored in GitHub Secrets. It sharply reduces the lifetime of credentials and lets you constrain the workflow's identity through claims such as repo, ref, environment or workflow.
But OIDC does not protect against a malicious workflow already allowed to request a token. The right question is therefore not "OIDC or no OIDC?". The right question is: "what code can run inside the job that has id-token: write?"
Post-Megalodon quick checklist
This is the audit pass I advise running on each repository in the days following an incident like Megalodon. Short, operational, and largely doable in half a day per project.
- Review every workflow that declares
id-token: write. - Move
id-token: writeto job level, never global. - Attach production jobs to a protected GitHub Environment.
- Restrict the cloud trust policy to a precise
sub. - Add
CODEOWNERSon.github/workflows/. - Block or monitor the runners' egress.
- Search for workflows named
SysDiagandOptimize-Build(Megalodon IOCs). - Audit recent commits on
.github/workflows/since 18 May 2026.
Key takeaways
- OIDC removes persistent cloud credentials, not the ephemeral credentials that live inside the runner during the job.
- Megalodon and actions-cool exploit this grey zone: stealing the ephemeral token from the runner's memory is enough for the attacker.
- OIDC alone is no longer a complete answer in 2026. It must be stacked with a strict trust policy, minimal permissions, egress control and workflow governance.
- The trust policy must use
StringEqualson a precise sub including the environment, notStringLikeon an organization wildcard. - GitHub Environments with required reviewers are the most forgotten and most effective piece to block an injected workflow.
id-token: writeat job level only, never at workflow level.- The
.github/workflows/directory is production code: CODEOWNERS, mandatory review, SHA pinning of third-party actions.