Git Security Best Practices: Protect Your Code and Pipeline

Why Git Security Matters

Git repositories contain your organization's crown jewels: source code, infrastructure configurations, API keys, and intellectual property. A single security lapse can expose sensitive data, compromise your supply chain, or allow unauthorized code injection. This comprehensive guide covers every aspect of Git security, from basic authentication to advanced supply chain protection.

Core principle: Git security is multi-layered. It spans authentication (who you are), authorization (what you can do), integrity (proving code hasn't been tampered with), and secrets management (keeping credentials out of repos). Each layer requires specific tools and practices.

1. SSH Key Management for Git

SSH keys are the most common authentication method for Git operations. Unlike passwords, SSH keys provide cryptographic proof of identity and can be individually revoked. Proper SSH key management is foundational to Git security.

Generating Strong SSH Keys

The first step in securing Git access is creating strong SSH keys. Modern best practices recommend Ed25519 keys for their security and performance. RSA keys with at least 4096 bits are also acceptable but slower.

# Generate Ed25519 key (recommended)
$ ssh-keygen -t ed25519 -C "your.email@example.com" -f ~/.ssh/git_key

# Or generate RSA 4096-bit key
$ ssh-keygen -t rsa -b 4096 -C "your.email@example.com" -f ~/.ssh/git_key

# Add key to ssh-agent
$ eval "$(ssh-agent -s)"
$ ssh-add ~/.ssh/git_key

Explanation: The -t flag specifies the key type, -b sets the bit length for RSA, and -C adds a comment (usually your email) for identification. The -f flag specifies the output file. Adding to ssh-agent prevents repeated passphrase prompts.

SSH Key Configuration Best Practices

Configure SSH to use specific keys for specific hosts, preventing key exposure and enabling fine-grained control:

# ~/.ssh/config
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/github_key
  IdentitiesOnly yes

Host gitlab.company.com
  HostName gitlab.company.com
  User git
  IdentityFile ~/.ssh/company_key
  Port 2222

Why this matters: The IdentitiesOnly yes directive ensures SSH only uses the explicitly specified key, not any keys loaded in ssh-agent. This prevents accidental authentication with the wrong key and provides better control over which keys are used where.

SSH Key Rotation and Revocation

Keys should be rotated regularly and immediately revoked when compromised:

# List keys loaded in agent
$ ssh-add -l

# Remove all keys from agent
$ ssh-add -D

# Generate new key pair (rotation)
$ ssh-keygen -t ed25519 -f ~/.ssh/new_key
$ ssh-add ~/.ssh/new_key

# Then remove old key from GitHub/GitLab UI and add new one

2. Commit Signing with GPG

Commit signing verifies that commits genuinely come from you, not an impostor. While SSH keys authenticate your connection to the remote, GPG (GNU Privacy Guard) signatures authenticate the commits themselves. This creates an immutable chain of custody.

Why Commit Signing Matters

Without signing, anyone with write access to your repository can create commits attributed to you. Attackers who compromise a developer's account can inject malicious code that appears to come from trusted team members. Signed commits provide cryptographic proof of authorship and integrity, ensuring that code hasn't been tampered with since it was signed.

Setting Up GPG Keys

# Install GPG tools
$ brew install gnupg # macOS
$ sudo apt install gnupg # Ubuntu/Debian

# Generate GPG key (interactive)
$ gpg --full-generate-key

# List your GPG keys
$ gpg --list-secret-keys --keyid-format LONG

# Export public key for GitHub/GitLab
$ gpg --armor --export [KEY_ID] | pbcopy # macOS copy to clipboard

Key generation explained: When generating a GPG key, you'll select the key type (RSA is most compatible), key size (4096 bits recommended), expiration date (set expiration for better security), and personal details. The email must match your Git commits' email address.

Configuring Git to Sign Commits

# Get your GPG key ID
$ gpg --list-secret-keys --keyid-format LONG
# Output: sec rsa4096/[KEY_ID] 2024-01-01 [SC]

# Configure Git to use your key
$ git config --global user.signingkey [KEY_ID]
$ git config --global commit.gpgSign true
$ git config --global tag.gpgSign true

# Sign commits automatically
$ git commit -S -m "Signed commit"
# Or with -S flag omitted (if commit.gpgSign=true)
$ git commit -m "Automatically signed"

Verifying Signed Commits

# Check if commits are signed
$ git log --show-signature -1

# Require signatures in repository (GitHub/GitLab settings)
# Enable "Require signed commits" in branch protection rules

# Verify a tag signature
$ git tag -v v1.0.0

⚠️ Critical: GPG keys expire. When they do, all commits signed after expiration will show as invalid. Set up key rotation reminders and update your key in Git hosting platforms before expiration. Consider using a hardware security key (YubiKey) for storing GPG keys in enterprise environments.

3. Git Hosting Platform Security

GitHub, GitLab, Bitbucket, and self-hosted solutions each have security features that must be properly configured. Understanding these settings is crucial for protecting your repositories.

Access Control Models

Modern Git platforms implement granular access control through teams, roles, and permissions:

  • Repository-level permissions: Read, triage, write, maintain, admin (GitHub). Each level grants progressively more capabilities.
  • Branch protection rules: Prevent force pushes, require pull request reviews, require status checks, and enforce signed commits on critical branches like main and develop.
  • Environment protection: Restrict who can deploy to production environments and require approvals.
# GitHub CLI command to set branch protection
$ gh api repos/:owner/:repo/branches/main/protection \
  --method PUT \
  --field required_status_checks='{"strict":true,"contexts":["continuous-integration"]}' \
  --field enforce_admins=true \
  --field required_pull_request_reviews='{"required_approving_review_count":2}' \
  --field restrictions=null

Organization Security Settings

Enterprise Git hosting offers organization-wide security controls:

  • SAML/SSO integration: Enforce corporate authentication and automatic user provisioning/deprovisioning.
  • IP allow lists: Restrict access to trusted networks only.
  • Audit logging: Track all actions for compliance and incident investigation.
  • Secret scanning: Automatically detect and block commits containing known secret patterns (API keys, tokens).

4. Secrets Management and Prevention

Accidentally committing secrets is the most common Git security incident. Once a secret is pushed, it's in the history forever unless completely purged. Prevention is far better than remediation.

The Secrets Leak Problem

When a developer accidentally commits an API key, AWS secret, or database password, that credential is compromised. Even if removed in a later commit, it remains in the Git history. Automated bots scan public repositories for such secrets within minutes. This has led to countless data breaches and cloud account takeovers.

Pre-commit Hooks for Secret Detection

# Install git-secrets (AWS Labs)
$ brew install git-secrets # macOS
$ sudo apt install git-secrets # Ubuntu

# Set up git-secrets in your repository
$ cd your-repo
$ git secrets --install
$ git secrets --register-aws # Add AWS patterns

# Add custom patterns
$ git secrets --add 'password\s*=\s*.+'
$ git secrets --add 'api[_-]key\s*=\s*.+'

# Scan entire history
$ git secrets --scan-history

Using .gitignore Effectively

Proper .gitignore configuration prevents accidental commits of sensitive files:

# Example .gitignore entries for secrets
# Environment and secrets files
.env
.env.*
*.pem
*.key
secrets.yml
config/credentials.yml.enc

# IDE and local configs
.vscode/
.idea/
*.local

# Logs and temp files
*.log
tmp/

Secrets Scanning in CI/CD

# GitHub Actions secret scanning
name: Secret Scan
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: GitLeaks Scan
        uses: zricethezav/gitleaks-action@v2

Removing Secrets from Git History

If a secret is committed, immediate action is required:

# Option 1: BFG Repo-Cleaner (faster for large repos)
$ bfg --delete-files .env your-repo.git
$ git reflog expire --expire=now --all && git gc --aggressive --prune=now
$ git push --force

# Option 2: git filter-repo (recommended)
$ git filter-repo --path .env --invert-paths

# CRITICAL: After force pushing, rotate ALL exposed secrets
# The secret was public in the history - assume compromised

⚠️ CRITICAL: After removing a secret from Git history, you must rotate (change) that secret immediately. Once a secret has been committed, even if removed, it should be considered compromised. Bots scan public repositories constantly and will capture secrets within minutes of being pushed.

5. Signed Tags and Releases

Tags mark important points in history, especially releases. Signed tags provide cryptographic proof that a release truly came from an authorized maintainer, preventing supply chain attacks where attackers could inject malicious code into a release tag.

# Create a signed tag
$ git tag -s v1.2.3 -m "Release version 1.2.3"

# Verify a signed tag
$ git tag -v v1.2.3

# Push tags (signed tags include signature)
$ git push origin v1.2.3

# Configure Git to always sign tags
$ git config --global tag.gpgSign true

Why signed tags matter: When users clone your repository and check out a signed tag, they can verify that the tag was created by you and hasn't been altered. This is crucial for software distribution—users can trust that the code they're running is authentic.

6. Repository Integrity and Monitoring

Continuous monitoring helps detect suspicious activity and maintain repository integrity.

Regular Audits with git fsck

# Check repository integrity
$ git fsck --full

# Check for dangling objects (potential data loss)
$ git fsck --unreachable

# Automatically run fsck on fetch/push
$ git config --global transfer.fsckObjects true

Audit Logs and Monitoring

Modern Git platforms provide comprehensive audit logging:

  • GitHub Audit Log: Track all organization events: repository access, team changes, security settings modifications.
  • GitLab Audit Events: Similar comprehensive logging with export capabilities.
  • Self-hosted solutions: Ship logs to SIEM systems (Splunk, ELK) for analysis and alerting.

Alerting on Suspicious Patterns

# Example: Detect force pushes to protected branches
# GitHub webhook payload monitoring
{
  "event": "push",
  "forced": true,
  "ref": "refs/heads/main",
  "sender": {"login": "user"}
}

7. CI/CD Pipeline Security

CI/CD pipelines often have elevated privileges and access to production. Securing them is critical.

Protected Secrets in CI

# GitHub Actions: Never echo secrets
- name: Deploy
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  run: |
    # This will mask the secret in logs
    echo "::add-mask::$AWS_ACCESS_KEY_ID"
    ./deploy.sh

Minimal CI Permissions

# GitHub Actions: Restrict token permissions
permissions:
  contents: read
  packages: write
  issues: none

8. Security Checklist Summary

Category Action Priority
Authentication Use SSH keys with passphrases, rotate annually High
Commit Integrity Enable GPG signing for all commits High
Branch Protection Protect main branch: require reviews, signed commits High
Secrets Prevention Install pre-commit hooks, use .gitignore High
Secrets Scanning Enable GitHub/GitLab secret scanning High
Access Control Review team permissions quarterly Medium
Audit Logging Export and monitor audit logs Medium
CI/CD Security Use OIDC for cloud authentication Medium
Release Integrity Sign all release tags Medium
What cryptographic method provides proof that a specific commit was created by you and not tampered with?
  • GPG commit signing
  • SSH key authentication
  • HTTPS password
  • Git hooks

Frequently Asked Questions

What's the difference between SSH and GPG for Git?

SSH authenticates your connection to the Git server—it proves you are who you claim to be when pushing or pulling. GPG signs commits and tags—it proves that a specific commit was created by you and hasn't been modified. SSH is for transport security; GPG is for content integrity. You need both for complete security: SSH to access the repository, GPG to verify authorship.

How often should I rotate SSH and GPG keys?

For most organizations, annual rotation is sufficient for SSH keys, provided they're protected with strong passphrases. GPG keys typically have expiration dates set during creation—common practice is 1-2 years. Set calendar reminders 30 days before expiration to generate new keys and update them on GitHub/GitLab. For high-security environments or teams with frequent turnover, consider 6-month rotation cycles.

Can I sign commits after pushing?

No, commits are immutable once created. If you pushed unsigned commits, you need to rebase to sign them, which creates new commit hashes. Use git rebase --exec 'git commit --amend --no-edit -S' main to sign all commits since main, then force push. This rewrites history, so coordinate with your team. The better approach is to configure Git to sign all commits automatically from the start.

What should I do if I accidentally commit a secret?

1. Immediately revoke/rotate the exposed secret—it's compromised. 2. Remove the secret from Git history using BFG or git filter-repo. 3. Force push the cleaned history. 4. Notify your security team and affected services. 5. Review access logs for any unauthorized use of the secret. 6. Implement pre-commit hooks to prevent recurrence. Time is critical—bots scan for secrets within minutes of being pushed.

How do I enforce signed commits for my team?

In GitHub/GitLab, enable branch protection rules for your main branch and check "Require signed commits." This blocks any unsigned commits from being merged. For complete enforcement, use server-side hooks or CI pipelines that verify signatures. Educate your team on setting up GPG keys and configure Git to sign commits automatically with git config --global commit.gpgSign true.

Previous: Git Aliases and Configuration Next: Advanced Rebase