Security · 8 min read
A GitHub hardening checklist: ten settings that close the common holes
Most repository compromises don't need a clever exploit. They use a leaked token, an over-permissioned workflow, or a third-party action that changed under you. This checklist covers the settings that close those gaps. Items follow GitHub's own security hardening guide for Actions.
1. Protect the default branch
Require pull requests, at least one approval and passing status checks; block force-pushes and deletion; apply the rules to admins too. See GitHub's docs on protected branches.
gh api -X PUT repos/OWNER/REPO/branches/main/protection --input - <<'JSON'
{
"required_status_checks": { "strict": true, "contexts": ["ci"] },
"enforce_admins": true,
"required_pull_request_reviews": { "required_approving_review_count": 1 },
"restrictions": null,
"allow_force_pushes": false,
"allow_deletions": false
}
JSON
2. Make the workflow token read-only by default
Set the repository or organisation default for GITHUB_TOKEN to read-only, then grant write per job only where needed.
# top of every workflow
permissions:
contents: read
3. Pin third-party actions to a full commit SHA
A tag like @v4 can be moved. A full-length commit SHA cannot. GitHub's guidance is that pinning to a full-length commit SHA is the only way to use an action as an immutable release.
- uses: actions/checkout@<full-40-char-sha> # v4.x.x
Let Dependabot keep the SHAs current (see item 6).
4. Use OIDC instead of long-lived cloud keys
Store no AWS_SECRET_ACCESS_KEY in GitHub. Let the workflow exchange a short-lived OIDC token for a role limited to exactly what the job needs. See GitHub's OIDC guide.
permissions: { id-token: write, contents: read }
steps:
- uses: aws-actions/configure-aws-credentials@<full-commit-sha>
with:
role-to-assume: arn:aws:iam::<account>:role/ci-least-privilege
aws-region: us-east-1
5. Turn on secret scanning and push protection
Push protection blocks a commit containing a recognised credential before it lands in history. Availability for private repositories depends on your GitHub plan.
gh api -X PATCH repos/OWNER/REPO \
-f 'security_and_analysis[secret_scanning][status]=enabled' \
-f 'security_and_analysis[secret_scanning_push_protection][status]=enabled'
If a secret has ever been committed, assume it is compromised: rotate it. Deleting the commit does not un-leak it.
6. Enable Dependabot, including for Actions
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule: { interval: weekly }
- package-ecosystem: npm
directory: /
schedule: { interval: weekly }
7. Add CODEOWNERS for workflows and sensitive paths
Changes to CI definitions are changes to what runs with your credentials. Require a review from a named owner.
# .github/CODEOWNERS
/.github/workflows/ @your-org/security-owners
/infra/ @your-org/platform
8. Require two-factor authentication for the organisation
Org settings, Authentication security. Members without 2FA are removed or blocked, which closes the commonest account-takeover path.
9. Restrict which actions can run
In Settings, Actions, General, allow only actions created by GitHub plus an explicit allow-list, rather than any action from anywhere.
10. Treat pull_request_target and deploy secrets with care
- Never check out and run untrusted pull-request code in a
pull_request_targetworkflow: it runs with repository secrets. - Put deploy credentials in an environment with required reviewers, so a production deploy needs a human approval.
- Never interpolate untrusted input (issue titles, branch names) directly into
run:steps; pass it through an environment variable.
How to use this
Work top to bottom; items 1 to 5 deliver most of the risk reduction. Our fixed-price GitHub Security Hardening offer applies all ten across your repositories and gives you a written report of every change.