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.
$ 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:
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:
$ 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
$ 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
$ 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
$ 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
mainanddevelop. - Environment protection: Restrict who can deploy to production environments and require approvals.
$ 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
$ 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:
# 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
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:
$ 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.
$ 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
$ 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
# 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
- 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
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 |
Frequently Asked Questions
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.
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.
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.
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.
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.