
August 24, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Hardcoded credentials remain one of the most common and damaging security failures in production web systems. Implementing secrets scanning in Git and CI with gitleaks catches API keys, database passwords, and private tokens before they reach your repository history or deployment pipeline. This guide provides the exact configuration I use on production Laravel and legal-tech projects to enforce this safety net without disrupting legitimate development workflows.
How do you configure secrets scanning in Git and CI with gitleaks locally?
The first line of defense is always local. Relying solely on CI means secrets have already been pushed to the remote, requiring history rewriting if caught. For developers working on sensitive projects like legal-tech portals handling client data, preventing the leak at the source is non-negotiable. Gitleaks v8.21+ (current stable in 2026) supports native Git hook integration that validates staged changes before the commit object is created.
Installing and configuring the pre-commit hook
Install gitleaks via Homebrew, apt, or direct binary download. For team consistency, pin the version in your project documentation or DevContainer configuration. On Ubuntu 24.04 servers I maintain, I install it globally so all users share the same scanner version:
# Install gitleaks v8.21.x (verify latest tag on GitHub releases)
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz | tar -xz -C /usr/local/bin gitleaks
# Verify installation
gitleaks version
# Initialize pre-commit hook in your Laravel/PHP project root
gitleaks protect --staged --pre-commit The --staged flag is critical. Without it, gitleaks scans the entire working directory including untracked files and build artifacts, producing excessive false positives. With --staged, it only examines what you are about to commit. The --pre-commit flag installs the hook script into .git/hooks/pre-commit automatically.
Handling legitimate test fixtures and example configs
Real projects contain placeholder credentials in test suites, example configurations, and documentation. Blocking these halts development unnecessarily. Create a .gitleaksignore file in your repository root to allowlist specific false positives by their SHA256 fingerprint:
# .gitleaksignore
# Example AWS key used in unit tests (tests/Fixtures/AwsClientTest.php)
a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
# Placeholder Stripe test key in README examples
fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321 Generate fingerprints for legitimate secrets using gitleaks detect --verbose on the offending commit, then copy the Fingerprint value. Never allowlist by file path alone — paths change during refactoring, but content hashes remain stable. Review the allowlist quarterly as part of your CI/CD maintenance routine.
How do you integrate gitleaks into GitLab CI pipelines?
Local hooks can be bypassed. A determined developer can skip them with --no-verify, or a misconfigured environment might not have the hook installed. CI integration serves as the authoritative enforcement point. In my experience managing shared infrastructure for multiple Nepal-based legal and translation service sites, the CI job is where most real-world catches happen after initial adoption.
Production-ready GitLab CI job configuration
Add this job to your .gitlab-ci.yml. It runs early in the pipeline, fails fast, and uploads results as an artifact for audit trails. This configuration works with GitLab CI runner images that include curl and tar:
gitleaks-scan:
stage: validate
image: alpine:3.20
variables:
GITLEAKS_VERSION: "8.21.2"
before_script:
- apk add --no-cache curl tar git
- curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz | tar -xz -C /usr/local/bin gitleaks
script:
# Scan full history for MRs; use --log-opts for branch-specific ranges
- gitleaks detect --source . --report-format json --report-path gitleaks-report.json --exit-code 1
artifacts:
when: always
paths:
- gitleaks-report.json
expire_in: 30 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH The when: always directive ensures the report uploads even when secrets are found. Without it, a failed job produces no artifact, making triage impossible. Set expire_in according to your compliance requirements — 30 days covers most audit windows while avoiding storage bloat.
Scanning only changed commits in large repositories
Full-history scans on mature projects with thousands of commits take minutes. For monorepos or legacy codebases, restrict scanning to the merge request diff range:
script:
# Fetch target branch for accurate diff calculation
- git fetch origin ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
# Scan only commits between target and source
- gitleaks detect --source . --log-opts "--since=${CI_MERGE_REQUEST_DIFF_BASE_SHA}" --report-format json --report-path gitleaks-report.json --exit-code 1 This reduces scan time from minutes to seconds on active branches. Reserve full-history scans for nightly scheduled pipelines or initial baseline assessments. On a legal document management system I maintained, switching to diff-based scanning reduced CI feedback time from 4 minutes to 18 seconds per merge request.
What custom gitleaks rules should PHP and Laravel projects use?
Gitleaks ships with 100+ built-in rules covering AWS, GitHub, Stripe, and other major providers. However, PHP and Laravel ecosystems have unique secret patterns that default rules miss. Custom rules catch framework-specific leaks like APP_KEY values, Sanctum tokens, and Nepal payment gateway credentials.
| Rule Target | Pattern Type | False Positive Risk | Detection Confidence |
|---|---|---|---|
| Laravel APP_KEY | Regex + entropy | Low | High |
| eSewa / Khalti Keys | Prefix + length | Medium | High |
| Sanctum Personal Tokens | Format + context | Low | Medium |
| Generic Base64 Secrets | Entropy only | High | Low |
| Database URLs in Config | URI scheme + password | Medium | High |
Creating a custom ruleset for Laravel applications
Create .gitleaks.toml in your project root. Extend the default rules rather than replacing them to retain upstream provider coverage:
# .gitleaks.toml
title = "Laravel Project Secrets Rules"
[extend]
useDefault = true
[[rules]]
id = "laravel-app-key"
description = "Detected Laravel APP_KEY in source code"
regex = '''APP_KEY\s*=\s*base64:[A-Za-z0-9+/]{43}='''
keywords = ["APP_KEY", "base64"]
tags = ["laravel", "php"]
[[rules]]
id = "nepal-payment-esewa"
description = "Detected eSewa merchant secret key"
regex = '''(?i)(esewa|ESEWA).{0,30}(secret|key|token)\s*[:=]\s*['\"][a-zA-Z0-9]{32,64}['\"]'''
keywords = ["esewa", "merchant"]
tags = ["nepal", "payment"]
[[rules]]
id = "sanctum-personal-token"
description = "Detected Laravel Sanctum personal access token"
regex = '''\b[a-zA-Z0-9]{40}\|[a-zA-Z0-9]{64}\b'''
keywords = ["|"]
tags = ["laravel", "sanctum", "api"]
[[rules]]
id = "database-url-with-password"
description = "Detected database connection string with embedded password"
regex = '''(mysql|postgres|pgsql):\/\/[^:]+:[^@]+@[^\s]+'''
keywords = ["mysql://", "postgres://", "pgsql://"]
tags = ["database", "config"] The Sanctum token rule deserves explanation. Sanctum formats tokens as {id}|{hash} where both segments are fixed-length alphanumeric strings. The pipe character combined with exact lengths makes this pattern highly specific. Generic base64 detectors miss this format entirely because the pipe breaks standard encoding assumptions.
Validating custom rules before deployment
Never deploy untested regex rules. False positives erode trust and lead developers to disable scanning. Validate against known-good and known-bad samples:
# Test against fixture files containing intentional secrets
gitleaks detect --config .gitleaks.toml --source tests/Fixtures/SecretsSamples/ --verbose
# Verify no matches on clean production config templates
gitleaks detect --config .gitleaks.toml --source config/ --verbose
# Benchmark rule performance on full repo (should complete in <30s for mid-size projects)
time gitleaks detect --config .gitleaks.toml --source . --no-git If a rule triggers more than 5 times on legitimate code during validation, refine the regex or add contextual keywords. Precision matters more than recall in custom rules — let the default high-confidence rules handle generic cases.
How do you remediate exposed secrets found by gitleaks?
Finding a secret is only half the problem. Remediation requires revoking the compromised credential, removing it from Git history, and verifying no downstream systems cached the old value. Skipping any step leaves residual risk.
Credential revocation must precede history rewriting
This ordering is counterintuitive but essential. If you rewrite Git history first, the old secret remains valid in production systems during the cleanup window. An attacker who cloned the repository before the force-push retains access. Always rotate the credential immediately upon discovery, then proceed to history cleanup.
For Laravel projects, regenerate APP_KEY with php artisan key:generate and re-encrypt any data encrypted with the old key. For API integrations like those described in Laravel API best practices, generate new tokens and update environment variables across all deployed instances simultaneously.
Rewriting Git history safely with git-filter-repo
Avoid git filter-branch — it is deprecated and error-prone. Use git-filter-repo (installed via pip or package manager) for reliable history rewriting:
# Install git-filter-repo
pip install git-filter-repo
# Remove specific secret value from entire history
git filter-repo --replace-text expressions.txt --force
# expressions.txt format (one per line):
# literal:sk_live_abc123xyz==>REDACTED_SECRET
# After rewrite, force-push all affected branches
git push --force --all
git push --force --tags Coordinate force-pushes with your team. Every contributor must re-clone or reset their local branches. Document the incident timestamp and affected commit ranges so future bisect operations account for the discontinuity. On shared infrastructure like the Deployer 7 + GitLab CI setup I use for sister sites, schedule rewrites during low-traffic windows to minimize deployment disruption.
Post-remediation verification checklist
- Run
gitleaks detect --source .on the rewritten repository — zero findings expected - Search raw Git objects:
git grep -n "old_secret_value" $(git rev-list --all) - Verify rotated credentials work in all environments (dev, staging, production)
- Check third-party provider audit logs for unauthorized usage during exposure window
- Update CI/CD secret stores (GitLab Variables, Vault, AWS Secrets Manager)
- Document incident in security log with timeline, impact assessment, and preventive measures added
Conclusion
Implementing secrets scanning in Git and CI with gitleaks transforms credential security from reactive incident response to proactive prevention. Start with local pre-commit hooks for immediate developer feedback, enforce mandatory CI gates for authoritative protection, extend default rules for your specific technology stack, and establish clear remediation procedures before your first real leak occurs. The configuration patterns in this guide reflect battle-tested setups from production Laravel applications and legal-tech platforms serving real clients.
If your team needs help integrating secrets scanning into existing CI/CD pipelines, configuring custom rules for PHP/Laravel projects, or remediating historical leaks safely, reach out to discuss your specific requirements. Proper secrets management is foundational infrastructure, not optional compliance theater.

