Git Security Best Practices for DevOps Engineers
Git Security Best Practices for DevOps Engineers
Protect your repositories from leaked secrets, unauthorised access, and supply-chain attacks with practical Git security controls.
Difficulty: Intermediate Estimated Reading Time: 13 minutes Last Updated: 2026-02-15
Table of Contents
- Why Git Security Matters
- Secrets in Repositories
- Scanning for Secrets
- Removing Secrets from History
- Access Control
- Signed Commits
- Branch Protection
- Dependency Security
- Pre-Commit Hooks
- Checklist
Why Git Security Matters
Your Git repository is a full history of every line ever committed — including that API key someone committed "temporarily" six months ago. Deleting the file doesn't remove the secret; it stays in history forever until explicitly purged.
Public breaches caused by exposed Git secrets are common: cloud credentials, database passwords, private keys, and OAuth tokens discovered in public repos lead to real account takeovers.
Git security covers three areas:
- Content security — what goes into the repository (secrets, sensitive data)
- Access security — who can read and write the repository
- Integrity — proving commits came from who they claim to
Secrets in Repositories
What counts as a secret
- Cloud credentials (
AWS_ACCESS_KEY_ID, Azure service principal secrets) - Private keys (
-----BEGIN RSA PRIVATE KEY-----) - Database connection strings with passwords
- API tokens (GitHub PATs, Slack tokens, npm tokens)
.envfiles with real values- Kubernetes Secrets decoded from committed YAML
What NOT to do
# Don't do this — even if you delete it in the next commit
echo "DATABASE_PASSWORD=supersecret123" >> .env
git add .env && git commit -m "temp config"
The password is now in Git history permanently.
The right way
Keep secrets out of the repo entirely:
# .gitignore — always include these
.env
.env.local
*.pem
*.key
secrets/
.terraform/
*.tfvars
Provide a template instead:
# .env.example — committed to the repo
DATABASE_PASSWORD=<your-password-here>
API_KEY=<your-api-key>
Scanning for Secrets
Scan history — not just the working tree — because secrets hide in old commits.
Gitleaks (recommended)
# Install
brew install gitleaks # macOS
# or: go install github.com/gitleaks/gitleaks/v8@latest
# Scan entire history
gitleaks detect --source . --verbose
# Scan only staged changes (pre-commit)
gitleaks protect --staged
Gitleaks outputs the file, line number, and rule that matched for every finding.
TruffleHog
# Scans history AND verifies whether found credentials are still active
trufflehog git file://. --only-verified
--only-verified is powerful — it attempts the credential against the real service and only reports live ones. That's your priority list for rotation.
GitHub built-in scanning
GitHub scans public repos automatically and blocks pushes containing recognised secrets (push protection). Enable it under Settings → Code security → Secret scanning. It covers common providers but use Gitleaks in CI for broader coverage.
Removing Secrets from History
If a secret was committed:
Step 1: Rotate the credential immediately
The secret is compromised the moment it's pushed. Rotate first, clean history second — assume it was already scraped.
Step 2: Purge the file from history
# Using git filter-repo (recommended over filter-branch)
pip install git-filter-repo
# Remove a file from ALL history
git filter-repo --path .env --invert-paths
# Remove a specific secret string from all files
git filter-repo --replace-text <(echo "supersecret123==>REMOVED")
This rewrites history — every commit hash changes. All collaborators must re-clone.
Step 3: Force-push and invalidate caches
git push --force --all
git push --force --tags
On GitHub, also contact support to clear cached views of the old commits — they persist briefly even after force-push.
Access Control
SSH keys over HTTPS passwords
# Generate a modern key
ssh-keygen -t ed25519 -C "your.email@example.com"
# Add to GitHub/GitLab — Settings → SSH Keys
cat ~/.ssh/id_ed25519.pub
Enforce least privilege
- Nobody pushes directly to
main— pull requests only - Use deploy keys (read-only) for servers that clone code
- Remove access promptly when people leave teams
- Prefer fine-grained personal access tokens over broad-scope PATs
Audit who has access
# GitHub: list collaborators via CLI
gh api repos/{owner}/{repo}/collaborators
# Check for unexpected deploy keys
gh api repos/{owner}/{repo}/keys
Signed Commits
Commit signing proves a commit actually came from you — important when git config user.email can be set to anything.
# Generate a GPG key
gpg --full-generate-key
# Find the key ID
gpg --list-secret-keys --keyid-format=long
# Configure Git to sign automatically
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
Upload the public key to GitHub (Settings → SSH and GPG keys) and commits show a "Verified" badge.
Simpler alternative: GitHub supports SSH signing — reuse your existing SSH key instead of managing GPG:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
Branch Protection
On the hosting platform (GitHub/GitLab/Azure DevOps), configure for main:
| Rule | Why |
|---|---|
| Require pull request reviews | No unreviewed code reaches production |
| Require status checks | CI must pass before merge |
| Require signed commits | Verify author identity |
| Restrict force pushes | Prevent history rewriting |
| Require linear history | Optional — keeps history clean |
| Include administrators | Rules apply to everyone |
GitHub example:
gh api repos/{owner}/{repo}/branches/main/protection \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["build"]}' \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":1}'
Dependency Security
Your package-lock.json, go.sum, or requirements.txt pins what actually ships. Keep them under control:
# Audit Node dependencies
npm audit
# Check for known vulnerabilities in Python
pip audit
# Go
govulncheck ./...
Enable Dependabot (GitHub) or Renovate (any platform) to open PRs when dependencies have vulnerabilities — automated dependency maintenance is the most effective single control for supply-chain security.
The .gitignore for security
# Secrets
.env*
*.pem
*.key
*.pfx
credentials.json
service-account*.json
secrets/
# Terraform state can contain secrets
*.tfstate
*.tfstate.*
*.tfvars
# IDE files that may contain local config
.idea/
.vscode/settings.json
*.code-workspace
Pre-Commit Hooks
Catch secrets before they ever reach the remote:
# Install pre-commit framework
pip install pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: detect-private-key
- id: end-of-file-fixer
- id: trailing-whitespace
pre-commit install
Now git commit runs the checks locally — a secret is caught before it exists anywhere in history.
Checklist
Repository security baseline:
-
.gitignorecovers secrets, keys, state files, IDE configs - Secret scanning enabled (Gitleaks in CI + platform push protection)
- Secrets rotated and history purged for any past leaks
- Branch protection on
main— PRs, reviews, status checks - Commit signing enabled for maintainers
- Dependency scanning (Dependabot/Renovate) active
- Access reviewed — no stale collaborators or deploy keys
- Pre-commit hooks installed by the team
-
.env.examplepattern instead of real.envfiles - Fine-grained tokens with expiry — no broad PATs