News
GitHub Actions write-all vs Least-Privilege Permissions
December 26, 2023
GitHub Actions write-all vs least-privilege permissions is a CI identity decision, not a website header. permissions: write-all (or a default token that can push, contents, and more than the job needs) turns a compromised step into a repo-wide incident. Least privilege is an explicit permissions: block per workflow or job — often contents: read, plus only the extra scopes that job must have. Pinning third-party actions by full commit SHA stops a moving tag from swapping the script. RoastMyOpsec's optional GitHub deepen reads a sample of default-branch workflow YAML you authorize. It does not execute workflows, does not steal tokens, and a public URL roast cannot see .github at all.
The practical difference
The website is the costume. Actions is the dressing room with a key to git. write-all is convenience. Least privilege is listing the rooms the job may enter. If a job only runs tests, it does not need contents: write.
Unpinned actions (uses: owner/action@v4) follow a tag the publisher can move. SHA pins freeze the exact commit you reviewed. This is the same class of decision as Subresource Integrity on a CDN script — mutability vs a digest.
How to choose based on the workflow
Start every workflow with a tight default (contents: read, then add). Split jobs so deploy write is not on the same job that checks out untrusted pull-request code. Treat pull_request_target plus checkout of the PR head as a combination GitHub documents as dangerous — isolate untrusted code; do not give it the base-repo token.
Only deepen the repo that actually builds the site you roasted. Connecting a random org is outside the consent model.
| Pattern | What it grants | Typical miss | Takeaway |
|---|---|---|---|
| permissions: write-all | Broad GITHUB_TOKEN | One bad step writes the repo | Replace with an explicit map |
| contents: read (job default) | Clone for tests | Still too much if the job does not need the tree | Start here; add scopes per job |
| Action @v4 / @main | Whatever the tag points at tomorrow | Supply-chain swap | Pin full SHA; review upgrades |
| pull_request_target + untrusted checkout | Base-repo token with PR code | Untrusted code in a trusted identity | Do not combine; follow GitHub's isolation guidance |
What deepen can and cannot prove
The paid GitHub check samples workflow files on the default branch for write-all, pull_request_target with checkout, and unpinned actions. It is a high-signal read, not a full CI pentest and not proof a workflow was exploited. File content of secrets is not dumped as a trophy.
If the leak is mixed content on www, tightening Actions will not rewrite the CMS. Run URL mode first.
Common mistakes
The first mistake is a 10/10 URL roast and write-all on every workflow.
The second mistake is pinning nothing because 'we trust the marketplace.'
The third mistake is deepening a fork that never deploys production.
Free audit the URL you own
RoastMyOpsec is a defensive public-surface roast: headers, cookies, sensitive paths, and more — no exploit payloads. Start with the free audit, then open the vault if the blurred findings look expensive.
Free audit nowFAQ
- Will a URL scan find GitHub Actions issues?
- No. Workflows are in the repo. Use optional GitHub deepen on a repository you own that ships that URL.
- Is write-all a vulnerability by itself?
- It is a privilege miss. It becomes an incident when a step is malicious or a dependency moves. Least privilege shrinks that blast radius.
- Does RoastMyOpsec run my workflows?
- No. It reads a bounded sample of YAML through the GitHub API after you authorize the repo. No exploit payloads, no token theft.