GitHub repository security checklist for small teams
For most small teams, the repository is the most valuable thing on the internet they own: the product, the customer data access, and the deployment path all meet there. And unlike a laptop or an office, a repo is reachable from anywhere, by anyone who has (or phishes, or buys) a single token. The good news: GitHub gives you a 90-minute setup pass that removes the worst half of the risk — no paid tools, no security engineer, just settings most repos leave at default. This is that pass, plus the thirty-minute monthly ritual that keeps it true. It pairs with the API key rotation checklist (the tokens that reach into your infra) and the secrets rotation checklist (the credentials your code uses at runtime).
1. Access: who can push, and to what
Repo security starts with one question: who can change the answers? Open the org's People page and the repo's Settings → Collaborators before touching anything else:
- Write access is the new admin. Anyone who can push to
maincan modify the workflow files that hold your deploy credentials, or edit the app to log passwords somewhere. Treat the push-to-mainlist like the super-admin list in the Workspace checklist: two to four named humans, and everyone else atreadortriage. The org-level Base permissions: read setting makes that the default instead of a per-repo promise. - Count the outside collaborators and bot accounts. Every repo accumulates them: the freelancer from last year, the CI bot someone gave
write"temporarily", the ex-founder's account still listed because nobody was sure removing it would break CI. Each one is an unowned credential. The offboarding checklist owns removing people — this audit owns finding them. - Use teams, not individuals, for anything larger than a founder pair. Access granted to individuals never leaves when they do; access granted to a
@yourorg/deployersteam is one removal. If your org is two people, skip this; the moment it is five, do it. - Require 2FA for the org (Settings → Authentication security), then survive the grace period. This one toggle removes an entire attack class — the password-only account that gets sprayed. Set it, give people a week to enroll, then let GitHub do the enforcing. The MFA rollout checklist covers the human side; here you just turn the switch GitHub already built.
2. Branch protection: make main annoying to break
The default branch is the only place where "someone made a mistake on a Friday" becomes "production is broken Monday". Protection rules are free and take ten minutes on Settings → Branches:
- Require a pull request before merging to
main, with one approval. On a two-person team, "one approval" is just "the other person glanced at it" — and that glance catches most of the accidental disasters (the deleted migration, the hardcoded key, the workflow that runs on every push). Solo founder? Skip the approval, keep the rest: the PR record itself is the change log. - Block force pushes and deletions on
main. A force push does not just rewrite history — it can silently revert a fix someone shipped, or hide an injected change nobody reviews because the commit "was already there". The box is one checkbox; the failure it prevents is the kind you find out about from a customer. - Require status checks to pass before merging — at minimum the test suite and, once you add it (section 3), the secret scan. This is the quiet control: it converts "we should run the tests before deploy" from a habit into a property of the system.
- Add a CODEOWNERS file so changes to the sensitive paths need the right eyes. Three lines: who owns
.github/workflows/, who owns theinfra/or deploy configs, who owns the code that touches customer data. Workflow edits are where an attacker (or a compromised dependency) turns code access into server access — those PRs should ping the person who will actually read them. - Treat protection changes as changes. Whoever can edit branch protection can un-protect it. The change-management checklist applies here too: a one-line note in the ops log when protection rules change keeps the audit story honest.
3. Secrets: assume everything committed is public
The single most common small-team breach pattern is not a sophisticated exploit — it is a key that was committed once, in a throwaway branch, in March. Assume any secret that has ever been committed is burned, because automated scanners harvest public repos within minutes:
- Turn on push protection and secret scanning (Settings → Code security; free on public repos, available on paid plans for private ones). Push protection blocks the commit that contains a recognized token format before it lands — the cheapest security control on this page, measured in prevented incidents per hour of setup.
- Sweep history once, then stop worrying about the old commits. Run a scanner (gitleaks or trufflehog) across the full history once; every hit is a rotation job, not a rewrite job. Revoking the key (the rotation checklist) closes the hole; history rewriting mostly breaks everyone's clones. Rotate first; clean history only for the repo you will actually maintain.
- Move secrets to Actions secrets or a secret manager, and say so in the README. The convention matters more than the tool:
secrets live in the platform vault, never in filesis a rule a five-person team can actually keep. The secrets checklist covers the rotation cadence; the repo side is just making the path obvious enough that nobody pastes a key intoconfig.pyat 6pm. - Pin the environment secrets down. GitHub Environments let you require approval before a deploy workflow can read production secrets. On a small team this is one click, and it means a rogue (or merely mistaken) PR cannot silently read production credentials by opening a workflow run.
- Audit personal access tokens (PATs) like API keys. Settings → Developer settings is where the months-old tokens with
reposcope live — exactly the pattern in the key-leak runbook. Fine-grained tokens, per-repo scope, expiry dates, and the ones nobody remembers creating get revoked. Org-level: check Settings → Personal access tokens for tokens members issued to themselves.
4. Dependencies: Dependabot plus a review habit
Most small teams run npm install against hundreds of transitive packages per build. You will not audit them all — the software supply chain checklist owns shrinking the chain itself; this section is just the three free GitHub controls that catch the realistic cases:
- Enable Dependabot alerts and version updates (Settings → Code security). Alerts tell you when a dependency you already ship has a known CVE; version updates keep the PRs coming so the fix is a merge, not a migration. Budget the triage: thirty minutes weekly, high/critical first, and expect some alerts to be noise you will explicitly dismiss with a reason — that reason is audit evidence, per the security questionnaire.
- Turn on dependency review for PRs. It shows the dependency delta on every PR — the malicious-package case (typo-squatting, a hijacked maintainer) is visible as "added: some-package-you-never-heard-of" right in the diff, which is exactly where a reviewer will notice it.
- Prefer lockfiles, and commit them. A lockfile makes every machine and every CI run get the same versions, which makes a supply-chain surprise visible as a one-line lockfile diff instead of a silent upgrade.
5. Actions: the CI is part of the attack surface
Workflows run with access to your secrets and often your deploy targets. The small-team hardening set, in priority order:
- Pin third-party actions to a full-length commit SHA, not a tag.
uses: actions/checkout@v4is a moving target; the tag can be re-pointed, and its maintainers' bad day becomes your build's bad day. One afternoon of pinning, forever after visible in review. - Set a default
permissions: contents: readat the workflow top, then grant more only where a job needs it. The default token scope in older repos iswritefor everything — which turns any workflow bug into a repo-writing bug. - Require approval for first-time contributors' workflow runs (Settings → Actions → Fork pull request workflows). This is the "approve and run" toggle; without it, a drive-by PR can execute code in your CI with access to your environment.
- Read your workflow triggers once, slowly.
pull_request_targetchecking out untrusted code with secrets present,schedulejobs nobody remembers writing, workflows that post to external URLs — the ten-minute read finds the config a rushed Friday wrote.
6. Deploy keys, webhooks, and app installs: the side doors
Repo access is not just members. Three doors get forgotten in every small-team audit:
- Deploy keys (Settings → Deploy keys): one SSH key per forgotten server that still has read (or worse, write) access to the repo. List them, name the server each one belongs to, and revoke the ones with no name — the same guilty-until-in-use rule the rotation checklist applies to vendor keys.
- Webhooks (Settings → Webhooks): each one holds a secret that lets its receiver impersonate your repo's events. Old CI installs, deleted Slack channels, that status tool from 2023. Verify the endpoint still exists, confirm TLS, and rotate the secret if it is older than your memory of creating it.
- GitHub App installs (org Settings → Integrations): every integration you clicked "install" on holds scoped, durable access. Review the permission list per app with the same eyes you used on the OAuth screen in the Workspace checklist: does a linting app need read access to all repositories? Uninstall the trials; scope the keepers.
7. Offboarding and the audit log
The day someone leaves is when repo defaults get expensive:
- Remove org membership the same day access ends — but before removing, list what they touched: personal forks (which survive as copies), deploy keys they created, PATs with repo scope, environments where their approval was required. The offboarding checklist sequences this; the repo section is five minutes.
- Rotate the credentials they could have seen. Deploy keys, environment secrets, and shared PATs that existed during their tenure get the rotate-and-verify treatment. This is paranoia priced correctly: it costs an hour and closes the "they copied the token in February" question permanently.
- Export the audit log monthly (org Settings → Audit log). Member adds/removals, protection-rule changes, secret-scanning alerts, token grants. A monthly CSV export into the evidence folder is the artifact that turns "we take access seriously" into a dated file a customer or insurer can read — the same move the annual review builds on.
8. The monthly 30-minute pass
- Minute by minute: member and collaborator list vs. the current team chart (5), new PATs and app installs since last month (5), open Dependabot alerts triaged — target zero critical older than two weeks (5), deploy keys and webhooks named and owned (5), audit-log export filed (5), branch protection still on and rules unchanged (5). One calendar event, one owner, one line of notes.
- Feed the evidence file. The export plus the alert-dismissal reasons plus the access list is most of what enterprise buyers and cyber-insurance renewals ask about your development process. Answering "do you enforce 2FA and review dependencies?" becomes a file path, not an afternoon.
- Keep the boundary honest: the repo is one layer. The servers it deploys to (the server hardening checklist), the laptops that push to it (the laptop runbook), and the humans who click phishing links (the phishing drill) are separate checklists — an afternoon across all four is a materially harder company.
9. Common failure modes
- "It's a private repo, we're fine." Private protects you from scanners, not from the compromised laptop of anyone on the collaborator list — and every token with
reposcope reads private repos too. The controls above are about the insider-token path, not the public path. - Protection rules added, then quietly removed during an "emergency." The emergency merge is fine; the missing follow-up note is the defect. Re-enable and log it the same day.
- Secrets rotated but never revoked. The old cloud key still works alongside the new one — the exact defect the rotation checklist exists to kill. Revoke means the old key errors.
- Bot accounts with human privileges. If a token can push to
main, it belongs in the same count as people. Name the bot, scope it to the repo, expire it. - "We'll audit after the release." The release ships; the audit slips a quarter; the freelancer from that quarter still has write access. Audits are calendar events, not moods — the same discipline as the patch cadence.
10. The one-afternoon version, in order
- If you only do one afternoon: (1) enforce org 2FA; (2) set base permissions to
readand audit the write list; (3) protectmain— PRs required, force pushes blocked, status checks required; (4) enable secret scanning with push protection; (5) enable Dependabot alerts; (6) audit PATs, deploy keys, and webhooks — revoke the unnamed; (7) export the audit log. Ninety minutes, no paid tools, and the repo stops being the easiest door into the company. - Then make it stick: the monthly 30-minute pass (section 8), CODEOWNERS when the team hits five, and the environment-approval gate before the next production deploy. Everything else on this page is refinement; those are the controls that keep working while you sleep.