← Back to list

Hardening Your Git Workflow: .gitignore, Gitleaks, and Pipeline Secret Scanning

Most credential leaks don’t happen because of hackers. They happen because a developer hit git push without thinking. Here's how to make…

Gautammakwana in DevSecOps & AI · 2026-03-08 17:40 · 4 claps · 12.0 min read
#devsecops #git #gitleaks #secret-scanning #cybersecurity
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity 🔓 · Open Source

Hardening Your Git Workflow: .gitignore, Gitleaks, and Pipeline Secret Scanning

Most credential leaks don’t happen because of hackers. They happen because a developer hit git push without thinking. Here's how to make that impossible.

Picture this: it’s 3 AM and nobody on your team is awake. But an automated bot is — and it just found an AWS key you committed six hours ago. By morning, you’re staring at a $47,000 cloud bill from hundreds of crypto-mining EC2 instances that spun up while you slept. This exact scenario played out in 2023, and the window between exposure and exploitation was just 11 minutes.

The frustrating part? Two tools sitting right in your development workflow would have stopped it cold: a thoughtfully configured .gitignore and Gitleaks running as a pre-commit hook — and as a CI/CD pipeline check.

Walk through this guide and you’ll have both operational — plus scanners running inside every major CI/CD platform you might use.

Git Doesn’t Forget — Even When You Want It To

Think of your Git repository less like a folder of files and more like a ledger that keeps every transaction forever. When you commit a file, that snapshot is permanent. When you delete it, you’re adding a new entry that says “this file is gone” — but every previous entry remains untouched and fully readable.

The moment a secret touches a commit, it’s out of your control. Removing the file afterward is cosmetic — it changes nothing about what’s already recorded.

This is why developers who discover a leaked credential and respond by simply deleting the file are still in trouble. Every person who cloned the repository, every service that mirrored it, every bot that indexed it — they already have a copy of that commit. The only real fix is preventing the commit from happening in the first place, then backing that prevention up at the pipeline level for anything that slips through.

🔒 First Principle

A credential that never reaches a commit cannot be leaked through version control. Build your defences at the earliest possible point — your local machine — and treat CI/CD scanning as the fallback, not the primary check.

Part 1: .gitignore as a Security Boundary

Most engineers treat .gitignore as a housekeeping file — somewhere to tuck away build output and IDE clutter. Flip that perspective. Treat it as an access control list for your repository. Everything that should never leave your machine belongs here, configured explicitly before you write your first line of code.

Files That Should Never Enter Version Control

  • All environment variable files: .env, .env.*, .env.local, .env.production
  • Credential stores: credentials.json, secrets.yaml, config/secrets.yml
  • Cryptographic keys: *.pem, *.key, id_rsa, id_ed25519
  • Platform credential directories: .aws/credentials, .gcloud/, kubeconfig
  • Application logs: *.log — these often capture full HTTP headers, including bearer tokens
  • Local databases: *.sqlite, *.db

⚠️ Timing Matters

*.gitignore only intercepts files that haven't been tracked yet. If Git is already watching a file, adding it to .gitignore does nothing. You'll need git rm --cached <file> to stop tracking it — and once you do, rotate whatever credentials were inside it, immediately.*

A Security-First .gitignore Template

# Environment & Secrets
.env
.env.*
!.env.example
*.local

# Private Keys
*.pem
*.key
*.p12
id_rsa
id_ed25519

# Cloud Credentials
.aws/
.gcloud/
kubeconfig
serviceAccountKey.json

# Secret Config Files
secrets.yaml
secrets.yml
credentials.json
config/secrets.*

# Logs
*.log
logs/

# Database
*.sqlite
*.sqlite3
*.db

# IDE & OS
.idea/
.vscode/
.DS_Store
Thumbs.db

Don’t Start from Scratch — Use gitignore.io

Rather than building your .gitignore by hand, gitignore.io (also reachable at gitignore.io) generates a tailored file based on your tech stack. Type in your languages, frameworks, editors, and operating systems — it outputs a comprehensive, community-maintained .gitignore covering all of them. It's a solid starting point that you then extend with the security-sensitive entries above.

Keeping New Developers Safe: The .env.example Pattern

