Skip to content

Git Security

Git's cryptographic design provides integrity (every object is hashed), but it doesn't provide authentication or secrecy on its own. This guide covers signing commits to prove authorship, managing credentials safely, detecting and removing secrets from repositories, and securing your Git workflow end to end.


Why Sign Commits?

Anyone can set user.name and user.email to any value. Without signing, there's no proof that a commit was actually written by the person it claims. Signed commits use cryptographic signatures to prove that the committer holds a specific private key, and platforms like GitHub and GitLab display a "Verified" badge.

Two signing methods are available:

Method Key type Git version Platform support
GPG signing GPG key pair Any GitHub, GitLab, Bitbucket
SSH signing SSH key pair 2.34+ GitHub, GitLab

GPG Signing

Setting Up GPG

# Generate a GPG key (choose RSA 4096 or Ed25519)
gpg --full-generate-key

# List your keys
gpg --list-secret-keys --keyid-format=long

# Output:
# sec   ed25519/ABC123DEF456 2024-01-15 [SC]
#       ABCDEF1234567890ABCDEF1234567890ABC123DE
# uid           [ultimate] Jane Developer <jane@example.com>

The key ID after the algorithm (ABC123DEF456) is what you configure Git with:

# Tell Git which key to use
git config --global user.signingkey ABC123DEF456

# Sign all commits by default
git config --global commit.gpgsign true

# Sign all tags by default
git config --global tag.gpgsign true

Adding Your Key to a Platform

Export the public key and paste it into your platform's settings:

# Export public key
gpg --armor --export ABC123DEF456

Copy the output (starting with -----BEGIN PGP PUBLIC KEY BLOCK-----) to: - GitHub: Settings > SSH and GPG keys > New GPG key - GitLab: User Settings > GPG Keys

Making Signed Commits

# Sign a single commit
git commit -S -m "Add signed authentication module"

# With commit.gpgsign = true, all commits are signed automatically
git commit -m "This is signed automatically"

# Create a signed tag
git tag -s v1.0 -m "Signed release 1.0"

SSH Signing (Git 2.34+)

SSH signing uses your existing SSH key - no GPG required. This is simpler if you already have SSH keys for authentication.

# Configure SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub

# Sign all commits
git config --global commit.gpgsign true

Allowed Signers File

For verifying SSH signatures locally, Git needs a list of trusted keys:

# Create an allowed signers file
echo "jane@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8r..." > ~/.config/git/allowed_signers

# Tell Git about it
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers

Verifying Signatures

# Verify a commit
git verify-commit HEAD

# Verify a tag
git verify-tag v1.0

# Show signatures in log
git log --show-signature

# Show signatures in one-line log
git log --oneline --format='%h %G? %s'
# %G? shows: G (good), B (bad), U (untrusted), N (no signature), E (expired)

Credential Management

Storing credentials safely is essential. Typing passwords or tokens for every push is impractical, but hardcoding them is dangerous.

Credential Helpers

Git's credential helpers cache or store authentication tokens:

# Cache in memory (default 15 minutes)
git config --global credential.helper cache
git config --global credential.helper 'cache --timeout=3600'  # 1 hour

# macOS Keychain
git config --global credential.helper osxkeychain

# Windows Credential Manager
git config --global credential.helper manager

# Linux libsecret (GNOME Keyring)
git config --global credential.helper /usr/lib/git-core/git-credential-libsecret

Personal Access Tokens

All major platforms require tokens (not passwords) for HTTPS Git operations:

  • GitHub: Settings > Developer Settings > Personal access tokens > Tokens (classic) or Fine-grained tokens
  • GitLab: User Settings > Access Tokens
  • Bitbucket: Personal Settings > App passwords

Use the token as your password when Git prompts. The credential helper caches it.

Never commit tokens

Tokens in your repository history are permanent (even after removal, they exist in old commits). Platforms scan for accidentally committed tokens and may revoke them, but the exposure window can be enough for damage.


Secret Scanning

Accidentally committing secrets (API keys, passwords, tokens, private keys) is one of the most common security mistakes. Prevention is far easier than cleanup.

Pre-Commit Detection

Install hooks that scan for secrets before they enter the repository:

# Using the pre-commit framework
pip install pre-commit
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

Dedicated Scanning Tools

Gitleaks scans repositories for hardcoded secrets:

# Install
brew install gitleaks  # macOS

# Scan the current repo
gitleaks detect

# Scan with verbose output
gitleaks detect -v

# Scan specific commits
gitleaks detect --log-opts="HEAD~10..HEAD"

TruffleHog performs deep scanning including entropy analysis:

# Scan a repository
trufflehog git file://./

# Scan a remote repository
trufflehog github --repo https://github.com/user/repo

Platform Secret Scanning

  • GitHub: Automatic secret scanning for public repos (free) and private repos (Advanced Security license). Detects known token formats from 100+ providers.
  • GitLab: Secret Detection CI component scans MR diffs automatically.

Removing Secrets from History

When a secret has been committed, you need to rewrite history to remove it from every commit.

git filter-repo is the modern replacement for git filter-branch. It's faster, safer, and easier to use:

# Install
pip install git-filter-repo

# Remove a file from all history
git filter-repo --path secrets.env --invert-paths

# Replace specific strings in all files across all history
git filter-repo --replace-text expressions.txt

The expressions.txt file maps secrets to replacements:

literal:sk_live_abc123def456==>***REDACTED***
regex:password\s*=\s*['"].*?[']==>password = "***REDACTED***"

BFG Repo-Cleaner

BFG is a simpler tool focused on common cleaning tasks:

# Remove a file by name
bfg --delete-files .env

# Replace strings
bfg --replace-text passwords.txt

# Remove large files
bfg --strip-blobs-bigger-than 100M

After either tool, force push the cleaned history:

git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force --all
git push --force --tags

Force pushing after history rewrite

History rewriting changes every commit hash from the rewritten point onward. All collaborators must re-clone or carefully rebase their work. Coordinate with your team before force pushing. Notify everyone that the history has changed.


Security-Focused .gitignore

Prevent secrets from being committed in the first place:

# Secrets and credentials
.env
.env.*
!.env.example
*.key
*.pem
*.p12
*.pfx
credentials.json
service-account.json
**/secrets/

# SSH keys (if stored in repo for some reason)
id_rsa
id_ed25519
*.pub

# Cloud provider configs
.aws/credentials
.gcp/

Exercises


Further Reading


Previous: Git Hooks and Automation | Next: Monorepos and Scaling Git | Back to Index

Comments