Preventing and Remediating Secrets in Source Control

Preventing and Remediating Secrets in Source Control - article cover image Containers & DevSecOps
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Secrets in source control — API keys, database passwords, cloud access keys, private certificates, OAuth client secrets — are one of the most consistently exploited findings in both bug bounty and internal red-team assessments, precisely because they require no vulnerability chain at all: a valid credential found in a commit, a fork, or a CI log is immediately usable. Unlike most application vulnerabilities, a leaked secret is not “fixed” by patching code; the credential itself must be rotated, because git history is effectively permanent — deleting a file in a later commit does not remove it from the object store, and a clone made before the deletion (or a public mirror, or GitHub’s cached views) can still contain it indefinitely.

The problem has grown with the shift to public and semi-public repositories, GitHub Actions logs, and infrastructure-as-code, where credentials are increasingly embedded not by careless copy-paste but by convenience during local development (“I’ll just hardcode it while I test”) that never gets removed before commit. Automated scanners — both defensive tools run by the owning organization and offensive tools run continuously by attackers against every public GitHub push — mean the window between a secret landing in a commit and it being found and abused is now frequently measured in minutes.

Attack Prerequisites

From the attacker’s side, exploiting a leaked secret requires very little:

  • Repository visibility or history access — a public repo, a public fork of a private repo, a leaked CI artifact/log, or an insider/compromised account with clone access to a private repo.
  • A secret that is still live — the credential has not been rotated since it was committed; the older the commit, the more likely it has already cycled, which is why speed of remediation matters as much as detection.
  • No compensating control on the credential itself — no IP allowlisting, no short expiry, no scope restriction — that would limit blast radius even if the raw value leaks.
  • Discoverability — for public repos, essentially guaranteed; automated scanners (both legitimate, like GitHub’s secret scanning partner program, and attacker-run) continuously diff every public push against known secret signatures.

How It Works

Secrets end up in git history through a small number of recurring patterns: a config file with real credentials committed alongside code (instead of .env.example with placeholders), a credential pasted into a debug print/log statement that gets committed, a CI/CD pipeline definition with an inline token instead of a secrets-manager reference, or a .git directory itself becoming web-accessible on a misconfigured server, exposing the entire object database including any secret ever committed, even if it was later removed from the working tree.

Detection tools work by pattern and entropy matching against every commit in history, not just the current tree — this is the critical distinction from ordinary code scanning. A tool like gitleaks or trufflehog walks the full git log, diffing each commit against the previous one, and runs regex signatures (AWS AKIA[0-9A-Z]{16}, Slack xox[baprs]-..., private-key PEM headers) plus Shannon-entropy checks against every added line, because a high-entropy string that matches no known signature is still very likely to be a generic API key or password. This is why git rm and even a force-push rewrite of the branch tip does not reliably solve a leak on its own: any clone, fork, or CI cache made before the rewrite still has the old commits with the secret intact, and GitHub itself may retain the objects for a period even after a rewrite.

The remediation is therefore two separate actions that are often conflated: rotating the credential (the only action that actually stops exploitation, since it invalidates the leaked value regardless of where copies exist) and cleaning history (git filter-repo or the BFG Repo Cleaner, which reduces future exposure and satisfies audit requirements but does nothing about copies that already exist). Teams that clean history without rotating the credential have fixed nothing an attacker cares about.

Practical Example / Configuration

A pre-commit hook configuration that blocks a secret from ever reaching a commit in the first place, using gitleaks via the pre-commit framework (.pre-commit-config.yaml):

repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']
YAML

A custom gitleaks.toml rule extending the default ruleset to catch an organization-specific internal token format, in addition to the built-in AWS/GCP/Slack/PEM signatures:

title = "acme gitleaks config"

[[rules]]
id = "acme-internal-api-token"
description = "Acme internal service token"
regex = '''acme_(prod|stg)_[a-f0-9]{40}'''
tags = ["key", "acme"]

