CloudOpsGuide
devsecops

Git Security Best Practices for DevOps Engineers

Intermediate
13 minutes
2026-02-15
CloudOpsGuide Team

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

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:

  1. Content security — what goes into the repository (secrets, sensitive data)
  2. Access security — who can read and write the repository
  3. 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)
  • .env files 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:

RuleWhy
Require pull request reviewsNo unreviewed code reaches production
Require status checksCI must pass before merge
Require signed commitsVerify author identity
Restrict force pushesPrevent history rewriting
Require linear historyOptional — keeps history clean
Include administratorsRules 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:

  • .gitignore covers 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.example pattern instead of real .env files
  • Fine-grained tokens with expiry — no broad PATs

Related Articles