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:

Enforce 2FA at the org level
Set GITHUB_TOKEN to read-only
Enable secret scanning with push protection

These three cover the most likely attack paths: account takeover, over-privileged automation, and leaked credentials. The rest builds on top.


Summary

#ControlWhere
1Enforce 2FA org-wideOrg Settings > Authentication security
2Lower base permission to Read or NoneOrg Settings > Member privileges
3Rulesets, not classic branch protectionSettings > Rules > Rulesets
4CODEOWNERS for sensitive paths.github/CODEOWNERS
5GITHUB_TOKEN read-onlyOrg Settings > Actions > Workflow permissions
6No PR create/approve for ActionsSettings > Actions > Workflow permissions
7Pin actions to commit SHAworkflow YAML files
8Allowlist for actionsOrg Settings > Actions > General
9Approval for fork workflow runsSettings > Actions > General
10Secret scanning + push protectionSettings > Security
11Audit deploy keys, fine-grained PATsRepo Settings / Org Settings
12Dependabot, all three partsSettings > Security + dependabot.yml
13OSSF Scorecard action.github/workflows/scorecard.yml
14Private vulnerability reporting + SECURITY.mdSettings > Security
Want a full review of your GitHub security posture?

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 call

References