GitHub default settings are made for convenience, not for security. Most repos and orgs go live with admin bypass open, tokens that can write to everything, and third party actions pinned to tags that the owner can change any time.
Good news is, you don’t need a paid plan to fix most of this. Everything below is free. Each one takes a few minutes. Together they close the most common ways GitHub environments get compromised.
I split them into five areas so you can go one by one.
Access and Identity
1. Enforce 2FA for the whole organisation
This is the biggest win you can get from one checkbox. Stolen passwords are still the number one way attackers get into GitHub accounts. With 2FA enforced, a stolen password alone is not enough.
Where: Org Settings > Authentication security > Require two-factor authentication
One warning. When you turn it on, every member without 2FA gets removed from the org immediately. Outside collaborators too. So tell your team before you click it.
2. Lower the base permission for org members
Every org has a base permission. It is what every member gets on every repo by default. Many orgs leave it on Write, which means any member can push to any repo. One compromised account then puts all your code at risk.
Set it to Read, or better None, and give access through teams. Only the people who work on a repo should have write to it.
Where: Org Settings > Member privileges > Base permissions
This costs nothing and nobody will even notice, until an attacker does.
3. Use Rulesets instead of classic branch protection
Classic branch protection has one problem people keep missing. By default it does not apply to admins. An admin can push to main directly, delete branches, rewrite history, all while “protection” is on. You have to find a small checkbox in every rule to stop this.
Rulesets work the other way. Admins are restricted too, unless you put them on a bypass list on purpose. You can also apply one ruleset to many repos at once, and protect tags, not only branches.
Where: Settings > Rules > Rulesets > New ruleset
Minimum to set:
- Restrict deletions
- Require a pull request before merging, at least 1 approval
- Block force pushes
- Require status checks to pass
I wrote before in detail about what each branch protection rule does, the risks if you skip it, and what makes sense for small teams versus big orgs. If you are new to this topic, start there: 10 Rules of GitHub Branch Protection: Why Every Org Should Use Them. The rules themselves are the same, Rulesets is just the better way to apply them.
Rulesets are free for public and private repos.
4. Add a CODEOWNERS file for sensitive paths
CODEOWNERS is a simple file that says who must review changes to which paths. Combine it with “Require review from Code Owners” in your ruleset and nobody can change your workflow files, IaC, or auth code without the right person looking at it.
A small example:
# .github/CODEOWNERS
/.github/workflows/ @your-org/security-team
/terraform/ @your-org/platform-team
/src/auth/ @lead-dev
Where: put the file in .github/CODEOWNERS, then enable the review requirement in your ruleset
Workflow files are the most important here. If someone can edit your CI pipeline, they can usually steal your secrets too.
GitHub Actions
5. Make GITHUB_TOKEN read-only by default
Every workflow run gets a GITHUB_TOKEN automatically, even if you never use it. Third party actions in your workflow can use it too. If the default is write, a compromised action can push code, create releases, or modify your repo.
Set the default to read-only at org level, then give write only where the job really needs it:
permissions:
contents: read
jobs:
deploy:
permissions:
contents: read
id-token: write
Where: Org Settings > Actions > General > Workflow permissions > “Read repository contents and packages permissions”
Any scope you don’t list becomes none. So a compromised action gets almost nothing.
6. Don’t let Actions create and approve pull requests
When this setting is on, a workflow can open a PR and approve it by itself. No human involved. That is a direct path for code injection. New repos have it off, but older repos and orgs often still have it on, so go and check.
Where: Settings > Actions > General > Workflow permissions > “Allow GitHub Actions to create and approve pull requests”
Keep it off unless you have a real, reviewed reason.
7. Pin third party actions to a commit SHA
A tag like actions/checkout@v4 is not fixed. The owner can move it to point at different code and you will never notice. This is exactly how several supply chain attacks on Actions worked.
A full commit SHA cannot be moved:
# Instead of this:
uses: actions/checkout@v4
# Use this:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Where: your workflow YAML files
Keep the version tag as a comment next to the SHA so it stays readable. One trade-off to know: Dependabot does not raise vulnerability alerts for SHA-pinned actions, so add Scorecard (item 13) and check release notes when you bump the pins.
8. Restrict which actions are allowed to run
By default your workflows can pull any public action from the marketplace. That is thousands of unreviewed third party code packages, one typo away from your pipeline.
Limit it to actions from GitHub, verified creators, and a small allowlist you control.
Where: Org Settings > Actions > General > “Allow select actions and reusable workflows”
Recommended:
- Allow actions created by GitHub
- Allow actions by Marketplace verified creators
- Add other actions to the allowlist one by one, after you looked at them
9. Require approval for workflow runs from forks
On public repos, anyone can fork your repo, open a PR, and your workflows run on their code. Attackers use this to probe pipelines and try to reach secrets. GitHub can require a maintainer to approve the run first.
Where: Settings > Actions > General > Fork pull request workflows from outside collaborators
Pick “Require approval for all external contributors”. It is one click for the maintainer per PR and it removes a whole attack class.
Secrets and Sensitive Data
10. Turn on secret scanning and push protection
Secret scanning finds leaked credentials, AWS keys, GitHub tokens, Stripe keys and 200+ other types, in your history and in open PRs. Push protection is even better, it blocks the push at the moment a secret is detected, before it enters the repo at all.
Where: Settings > Security > Secret scanning > Enable, then enable Push protection below it
Free on public repos. On private repos full secret scanning needs the paid Advanced Security, but push protection for common secret types is free.
Check your old repos too. Scanning alerts on historical commits, and a key leaked two years ago is still a valid key if nobody rotated it.
11. Audit deploy keys and move to fine-grained tokens
Deploy keys get added for some one-time deployment, then everybody forgets them. They keep working forever. Classic personal access tokens are even worse, they carry the full power of the user who made them, they don’t expire by default, and they survive after the person leaves the company.
Where: Repository Settings > Deploy keys, and Org Settings > Personal access tokens
What to do:
- Delete every deploy key you cannot explain
- Require fine-grained PATs at org level
- Fine-grained PATs have expiry, repo scope and granular permissions. There is no reason to use classic ones anymore
Dependencies and Supply Chain
12. Enable Dependabot, all three parts
Dependabot is actually three features and people often enable only one:
Alerts tell you when a dependency has a known CVE. Security updates open a PR automatically to fix it. Version updates keep everything current on a schedule, so you are not jumping five major versions in panic when a critical CVE drops.
Where: Settings > Security for alerts and security updates, plus a .github/dependabot.yml file for version updates:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
The second block keeps your actions updated too. All free, on all repos.
13. Add the OSSF Scorecard action
Scorecard checks your repo against a list of supply chain controls, SHA pinning, branch protection, token permissions, dependency tooling and more, and gives you a score. Run it as a weekly action and you see when your posture drifts.
How: add .github/workflows/scorecard.yml:
name: Scorecard
on:
branch_protection_rule:
schedule:
- cron: "30 1 * * 6"
push:
branches: ["main"]
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
Results show up under Security > Code scanning alerts. Free on public repos.
14. Set up private vulnerability reporting and a SECURITY.md
If a researcher finds a bug in your code, where do they report it? Without a channel, the answer is usually Twitter, or nowhere. Private vulnerability reporting gives them a button on your repo to report it to you privately. SECURITY.md tells them how you want to be contacted and what to expect.
Where: Settings > Security > Private vulnerability reporting > Enable, and add a SECURITY.md file to the repo root
Takes five minutes. The day someone finds something, you will be very glad it goes to your inbox and not to the public.
Where to Start
If you do only three things today, do these:
These three cover the most likely attack paths: account takeover, over-privileged automation, and leaked credentials. The rest builds on top.
Summary
| # | Control | Where |
|---|---|---|
| 1 | Enforce 2FA org-wide | Org Settings > Authentication security |
| 2 | Lower base permission to Read or None | Org Settings > Member privileges |
| 3 | Rulesets, not classic branch protection | Settings > Rules > Rulesets |
| 4 | CODEOWNERS for sensitive paths | .github/CODEOWNERS |
| 5 | GITHUB_TOKEN read-only | Org Settings > Actions > Workflow permissions |
| 6 | No PR create/approve for Actions | Settings > Actions > Workflow permissions |
| 7 | Pin actions to commit SHA | workflow YAML files |
| 8 | Allowlist for actions | Org Settings > Actions > General |
| 9 | Approval for fork workflow runs | Settings > Actions > General |
| 10 | Secret scanning + push protection | Settings > Security |
| 11 | Audit deploy keys, fine-grained PATs | Repo Settings / Org Settings |
| 12 | Dependabot, all three parts | Settings > Security + dependabot.yml |
| 13 | OSSF Scorecard action | .github/workflows/scorecard.yml |
| 14 | Private vulnerability reporting + SECURITY.md | Settings > Security |
We assess GitHub organisations as part of our security engineering and DevSecOps work. We tell you what is misconfigured and give you a prioritised fix list.
Book a free callReferences
- Hadosec: 10 Rules of GitHub Branch Protection
- GitHub Docs: About rulesets
- GitHub Docs: About protected branches
- GitHub Docs: Setting base permissions for an organization
- GitHub Docs: About code owners
- GitHub Docs: Controlling permissions for GITHUB_TOKEN
- GitHub Docs: Security hardening for GitHub Actions
- GitHub Docs: Approving workflow runs from public forks
- GitHub Docs: About secret scanning
- GitHub Docs: Managing your personal access tokens
- GitHub Docs: Configuring private vulnerability reporting
- OpenSSF Scorecard
- OSSF Scorecard Action