Learn how to secure Git repositories from leaked secrets, unauthorized commits, and supply chain attacks - covering pre-commit hooks, Gitleaks, TruffleHog, branch protection, CODEOWNERS, signed commits, and Dependabot.
In 2024, GitHub reported scanning over one billion commits and detecting more than 12 million exposed secrets in public repositories. AWS keys, database connection strings, Stripe API tokens, private SSH keys — once pushed to a remote repository, these credentials are effectively public, even if the commit is later deleted. Here is why this is so serious: Git history is permanent. When a developer commits a secret and pushes it, automated bots are scanning public repositories continuously and can find exposed keys within minutes. Deleting the file in a new commit does not help — the secret is still visible in the commit history. Anyone who clones the repository gets the full history, including the secret. The damage from a leaked credential can be severe: * An exposed AWS access key can result in thousands of dollars of cloud charges within hours — attackers spin up expensive GPU instances for cryptocurrency mining * A leaked database password gives attackers direct access to customer data * An exposed Stripe API key allows fraudulent transactions against your payment account * A compromised CI/CD token can give attackers the ability to modify your deployment pipeline The good news: most of these incidents are entirely preventable with the right tooling and process. This module covers the four-layer approach to Git security that prevents secrets from entering repositories, detects any that slip through, and responds quickly when something is found. ---
Before setting up any tooling, every developer on the team needs to understand what should never appear in a Git repository. This is the foundation everything else builds on. ### The Secrets That Cause Real Incidents ``` Never commit these to Git: Cloud provider credentials: AWS access key IDs (start with AKIA or ASIA) AWS secret access keys GCP service account JSON files Azure client secrets API tokens: Stripe, Twilio, SendGrid, OpenAI API keys GitHub personal access tokens (start with ghp_) Slack tokens, Discord tokens Any string that looks like a random 32+ character value Database credentials: Database connection strings (postgresql://user:password@host/db) Database passwords Redis connection strings with auth tokens Infrastructure credentials: Private SSH keys (.pem files, id_rsa, id_ed25519) Kubernetes kubeconfig files with cluster credentials Docker registry credentials Vault tokens Configuration files with real values: .env files with actual credentials application.properties with database passwords config.yml with API keys terraform.tfvars with access keys ``` ### The Safe Pattern — .env Files The correct pattern for managing local development secrets is: ```bash # .env — this file contains real credentials, NEVER commit this DATABASE_URL=postgresql://appuser:realpassword@localhost:5432/mydb STRIPE_SECRET_KEY=sk_live_your_real_key_here AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE ``` ```bash # .env.example — this file contains placeholders, SAFE to commit # Copy this file to .env and fill in your values DATABASE_URL=postgresql://user:password@localhost:5432/dbname STRIPE_SECRET_KEY=sk_live_your_stripe_key AWS_ACCESS_KEY_ID=your_aws_access_key_id ``` ```bash # .gitignore — this file blocks .env from ever being tracked .env .env.local .env.*.local *.pem *.key credentials.json service-account.json .aws/credentials kubeconfig ``` The `.env.example` file gets committed so new developers know which variables are required. The `.env` file never gets committed because it is in `.gitignore`. ### What Does Not Work — Common Mistakes ``` Mistake 1: Delete the file and commit again Result: Secret still visible in git log — anyone can run git show <old-commit-hash> and see it Mistake 2: Make the repository private Result: If anyone forked or cloned before, they have the secret Bots may have already scraped it Mistake 3: Use git revert Result: Revert commit shows the old file in the diff Secret is still in the history The only safe response is: 1. Revoke the secret immediately (before doing anything else) 2. Remove it from Git history using git-filter-repo 3. Force push the cleaned history 4. Check access logs for any unauthorized use ``` ---
Pre-commit hooks are scripts that run automatically on the developer's machine before a commit is created. If the hook detects a secret, the commit is blocked before it ever leaves the local machine. This is the first layer of defense — catch secrets before they reach the remote repository at all. ### Install the Pre-Commit Framework The pre-commit framework manages hook installation and keeps hooks updated across the team. ```bash pip install pre-commit # Verify installation pre-commit --version ``` ### Create Your Pre-Commit Configuration Create a `.pre-commit-config.yaml` file in the root of your repository: ```yaml repos: # Standard Git safety checks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: detect-private-key # Catches PEM private keys - id: check-added-large-files # Prevents committing large files (often data with secrets) args: ['--maxkb=500'] - id: check-yaml # Prevents broken YAML config files - id: check-json # Prevents broken JSON files - id: trailing-whitespace # Gitleaks secret detection - repo: https://github.com/gitleaks/gitleaks rev: v8.18.4 hooks: - id: gitleaks ``` ### Install the Hooks ```bash # Install hooks into the .git directory — runs on every commit pre-commit install # Run against all files in the repository (first-time scan) pre-commit run --all-files # Update hooks to latest versions pre-commit autoupdate ``` Once installed, every `git commit` automatically runs these checks first. If a secret is detected, the commit is blocked: ```bash # Example: developer accidentally tries to commit a file with an API key git commit -m "add payment integration" Detect hardcoded secrets.................................................Failed - hook id: gitleaks - exit code: 1 Finding: STRIPE_SECRET_KEY=sk_live_abc123def456... File: src/payment.js Line: 14 RuleID: stripe-access-token ``` The commit is rejected and the developer sees exactly which file and line contains the problem. ### Sharing Hooks Across the Team The `.pre-commit-config.yaml` file is committed to the repository so everyone uses the same hooks. But each developer must run `pre-commit install` locally to activate them. Add this to your project setup script or Makefile: ```bash # Makefile setup: pip install pre-commit pre-commit install @echo "Pre-commit hooks installed. Every commit will now be scanned for secrets." ``` ### Important — Hooks Can Be Bypassed Pre-commit hooks can be bypassed with `git commit --no-verify`. This is why pre-commit hooks are the first layer, not the only layer. Always add secret scanning in CI/CD as a second mandatory layer that cannot be bypassed. ---
Gitleaks is the industry standard tool for detecting secrets in Git repositories. It scans commit history, staged changes, and files using pattern matching against hundreds of known secret formats including AWS keys, GitHub tokens, Stripe keys, database URLs, and more. ### Installation ```bash # macOS brew install gitleaks # Linux — download binary curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64 -o gitleaks chmod +x gitleaks && sudo mv gitleaks /usr/local/bin/ # Docker docker pull ghcr.io/gitleaks/gitleaks:latest ``` ### Basic Usage ```bash # Scan the entire current repository including all history gitleaks detect --source . --verbose # Scan only staged changes (useful before committing) gitleaks protect --staged --verbose # Scan a specific commit range gitleaks detect --source . --log-opts="HEAD~10..HEAD" # Scan and save results to a JSON report gitleaks detect --source . --report-path gitleaks-report.json # Scan a remote repository without cloning gitleaks detect --source https://github.com/org/repo --verbose ``` ### Adding Gitleaks to CI/CD — GitHub Actions ```yaml # .github/workflows/secret-scan.yml name: Secret Scanning on: push: branches: [main, develop] pull_request: branches: [main] jobs: gitleaks: name: Detect Secrets runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Full history required for complete scanning - name: Run Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ### Adding Gitleaks to CI/CD — GitLab CI ```yaml # .gitlab-ci.yml secret-scan: stage: security image: zricethezav/gitleaks:latest script: - gitleaks detect --source . --verbose --exit-code 1 rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' ``` ### Custom Rules for Project-Specific Secrets If your project uses internal API tokens with specific patterns, create a `.gitleaks.toml` configuration: ```toml # .gitleaks.toml title = "Project-specific Gitleaks config" [extend] useDefault = true # Keep all default rules and add our own # Custom rule: detect internal API tokens [[rules]] id = "internal-api-token" description = "Internal API token" regex = '''(?i)internal[_-]?api[_-]?(?:key|token)\s*[:=]\s*['"]?[a-zA-Z0-9]{32,}['"]?''' tags = ["internal", "api"] # Custom rule: database connection strings [[rules]] id = "database-url" description = "Database connection string with credentials" regex = '''(?i)(?:postgres|mysql|mongodb)(?:ql)?:\/\/[^:]+:[^@]+@[^\/]+''' tags = ["database"] # Allowlist — safe to ignore [[allowlists]] description = "Known test fixtures and example files" paths = [ '''tests/fixtures/.*''', '''docs/examples/.*''', '''\.gitleaks\.toml$''' ] ``` ### Using a Baseline to Manage Existing Findings For repositories that already have findings in their history that you cannot fix immediately, create a baseline so only new findings are flagged: ```bash # Create baseline from current findings gitleaks detect --source . --report-path .gitleaks-baseline.json # Future scans only report NEW findings not in the baseline gitleaks detect --source . --baseline-path .gitleaks-baseline.json ``` ---
While Gitleaks is excellent for active scanning, TruffleHog has a powerful capability that sets it apart: it validates whether detected secrets are still live and active by making actual API calls to verify them. This is critical when auditing a repository — knowing which exposed secrets are still valid tells you exactly which ones require immediate rotation versus which have already been rotated or expired. ### Installation ```bash # macOS brew install trufflehog # Linux/Docker docker run --rm trufflesecurity/trufflehog:latest --help # Via script curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin ``` ### Scanning a Repository ```bash # Scan a local repository — only verified (active) secrets trufflehog git file://. --results=verified # Scan a remote GitHub repository trufflehog git https://github.com/org/repo --results=verified # Scan and output JSON for further processing trufflehog git file://. --results=verified --json # Scan a specific commit range trufflehog git file://. --since-commit main --branch feature-branch --results=verified ``` ### What TruffleHog Verification Looks Like When TruffleHog finds an AWS key, it calls `sts:GetCallerIdentity` to check if the key is active. The output tells you everything you need to know: ``` 🐷🔑🐷 TruffleHog. Unearth your secrets. Found verified result 🐷🔑 Detector Type: AWS Verified: true ← This key is ACTIVE and must be rotated NOW Raw result: AKIAYVP4CIPPERUVIFXG File: config/deploy.sh Commit: fbc14303ffbf8fb1c2c1914e8dda7d0121633aca Author: developer@company.com Repository: https://github.com/company/repo ``` A `Verified: true` result means the credential is currently active and exploitable. Rotate it immediately. ### Adding TruffleHog to CI/CD ```yaml # GitHub Actions - name: TruffleHog Secret Scan uses: trufflesecurity/trufflehog@main with: base: ${{ github.event.pull_request.base.sha }} head: ${{ github.event.pull_request.head.sha }} extra_args: --results=verified,unknown ``` ### Gitleaks vs TruffleHog — When to Use Each | Scenario | Tool | Why | |:---------|:-----|:----| | Pre-commit hook | Gitleaks | Fast, pattern-based, blocks commits instantly | | CI/CD pipeline scan on every PR | Gitleaks | Reliable, no external API calls required | | Full repository audit | TruffleHog | Scans all branches, tags, and deleted commits | | Verifying if found secrets are active | TruffleHog | Only tool that validates against live APIs | | Scheduled weekly security sweep | Both | Complementary — different detection strengths | | On-boarding a legacy repository | TruffleHog | Deep history scan with verification | ---
Branch protection rules prevent unauthorized or unreviewed code from being merged into your main branch. They are one of the most important controls for securing Git repositories because they enforce process — no individual can bypass code review or push directly to production branches. ### Setting Up Branch Protection on GitHub Go to your repository → Settings → Branches → Add rule. For your `main` branch, configure: ``` Branch name pattern: main Required settings: ✅ Require a pull request before merging - Require at least 1 approving review - Dismiss stale pull request approvals when new commits are pushed - Require review from Code Owners (if CODEOWNERS file exists) ✅ Require status checks to pass before merging - Require branches to be up to date before merging - Add your CI/CD pipeline jobs as required status checks - Add your secret scanning job as a required status check ✅ Do not allow bypassing the above settings (This applies rules even to administrators) Recommended additional settings: ✅ Require signed commits ✅ Restrict who can push to matching branches ✅ Block force pushes ``` ### The Result — Why This Matters With branch protection enabled: ``` Without protection: Developer A pushes directly to main Code review happens after merge (or never) Secret in code goes undetected until production Attacker finds secret within hours With protection: Developer A creates a PR Gitleaks secret scan runs automatically PR blocked — secret detected before anyone reviews Developer removes secret, PR approved, merge allowed Secret never reaches main branch ``` ### CODEOWNERS — Require Specific Approvals for Sensitive Files CODEOWNERS lets you define that specific files or directories require approval from designated owners — not just any reviewer. Create a file called `CODEOWNERS` in the root of your repository: ``` # CODEOWNERS # Format: file-pattern @owner # Infrastructure files require DevOps team approval /terraform/ @company/devops-team /kubernetes/ @company/devops-team /.github/workflows/ @company/devops-team # Security configuration requires security team approval /security/ @company/security-team .gitleaks.toml @company/security-team # Payment code requires senior engineer sign-off /src/payments/ @senior-engineer-username @company/security-team # Default ownership (applies to everything else) * @company/engineering-leads ``` With `Require review from Code Owners` enabled in branch protection, changes to `/terraform/` cannot be merged without approval from someone in the `devops-team` group — even if the PR already has other approvals. ---
In 2024, GitHub reported scanning over one billion commits and detecting more than 12 million exposed secrets in public ...
Before setting up any tooling, every developer on the team needs to understand what should never appear in a Git reposit...
Pre-commit hooks are scripts that run automatically on the developer's machine before a commit is created. If the hook d...
Gitleaks is the industry standard tool for detecting secrets in Git repositories. It scans commit history, staged change...
While Gitleaks is excellent for active scanning, TruffleHog has a powerful capability that sets it apart: it validates w...
Branch protection rules prevent unauthorized or unreviewed code from being merged into your main branch. They are one of...
Your application code is only part of what gets deployed. Every npm package, pip library, and Maven dependency is code y...
Commit signing proves that a commit was genuinely made by the person whose name is on it. Without signing, anyone with r...
Even with all these controls in place, leaks happen. The response must be immediate — automated scrapers find exposed ke...
This lab walks through setting up complete Git security on a real repository. By the end you will have pre-commit hooks ...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.