CHAPTER 20

Security tooling

Anvesha typed git add . without looking. One-tenth of a second later, GitHub had already decided that push wasn't going to go through.

A Thursday, wiring up the newsletter signup

Anvesha's connecting the homepage's newsletter form to a real email service, which means a real API key, which — for a first pass, just to get it working locally — she's dropped straight into a file called .env in the project root. It works. She's happy. Out of habit, since she's mid-flow and not thinking hard about it, she runs git add . and commits everything in the folder, .env included, and pushes.

remote: error: GH013: Repository rule violations found
remote:   — Push cannot contain secrets
remote:   —— Mailgun API Key
remote:      locations:
remote:        - commit: 8a1b2c3
remote:          path: .env:1
remote:
remote: (?) Learn how to resolve a blocked push
remote: https://docs.github.com/code-security/...

The push simply never lands. Not "flagged for later review" — refused, on the spot, before the key ever reaches a public, permanent, forkable history. Anvesha didn't do anything unusual; this is close to the single most common way real secrets leak in the real world, and it very nearly happened here too.

Push protection: the wall that caught it

GitHub maintains a database of roughly 200 known secret formats — AWS keys, Stripe tokens, GitHub PATs, Mailgun keys among them — each with a recognizable shape. Push protection scans every incoming push against that list, before accepting it, and simply rejects anything that matches. It's on for every public repo Accelerate has, free, no configuration needed beyond the toggle already flipped in Settings → Code security.

EVEN THOUGH IT WAS CAUGHT — THE KEY IS STILL BURNED
Anvesha typed the real key into her local .env file and it briefly existed in a local commit before the push was rejected. The safe move now isn't just "delete the file and try again" — it's rotating the key at Mailgun first, generating a fresh one, and treating the old one as compromised regardless of whether it ever technically reached GitHub's servers. This is the exact same lesson from Chapter 2's .gitignore callout, now happening for real: once a secret has been typed anywhere near a commit, the file removal isn't the fix, the rotation is.

Dependabot — the same idea, aimed at other people's code

The club's own code isn't the only thing that can carry a vulnerability — every dependency the project installs is code the club is trusting, sight unseen, and those get security patches too.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule: { interval: weekly }
    open-pull-requests-limit: 5
    groups:
      dev-deps: { dependency-type: development }

  - package-ecosystem: github-actions
    directory: /
    schedule: { interval: weekly }

The second entry matters specifically because of Chapter 17's SHA-pinning discipline — it's how those pinned SHAs actually get refreshed on a schedule, instead of silently going stale forever once pinned.

CodeQL — reading the code itself for known-dangerous patterns

Push protection catches secrets by their shape. CodeQL is a different kind of scanner — it reads the actual logic of the code for patterns known to cause real vulnerabilities: SQL injection, cross-site scripting, path traversal, hardcoded cryptographic keys.

# .github/workflows/codeql.yml
name: codeql
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }
  schedule: [{ cron: "0 3 * * 1" }]

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    strategy:
      matrix: { language: [javascript] }
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with: { languages: ${{ matrix.language }} }
      - uses: github/codeql-action/analyze@v3

Findings land in the Security tab under Code scanning. A high-severity finding can be wired into branch protection (Chapter 15) as a required status check, the same way lint already is.

Everything else already sitting quietly underneath the whole story

None of the earlier chapters called this out directly, but every one of them was resting on choices made at the org level, back before the story even started:

SECRET SCANNING VALIDITY CHECKS — STILL AN OPEN ITEM
One setting from this list genuinely isn't flipped on yet, honestly: validity checks, which would have GitHub actually ping Mailgun to confirm whether Anvesha's almost-leaked key was still active. It's a real gap, tracked, not yet closed — worth knowing the difference between "we have this covered" and "we know exactly what's still missing," which is itself the discipline the whole setup log is built around.

Reporting a vulnerability, if you ever find one

A SECURITY.md at the repo root tells anyone who spots a real vulnerability exactly where to report it responsibly, rather than posting it publicly as an issue where it could be exploited before it's fixed. For anything sensitive enough to need a private conversation first, GitHub's own private security advisories feature gives a maintainer and a reporter a confidential space to work out a fix before any public disclosure.

CHECK
Anvesha's push was rejected before the key ever reached GitHub's public history. Is deleting .env from the commit and re-pushing now sufficient?
Push protection is a last line of defense at the network boundary, not proof the key was never at risk — it already existed in a local commit, in a local .git/ object database, on a machine that could be lost, backed up somewhere, or synced to a cloud drive. Treating "it got blocked" as equivalent to "it's safe" is exactly the gap that turns a near-miss into a real incident later.

Every mechanism in this story — commits, branches, PRs, Actions, security scanning — has been in service of one club: Accelerate specifically. Time to step back from the day-to-day drama and see the whole shape of how it actually fits together, end to end.