The safest secret in a Git repository is the secret that never enters it.
That sentence is the whole cluster. Everything after it is either making that outcome more likely, or dealing with the times it did not happen.
Start with Secret ScanningWhy a committed credential is a different kind of problem
Section titled “Why a committed credential is a different kind of problem”A vulnerability in your code is latent. Someone has to find it, reach it and exploit it. You have time, and the fix is a code change.
A committed credential is not latent. It is a working key, in a text file, and the only thing between it and use is whether anyone has read it yet. Public repositories are scraped continuously by automation that exists solely to find them; the interval between push and first use is routinely measured in minutes.
It is also retroactively permanent. Git is content-addressed and append-only in practice: the commit containing the secret is reachable from every clone anyone ever made, every fork, every CI cache, every mirror. Deleting the file in a new commit changes nothing about the old one.
And it does not respect boundaries the way a code vulnerability does. A leaked cloud key is not a weakness in your application — it is valid credentials for your cloud account, usable from anywhere, by anyone, with no need to touch your software at all.
Defence in depth, applied
Section titled “Defence in depth, applied”No single layer catches everything, and the failure modes are what make the stack worth building.
A vertical chain: developer training, then gitignore, then a pre-commit scan, then push protection, then a pull request scan, then continuous repository scanning, then credential rotation. Each layer has a different failure mode.
Each layer misses something specific:
| Layer | Fails when |
|---|---|
| Training | Someone is in a hurry, or new, or debugging at 23:00 |
.gitignore | The file is already tracked — .gitignore does not untrack |
| Pre-commit hook | Not installed, or bypassed with --no-verify |
| Push protection | The pattern is not one it supports, or someone bypasses it |
| PR scan | The secret is not in this diff, only in history |
| Continuous scan | It is retrospective — the credential already leaked |
Reading down that column is the argument for the stack. Reading it across is the argument against believing any one layer is sufficient.
The threat model
Section titled “The threat model”Threat. A valid credential is written into a file that gets committed and pushed, and is then used by someone other than its owner.
Attack surface. Application config and .env files. Test fixtures containing a real token
because the fake one did not work. Notebooks with output cells. Terraform state and plan files.
CI configuration. Screenshots and logs pasted into an issue. Commit messages. Anything in a directory
someone ran git add . in.
Impact. Whatever the credential can reach — which is frequently more than the person who committed it believed, because credentials get reused and over-scoped.
Control. Prevention at the point of writing (secret managers, .env.example templates),
prevention at the point of pushing (push protection, pre-commit scanning), detection everywhere else,
and short credential lifetimes so an undetected leak expires on its own.
Verification. Commit a deliberately fake but correctly shaped credential in a disposable repository and confirm each layer behaves as documented. This is one of the few security controls you can genuinely test end to end in ten minutes.
Detection is not prevention
Section titled “Detection is not prevention”The distinction runs through the whole cluster and is worth stating precisely, because the two are routinely described as one feature.
Secret scanning detects credentials that may already exist in content.
Push protection tries to stop supported secrets before they enter the repository.
The gap between them is the incident. A detected secret has already been committed, has already been pushed, and is already in everyone’s clones. A blocked push has not.
Secret Scanning and Push Protection are separate lessons for that reason.
The lessons
Section titled “The lessons”- Lesson 1: 01. Secret ScanningHow GitHub secret scanning works — which surfaces are scanned, partner patterns and provider notification, validity checks, generic and AI-detected secrets, custom patterns.
- Lesson 2: 02. Push ProtectionPush protection in depth — which surfaces it covers, the three bypass reasons and what each records, delegated bypass, push protection for users, false positives and rollout.
- Lesson 3: 03. Remove Secrets from HistoryAn incident-response procedure for a credential committed to Git — rotate first, determine exposure, rewrite history with git filter-repo, coordinate force updates, clean clones and forks, and verify.
- Lesson 4: 04. Rotate Exposed CredentialsAn incident-response framework for a leaked credential — revoke versus rotate, identifying what it reaches, dual-key rotation, updating consumers, validating, monitoring and writing it up.
- Lesson 5: 05. Prevent .env CommitsKeeping environment files out of Git — .gitignore patterns, untracking a file that is already committed, .env.example templates, pre-commit checks, Docker build contexts and the mistakes that recur.
- Lesson 6: 06. GitLeaksUsing GitLeaks in practice — scanning history and working directories, configuration and custom rules, allowlists and baselines, pre-commit and CI integration, and what it cannot find.
- Lesson 7: 07. TruffleHogTruffleHog in practice — detectors and live verification, verified versus unverified results, scanning Git history and filesystems, CI integration, and how it compares with GitLeaks.
- Lesson 8: 08. Secret Scanning in CIBuilding defence in depth against credential leaks — pre-commit hooks, push protection, pull request scans, scheduled full-history scans, findings handling.
Lessons 1 and 2 cover GitHub’s native controls — detection and prevention respectively. Lessons 3 and 4 are the incident response pair, and should be read together and in the order they are presented, because the ordering is the lesson. Lesson 5 handles the specific case that causes most real leaks. Lessons 6 and 7 cover the two dominant open-source scanners. Lesson 8 assembles everything into a layered pipeline.
The incident sequence
Section titled “The incident sequence”When a credential is exposed, the order of operations matters more than the speed of any individual step.
A vertical sequence: secret discovered, revoke or rotate, determine exposure, remove from current files, rewrite history if required, clean clones and forks, verify, monitor.
Step 5 is optional and step 2 never is. A team that rewrites history without rotating has done the expensive half of the work and none of the effective half.
Where secrets belong instead
Section titled “Where secrets belong instead”Prevention only works if there is somewhere better to put the thing. “Do not commit secrets” without
an alternative produces a .env file on every laptop and a shared document nobody admits to.
Local development. An untracked .env file, generated from a committed .env.example that
contains keys with placeholder values. The example file is the documentation; the real one never
leaves the machine. Preventing .env commits covers
the pattern and the failure modes.
CI/CD. GitHub Actions secrets and environment secrets, scoped to the environments that need them, with required reviewers on the environments that hold production credentials. Pillar 4 covers the mechanics in Secrets and Environments.
Cloud access from CI. Nothing stored at all. OIDC exchanges a short-lived, workload-scoped token at run time, which means there is no credential in the repository to leak. Remove long-lived cloud credentials is the highest-value single change in this category.
Runtime. A secret manager, injected at deploy or read at start-up, so the credential exists in the process and not in an artifact.
The pattern across all four: a secret that is short-lived, narrowly scoped, and never written to disk in your repository is a secret whose exposure is survivable.
What counts as a secret
Section titled “What counts as a secret”Broader than most checklists suggest, and the unobvious entries are the ones that get committed.
- API keys, tokens and passwords — the obvious set
- Private keys of any kind: SSH, TLS, GPG, code signing, JWT signing
- Cloud credentials, including session tokens people believe are too short-lived to matter
- Database connection strings, which embed credentials in a format scanners often miss
- Webhook signing secrets — they authenticate inbound requests, so leaking one lets an attacker forge them
- Internal hostnames and infrastructure identifiers, which are not credentials but are reconnaissance
- Anything in a
.tfstatefile, which records the values Terraform managed
Two are worth calling out because teams routinely decide they do not count. Test credentials are real credentials against a real system in a surprising number of codebases. Expired credentials tell an attacker your token format, your naming conventions and which providers you use — and are sometimes not as expired as assumed.
Prerequisites
Section titled “Prerequisites”Common mistakes
Section titled “Common mistakes”Adding .env to .gitignore after committing it. .gitignore governs untracked files. A tracked
file keeps being tracked, and the historical commit is unaffected either way.
Assuming private means safe. Private restricts who can read the repository. It is not encryption, it does not shrink over time, and it does not survive the repository being made public by mistake.
Bypassing push protection to “fix it properly later”. The bypass reason “I’ll fix it later” exists, it creates an open alert, and it is recorded. It is the right choice occasionally and the wrong default always.
Scanning only the current tree. A working-tree scan tells you today’s state. History is where committed secrets live, and it is what everyone with a clone already has.
After this cluster
Section titled “After this cluster”Secret Security is about credentials that should never have been in the repository. The next cluster is about defects that legitimately are — in code you wrote, and in code you depend on.
Next cluster — Code & Dependency Security