When you exclude your .env file, contributors who clone the project have no idea what variables the application needs. Solve this by committing a .env.example alongside it — a file that documents every required variable using obviously fake placeholder values. Developers copy it locally, substitute real values, and the actual .env never touches Git.

# .env.example — COMMIT THIS FILE (no real secrets)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
AWS_ACCESS_KEY_ID=your_access_key_here
STRIPE_SECRET_KEY=sk_live_your_key_here
JWT_SECRET=your_jwt_secret_here

Part 2: Blocking Bad Commits Before They Happen

Scanning history tells you what’s already done. Pre-commit hooks stop problems before they start. A pre-commit hook is a script Git invokes automatically whenever you run git commit — and if the script exits with a non-zero code, the commit is cancelled entirely.

Option A: The Minimal Setup

For solo projects or quick onboarding, a single line in your hook file is all you need:

# .git/hooks/pre-commit
#!/bin/sh
gitleaks protect --staged --redact

Try committing a file that contains a real-looking secret and you’ll see something like this:

$ git commit -m "add config"

Finding:  STRIPE_SECRET_KEY=sk_live_51HG...
RuleID:   stripe-secret-key
File:     src/config.js  Line: 8

Detect hardcoded secrets.........................Failed

The commit never happens. The secret never leaves your machine.

Option B: The pre-commit Framework (Best for Teams)

Writing directly to .git/hooks/ has one major drawback — those files aren't committed to the repository, so each developer manages their own. Standards drift, and coverage becomes inconsistent across the team. The pre-commit framework solves this by centralising all hook definitions in a YAML file that does live in version control.

Step 1: Install the framework:

brew install pre-commit  # macOS
pip install pre-commit   # cross-platform

Step 2: Define your hooks in .pre-commit-config.yaml at the repository root:

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

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files

Step 3: Activate and verify:

pre-commit install
# pre-commit installed at .git/hooks/pre-commit

Now trigger a detection with a fake Stripe secret. Use the command for your platform:

macOS / Linux

echo 'STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1' > secret_test.py
git add secret_test.py && git commit -m "test"

Windows CMD

echo STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1 > secret_test.py
git add secret_test.py && git commit -m "test"

Windows PowerShell

Set-Content secret_test.py 'STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1'
git add secret_test.py && git commit -m "test"

Expected output — commit is blocked:

Detect hardcoded secrets................................................Failed
- hook id: gitleaks
- exit code: 1

    Finding:     STRIPE_SECRET_KEY=REDACTED
    RuleID:      stripe-access-token
    Entropy:     4.875000
    File:        secret_test.py
    Line:        1

Clean up the test file:

git restore secret_test.py   # macOS / Linux
git checkout secret_test.py  # Windows

⚠️ Why Not the AWS Example Key?

You may have seen tutorials use AWS_SECRET=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY as a test value. This will not trigger Gitleaks. That exact string — along with other well-known documentation keys like AKIAIOSFODNN7EXAMPLE and ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456789 — is on Gitleaks' internal allowlist because it appears in thousands of official AWS, GitHub, and tutorial pages. Always use a realistic-looking key with random characters that isn't from official documentation. The Stripe key above works reliably for this purpose.

Commit .pre-commit-config.yaml to the repository. Any developer who clones the project and runs pre-commit install automatically inherits the same protections — one command, consistent security posture across the whole team.

Part 3: Gitleaks — Your Automated Secret Detector

Even a well-maintained .gitignore won't catch a developer who hardcodes an API key directly into app.js because they were "just testing." That's where Gitleaks earns its place in the toolchain.

Gitleaks is an open-source secret scanner — MIT-licensed, with north of 19,000 GitHub stars and over 20 million Docker pulls. It scans your repository using a combination of regular expression matching and Shannon entropy analysis. Strings that look like real credentials (high randomness, pattern-matches a known format) get flagged. It ships with detection rules for 160+ credential types and lets you extend those rules for anything proprietary to your organisation.

💡 Under the Hood

Entropy scoring is what separates Gitleaks from a basic regex grep. Placeholder values like your_api_key_here score low on the entropy scale and pass through. High-entropy strings that match a known secret pattern — like sk_live_4xKqP9mN2rL7vB8wE3jH6tY1 (entropy: 4.875) — get caught. Note that well-known documentation example keys are internally allowlisted by Gitleaks regardless of their entropy, so always test with realistic random values rather than strings copied from official docs.

Getting Gitleaks Installed (Current Release: v8.30.0)

# macOS
brew install gitleaks

# Linux (Debian / Ubuntu)
sudo apt install gitleaks

# Linux (Red Hat / Fedora)
sudo dnf install gitleaks

# Windows (Chocolatey)
choco install gitleaks

# Confirm the installation
gitleaks version  # Expected: v8.30.0

Auditing What’s Already in Your Repository

Before setting up ongoing protection, run a retrospective scan across your full commit history. You may already have leaked credentials sitting in old commits.

# Scan full git history
gitleaks git .

# Verbose output
gitleaks git . --verbose

# Save findings to a JSON report
gitleaks git . --report-path gitleaks-report.json

Here’s what a positive finding looks like:

Finding:    STRIPE_SECRET_KEY=sk_live_4xKqP9mN2rL7vB8wE3jH6tY1
RuleID:     stripe-access-token
Entropy:    4.875
File:       config/deploy.rb  Line: 14
Commit:     3f2a1b9c...
Author:     dev@example.com

Spot real credentials in that output? Rotate them before doing anything else — before cleaning history, before notifying anyone. Even visibility into a private repository is enough to constitute a breach.

Part 4: Tuning Gitleaks with .gitleaks.toml (Custom Rules)

Out of the box, Gitleaks is thorough — sometimes more thorough than you need. Documentation files, example configs, and test fixtures will generate false positives against the default ruleset. The .gitleaks.toml configuration file lets you suppress that noise while also adding detection rules tailored to tokens your platform uses internally.

# .gitleaks.toml
[extend]
useDefault = true

[[allowlists]]
description = "Ignore example values in documentation and templates"
paths = ['''docs/.*''', '''README.md''', '''.env.example''']

[[rules]]
description = "Proprietary Internal API Token"
id = "myapp-api-token"
regex = '''myapp_[0-9a-zA-Z]{32}'''
tags = ["api", "internal"]

The useDefault = true directive preserves all 160+ built-in rules while layering your customisations on top. The allowlist exempts specific paths where fake credentials are intentional by design, and the custom rule adds detection for your own token format — something the default ruleset naturally can't know about.

Part 5: Enforcing Secret Scanning Across Every CI/CD Platform

Local hooks protect developers who set them up — but what about someone who joined the team last week and hasn’t run pre-commit install yet? Or a CI bot making automated commits? Or a third-party integration pushing directly to a branch?

Developer workstation  →  pre-commit hook  →  stopped at source
        ↓  (if bypassed for any reason)
CI/CD pipeline         →  Gitleaks scan    →  PR or branch blocked
        ↓  (belt and suspenders)
Protected branch       →  clean history    →  nothing dangerous merged

GitHub Actions

The official gitleaks-action handles the integration end to end — no binary installation, no manual configuration. It scans every push and pull request and posts inline annotations when it finds something suspicious.

# .github/workflows/gitleaks.yml
name: Secret Scanning

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required — shallow clones miss historical secrets

      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

fetch-depth: 0 is the one setting you cannot skip. GitHub Actions defaults to a shallow clone containing only the most recent commit. Without the full history, Gitleaks is blind to anything that leaked weeks or months ago.

GitLab CI/CD

GitLab pipelines use the official Gitleaks Docker image, keeping the setup clean and version-pinned without any installation steps on the runner.

# .gitlab-ci.yml
stages:
  - security
  - build
  - test
  - deploy

gitleaks:
  stage: security
  image: zricethezav/gitleaks:v8.30.0
  script:
    - gitleaks git . --verbose --report-path gitleaks-report.json
  artifacts:
    when: on_failure
    paths:
      - gitleaks-report.json
    expire_in: 7 days
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_BRANCH == "develop"

On large repositories where scanning the full history on every merge request is slow, scope the scan to commits introduced by the MR only:

gitleaks-diff:
  stage: security
  image: zricethezav/gitleaks:v8.30.0
  script:
    - git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
    - gitleaks git . --log-opts="origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}..HEAD" --verbose
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

This scans only the delta between the feature branch and the merge target — substantially faster for repositories with long commit histories.

Azure DevOps

Azure Pipelines requires downloading the Gitleaks binary directly since there’s no managed action equivalent. Pin the download URL to a specific version — Azure agents are ephemeral and rebuild from scratch on every run, so version consistency is your responsibility.

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main
      - develop

pr:
  branches:
    include:
      - main
      - develop

stages:
  - stage: Security
    displayName: Security Scanning
    jobs:
      - job: Gitleaks
        displayName: Secret Scanning with Gitleaks
        pool:
          vmImage: ubuntu-latest
        steps:
          - checkout: self
            fetchDepth: 0  # Full repository history

          - script: |
              curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.30.0/gitleaks_8.30.0_linux_x64.tar.gz | tar -xz
              sudo mv gitleaks /usr/local/bin/
            displayName: Install Gitleaks

          - script: |
              gitleaks git . --verbose --report-path $(Build.ArtifactStagingDirectory)/gitleaks-report.json
            displayName: Run Gitleaks Scan

          - task: PublishBuildArtifacts@1
            condition: failed()
            inputs:
              pathToPublish: $(Build.ArtifactStagingDirectory)/gitleaks-report.json
              artifactName: gitleaks-findings
            displayName: Archive Findings on Failure

If your agent pool supports Docker, this alternative is cleaner and eliminates the install step entirely:

- task: Docker@2
  displayName: Run Gitleaks via Docker
  inputs:
    command: run
    arguments: >
      -v $(Build.SourcesDirectory):/repo
      zricethezav/gitleaks:v8.30.0
      git /repo --verbose

Bitbucket Pipelines

Bitbucket’s architecture makes the Docker-as-image approach particularly clean. Each pipeline step runs inside its own isolated container, so pointing the step image directly at Gitleaks means zero setup overhead and guaranteed version consistency.

# bitbucket-pipelines.yml
image: atlassian/default-image:4

definitions:
  steps:
    - step: &gitleaks-scan
        name: Secret Scanning (Gitleaks)
        image: zricethezav/gitleaks:v8.30.0
        script:
          - gitleaks git . --verbose --report-path gitleaks-report.json
        artifacts:
          - gitleaks-report.json

pipelines:
  default:
    - step: *gitleaks-scan

  branches:
    main:
      - step: *gitleaks-scan
      - step:
          name: Build & Test
          script:
            - echo "Build steps here"

  pull-requests:
    '**':
      - step: *gitleaks-scan

To scan only commits introduced by a pull request rather than the full history:

- step:
    name: Scan PR Commits Only
    image: zricethezav/gitleaks:v8.30.0
    script:
      - git fetch origin $BITBUCKET_PR_DESTINATION_BRANCH
      - gitleaks git . --log-opts="origin/${BITBUCKET_PR_DESTINATION_BRANCH}..HEAD" --verbose

Jenkins

Jenkins supports two integration patterns. The shell-based approach works on any agent type; the Docker agent is more reproducible across heterogeneous build infrastructure.

Shell-based (universal compatibility):

// Jenkinsfile
pipeline {
    agent any

    stages {
        stage('Secret Scanning') {
            steps {
                sh '''
                    if ! command -v gitleaks &> /dev/null; then
                        curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.30.0/gitleaks_8.30.0_linux_x64.tar.gz | tar -xz
                        sudo mv gitleaks /usr/local/bin/
                    fi
                    gitleaks git . --verbose --report-path gitleaks-report.json
                '''
            }
            post {
                failure {
                    archiveArtifacts artifacts: 'gitleaks-report.json', allowEmptyArchive: true
                    error 'Secret detected by Gitleaks — build halted.'
                }
            }
        }

        stage('Build') {
            steps {
                echo 'Build steps here'
            }
        }
    }
}

Docker agent (recommended for consistent environments):

pipeline {
    agent {
        docker {
            image 'zricethezav/gitleaks:v8.30.0'
            args '-v $WORKSPACE:/repo --entrypoint=""'
        }
    }

    stages {
        stage('Secret Scanning') {
            steps {
                sh 'gitleaks git /repo --verbose --report-path /repo/gitleaks-report.json'
            }
            post {
                failure {
                    archiveArtifacts artifacts: 'gitleaks-report.json'
                }
            }
        }
    }
}

One Jenkins-specific detail worth highlighting: the post { failure { } } block is essential. Without it, the findings report is cleaned up along with the failed build workspace and you lose visibility into exactly what triggered the block. If you manage multiple repositories, consider packaging the Gitleaks install logic into a Jenkins Shared Library so teams aren't each maintaining their own copy.

Platform Quick-Reference

The one rule that applies on every platform: always fetch the complete repository history before scanning. Most CI platforms default to shallow clones for speed — great for build times, terrible for security scanning. Secrets committed months ago become completely invisible to a shallow scan.

Part 6: When Something Gets Through — Your Response Playbook

Even well-defended systems have incidents. Speed matters more than anything else once an exposure is confirmed. Work through this sequence in order:

  1. Revoke the credential before anything else. Don’t wait until history is clean or the team is notified. Treat every exposed credential as already harvested — revoke it immediately, and this is true whether the repository is private or public.
  2. Rewrite the commit history. Use git filter-repo (the current recommended approach) or BFG Repo Cleaner to surgically remove the secret from every commit where it appeared.
  3. Force push and coordinate the team. Rewriting history requires a force push, and everyone working from the old history will need to re-clone or carefully rebase. Communicate the plan before executing.
  4. Scan the cleaned repository. Run gitleaks git . after the rewrite to confirm nothing else is hiding in history.
  5. Audit your pipeline artefacts. CI systems frequently capture build logs, environment dumps, and debug output. Verify the exposed credential didn’t appear in any stored artefact that’s still accessible.

⚠️ On GitHub Caching

GitHub maintains cached views of repository content that can persist even after a force push rewrites history. If your repository was public at any point during the exposure window, assume the credential has already been collected somewhere. Rotate first — history cleanup closes the door, it doesn’t undo what’s already been seen.

Pre-Launch Security Checklist

Before shipping any project, verify each of these is in place:

**.gitignore** covers environment files, private keys, credential stores, cloud configs, and logs

**.env.example** is committed with realistic-looking placeholder values for every required variable

**.pre-commit-config.yaml** is committed with Gitleaks pinned at v8.30.0

**pre-commit install** is documented in the project README and confirmed on every contributor's machine

**.gitleaks.toml** is configured to suppress false positives from documentation paths and includes any organisation-specific token patterns

Retrospective scan complete — gitleaks git . has been run against the full repository history at least once

CI/CD pipeline has Gitleaks running on all pull requests with full history checkout enabled

References

  1. Gitleaks Official Site
  2. GitHub
  3. [Gitleaks v8.30.0 Release Notes](http://github.com/gitleaks/gitleaks/releases · November 2025)
  4. pre-commit Framework Documentation
  5. GitHub Docs: Removing Sensitive Data from a Repository
  6. OWASP Secrets Management Cheat Sheet
  7. Git Reference: gitignore
  8. gitleaks/gitleaks-action
  9. BFG Repo Cleaner
  10. GitLab CI/CD Documentation
  11. Azure Pipelines Documentation
  12. Bitbucket Pipelines Documentation
  13. Jenkins Pipeline Syntax Reference

Further Reading — DevSecOps Series

What is DevSecOps & Why It Matters The foundations of DevSecOps — what it is, why it matters, and how security shifts left. 🔗 Read on Medium

Article 2 → Hardening Your Git Workflow: .gitignore, Gitleaks & Pipeline Secret Scanning Hands-on setup of pre-commit hooks, catching secrets before they hit GitHub, and building team habits from day one. 📍 You are here

Article 3 → Dependency Security — Automating SCA with Dependabot and Open Source Vulnerability Scanning


메타데이터
post_id
0f7f6a753cb3
slug
hardening-your-git-workflow-gitignore-gitleaks-and-pipeline-secret-scanning-0f7f6a753cb3
url
https://medium.com/devsecops-ai/hardening-your-git-workflow-gitignore-gitleaks-and-pipeline-secret-scanning-0f7f6a753cb3
canonical_url
https://medium.com/devsecops-ai/hardening-your-git-workflow-gitignore-gitleaks-and-pipeline-secret-scanning-0f7f6a753cb3
author_url
https://medium.com/@gautammakwana421
status
ok
fetched_at
2026-07-11 03:16:26