[[rules]]
id = "generic-high-entropy-secret"
description = "Generic high-entropy string assigned to a secret-like var"
regex = '''(?i)(secret|token|passwd|password|api_?key)\s*[:=]\s*['"][a-zA-Z0-9_\-]{20,}['"]'''
entropy = 4.0
tags = ["generic"]

[allowlist]
paths = [
  '''(.*?)(test|fixture)(.*?)''',
]
TOML

CI-side enforcement (blocking the whole history on every push, not just new commits, catching anything a local hook was bypassed for):

# .github/workflows/gitleaks.yml
name: gitleaks
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITLEAKS_CONFIG: .gitleaks.toml
YAML

Walkthrough / Implementation

Responding to an already-leaked secret found in history:

# 1. Confirm scope: find every commit that ever touched the secret
gitleaks detect --source . --report-format json --report-path found.json

# 2. IMMEDIATELY rotate the credential at its source (AWS IAM, DB, vendor
#    console) - this is the step that actually stops exploitation.
aws iam update-access-key --access-key-id AKIA... --status Inactive
aws iam create-access-key --user-name svc-checkout

# 3. Purge the value from history (reduces future exposure / audit trail;
#    does NOT undo any copy that already exists elsewhere)
git filter-repo --replace-text <(echo 'AKIAABCD1234EFGH5678==>***REMOVED***')

# 4. Force-push the rewritten history and have every collaborator re-clone
#    (rebasing onto rewritten history is not sufficient - old clones still
#    have the objects)
git push --force origin main
Bash

Step 2 must happen before step 3 in every case: history rewriting takes time to propagate and can fail partway, while credential rotation is immediate and unconditionally closes the exposure regardless of how many copies of the old history exist.

Note: .env files belong in .gitignore from the first commit of a project, not added reactively — check git log --all --full-history -- .env on any existing repo before assuming it was never committed; a later .gitignore entry does not retroactively remove earlier commits.

Opsec: Attackers run continuous scanning against public GitHub pushes; several cloud providers (AWS, GitHub itself, Slack) run automated leaked-key revocation partnerships that can auto-quarantine a key within minutes of a public commit — which is a useful safety net, but a private repo, a public fork, or a GitLab/Bitbucket host outside those partnerships gets no such backstop, so treat prevention as the primary control regardless.

Detection and Defense

A layered program, in order of where each control catches the problem:

  • Pre-commit hooks (gitleaks, detect-secrets) — cheapest catch, blocks the secret before it ever reaches a shared branch; can be bypassed locally, so it is not sufficient alone.
  • CI-enforced scanning on every push/PR, scanning full history on new repos and incremental diffs thereafter, as a backstop that cannot be bypassed by an individual developer.
  • Short-lived, scoped credentials (STS tokens, OIDC federation instead of static cloud keys, database credentials issued per-session by a vault) so that even a successful leak has a small blast radius and window.
  • A vault/secrets-manager as the only source of truth (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager) with runtime injection, so there is never a legitimate reason for a real secret to be typed into a file at all.
  • A documented, rehearsed rotation runbook per credential type, so that when a leak is found, rotation happens in minutes, not after a ticket sits in a queue.

Real-World Impact

GitHub’s own secret-scanning partner program and academic studies of public repositories have repeatedly found large volumes of live cloud credentials committed to public GitHub at any given time, and major cloud breaches have traced back to a single leaked access key in a public repo or a misconfigured .git directory left web-accessible. The pattern is consistent enough that “scan for secrets in git history” is now a standard first step in both external attack-surface assessments and internal security reviews, because the payoff-to-effort ratio for an attacker is extremely high.

Conclusion

Secrets in source control are a solved problem operationally, but only if prevention (pre-commit and CI scanning, secrets managers, short-lived credentials) is in place before the leak, and if the response to a found leak always starts with rotation — history cleanup is good hygiene but does not, by itself, revoke anything an attacker may have already copied.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL