What GitHub already gives you, what the paid features add, and which controls are worth enabling first
GitHub Actions is easy to adopt, which is one of the reasons it is so popular. It is also one of the reasons it shows up so often in discussions around Poisoned Pipeline Execution (PPE), one of the risks highlighted in the OWASP Top 10 CI/CD Security Risks.
There is already a lot of good PPE content out there, especially after several prominent GitHub Actions security incidents and, more recently, the compromise of the trivy-action repository in March 2026. So I do not want to re-explain PPE in depth here. In short: if an attacker can influence what your workflow executes, or what trusted action code your workflow pulls in, your CI system can become part of the attack path. Hence your pipeline is poisoned.
In practice, that usually starts with normal-looking workflow decisions:
- too many token permissions
- unreviewed workflow changes
- broad trust in marketplace actions
- unsafe handling of pull request input
- deployment secrets that are available too early or too broadly
The uncomfortable part is that none of these look especially dramatic in a pull request. They often look like convenience.
That is also the main theme of this article: before adding more tooling, it is worth asking how far GitHub’s own built-in controls already get you. The answer is: quite far, if you focus on the right ones.
So this article is intentionally practical. It is less about explaining PPE theory and more about what to enable in GitHub, why it matters, and what I would actually recommend teams do first. For the exhaustive configuration reference, have a look a the appendix to this article.
The Main Principle: Treat Workflows as Privileged Code
The most important mindset shift is simple: a workflow file is not harmless configuration.
It is executable control logic for a privileged system. It may decide:
- which code runs
- which secrets are exposed
- which identities can write back to the repository
- whether a deployment happens
- whether cloud credentials are minted through OIDC
That means .github/workflows/* deserves the same level of review as production code handling authentication, release logic, or infrastructure changes.
If a team protects application code carefully but treats workflow YAML as plumbing, it has probably classified the wrong thing as low risk.
1. Lock Down the Default Token Permissions
If you only make one configuration change this week, make it this one.
Set the default GITHUB_TOKEN permissions at org or repository level to read-only, and then set permissions: {} explicitly at workflow level.
Why both?
- The org or repo setting gives you a safe default for everything in the repository.
- The explicit workflow-level declaration makes the intent visible in code review.
Without this, many workflows run with more access than they need. That is dangerous because the token is usually the first thing an attacker will try to abuse once they gain execution in a job.
Recommended setup
At org or repo level:
- set workflow permissions to restricted, meaning read-only for contents and packages
- do not allow workflows to create or approve pull requests unless you have a very specific reason
Inside workflows:
permissions: {}
jobs:
build:
permissions:
contents: read
Why this matters
If a build job only needs to read source code, it should not be able to push commits, create releases, or write packages. That sounds obvious, but the default state in many repositories is closer to „one token to rule them all.“
That is usually where incidents begin: not with one catastrophic design choice, but with five small permissions that nobody removed.
2. Protect Workflow Changes Like High-Risk Changes
Workflow files should not be casually editable.
The minimum baseline is:
- protect your default branch
- require pull requests
- require approval from someone other than the last pusher
- dismiss stale approvals when new commits are pushed
- add CODEOWNERS for
.github/workflows/**and custom actions
The last two settings matter more than they first appear to.
„Dismiss stale approvals“ prevents the classic situation where a benign version of a change gets approved and a risky extra commit is pushed afterward. „Require approval from someone other than the last pusher“ closes the equally annoying loophole where the final modifier effectively reviews their own change.
And CODEOWNERS matters because workflow changes should reliably land in front of people who understand the consequences.
There is also an organizational lesson here. If only one person is realistically able to review workflow changes, that is already a risk signal. It’s dangerous to go alone.
3. Restrict Which Actions May Run
This is usually the point where the security discussion meets developer experience.
In theory, the strictest answer is easy: allow nothing except an explicit allowlist. In practice, that is often too rigid as a starting point and can create enough friction that people begin bypassing the process.
A practical default is this:
- allow GitHub-owned actions
- allow Marketplace actions by verified creators
- maintain an explicit allowlist for additional trusted actions
- require full commit SHA pinning
That gives you a much better trust boundary without forcing every team to open a governance ticket for every harmless improvement.
Important distinction
The allowlist and SHA pinning solve different problems.
- The allowlist controls which action repositories are allowed.
- SHA pinning controls which exact revision is executed.
If you only allow an action but reference it by tag, you still trust that the tag will not move. That is not an assumption I would base a security-sensitive workflow on.
This is also one of the places where the PPE framing matters most. The problem is not just „third-party dependencies are scary“ in the abstract. The problem is that GitHub Actions workflows routinely execute code fetched from outside your repository with whatever trust boundary the job happens to have. If that code changes underneath you, or if you introduce a new action without sufficient review, you have created a very direct path into your pipeline.
4. Pin Actions to Full Commit SHAs
Pinning to full SHAs is one of the few security recommendations in GitHub Actions that is both slightly annoying and completely justified.
This:
- uses: actions/checkout@v4
is easy to read, but it is not immutable.
This:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
is less elegant to look at, but it is the version you actually reviewed.

That matters because tags can move. If a third-party action gets compromised and a tag is retargeted, every workflow pinned only to the tag becomes part of the surprise party.
Make the tradeoff manageable
The operational downside of SHA pinning is maintenance. The practical answer is not to abandon pinning. It is to automate updates.
For GitHub-native automation, Dependabot can keep GitHub Action references current. That turns SHA pinning from a manual chore into a reviewable maintenance flow.
5. Do Not Let Workflows Approve Their Own Changes
There is a category of Actions convenience features that becomes risky very quickly when mixed with broad permissions or weak review boundaries.
One example is allowing GitHub Actions to create and approve pull requests.
There are legitimate automation cases for workflows opening pull requests, for example dependency updates or generated changelog changes. But approval is a different matter. Once automation can both create and approve a change, you are very close to removing the human control point you probably wanted to keep.
My recommendation is straightforward:
- allow automated PR creation only where it serves a clear maintenance purpose
- do not allow workflows to approve PRs by default
- keep a human reviewer in the path for changes that affect release, deployment, workflow logic, or secrets
If a process is important enough to automate, it is important enough to think through carefully.
6. Protect Sensitive Deployments with Environments, Not Just Secrets
A common anti-pattern is putting sensitive deployment credentials into repository secrets and calling it a day.
The better GitHub-native model is to use environments with required reviewers.
That gives you two advantages:
- the deployment secret is scoped to the environment
- the job pauses until an authorized reviewer approves it
This is a far better control than letting any workflow with the right branch trigger immediately receive production credentials.
If you are deploying to production, the deployment job should cross a deliberate gate.
Practical recommendation
For anything production-like:
- create a dedicated environment such as
production - attach required reviewers
- enable self-review prevention if available for your plan
- store the production secrets there, not as repository-wide secrets
This gives you a clean separation between „the workflow ran“ and „the workflow may now touch production.“
7. Prefer OIDC Over Long-Lived Cloud Credentials
This is one of the clearest cases where GitHub supports the right security model, but teams still frequently use the older, weaker one.
If your workflows authenticate to AWS, Azure, GCP, or another cloud provider, prefer OIDC-based short-lived credentials over static secrets stored in GitHub.
Why?
- static secrets are long-lived
- they need rotation
- they are high-value targets
- once exposed, they often remain valid far longer than they should
OIDC is better because the workflow requests a short-lived token at runtime, assumes a tightly scoped role, and that credential expires automatically.
This does require some cloud-side configuration, but it is one of the best upgrades you can make if your workflows touch infrastructure.
8. Where Paid GitHub Security Features Help
The paid GitHub security features are useful. That is not really in question.
The more relevant question is where they add enough value to justify the cost.
The strongest candidates are usually:
- secret scanning for private repositories
- push protection for private repositories
- dependency review in private pull requests
- private code scanning where CodeQL is a good fit
- broader organization-wide security visibility
The mistake would be to treat this as an all-or-nothing decision.
You do not need every paid feature to improve GitHub Actions security meaningfully. But some of them can save a lot of operational pain, especially in larger organizations with many private repositories.
If I had to prioritize, secret scanning and push protection are usually near the top of the list. Preventing a leaked credential from ever landing in history is one of the rare controls that feels satisfying even before incident response gets involved.
Closing
GitHub already gives teams enough native controls to remove a surprising amount of unnecessary risk. The hard part is not access to features. The hard part is using them consistently and treating workflow security as an engineering concern rather than an afterthought.
Of course, this is not the whole story. I intentionally left out custom runners here. They can play an important role in PPE scenarios, but they would easily deserve an article of their own. GitHub’s native controls for that area are covered in the appendix to this article, but in practice custom runners usually need additional tooling as well. After all, once you run your own runner, you are not just configuring CI anymore, you are operating another container or VM that needs hardening, monitoring, and network controls.
And that is really the PPE angle in one sentence: most poisoned pipeline stories do not begin with sophisticated malware. They begin with a workflow that was trusted too broadly, reviewed too lightly, or allowed to execute more than it should have.
Outlook: Do the Same and More with Open Source Tooling
Besides the appendix, I created a follow-up to this article that looks at the open-source tooling side (coming soon): what you can add when you want to go beyond GitHub’s native controls, stay a bit more independent of GitHub, or simply save some money if budget matters.
Thanks for reading. I hope this helps you and your team on a safe cloud-native journey.


