Signed Commits (GPG/SSH)
Complete guide to Git commit signing with GPG and SSH keys. Setup, verification, trust chains, and why signed commits matter for supply chain security.
Introduction
Every commit in Git records an author name and email — but nothing proves that the person who made the commit actually owns that identity. Anyone can commit as “Linus Torvalds torvalds@linux-foundation.org” in their local repository. The commit hash verifies content integrity, but not authorship.
Signed commits solve this problem. By cryptographically signing each commit with a GPG or SSH key, you create a verifiable chain of trust from your identity to every change in the repository. GitHub, GitLab, and other platforms display “Verified” badges on signed commits, giving you and your team confidence in the provenance of every line of code.
This guide covers both GPG and SSH signing methods, step-by-step setup for every major platform, and the operational practices that make signed commits a practical part of your workflow.
When to Use / When Not to Use
When to sign commits:
- Open source projects where identity matters
- Enterprise repositories with compliance requirements
- Supply chain security for critical software
- Regulated industries (finance, healthcare, government)
- Any project where you need non-repudiation
When signing may be optional:
- Personal projects with single contributors
- Internal tools with trusted teams
- Rapid prototyping where overhead outweighs benefit
Core Concepts
Git supports two signing methods:
graph TD
COMMIT["Commit Object"] -->|signs with| KEY["Cryptographic Key"]
KEY --> GPG["GPG/PGP Key\n(traditional)"]
KEY --> SSH["SSH Key\n(modern, simpler)"]
GPG --> SIG1["GPG Signature\nin commit header"]
SSH --> SIG2["SSH Signature\nin commit header"]
SIG1 --> VERIFY1["Verify with GPG\npublic key"]
SIG2 --> VERIFY2["Verify with SSH\npublic key"]
VERIFY1 --> TRUST1["Web of Trust\nor explicit trust"]
VERIFY2 --> ALLOW["Allowed Signers File\nor platform trust"]
Both methods embed a cryptographic signature in the commit object. The signature covers the commit content (tree, parent, author, committer, message), so any tampering invalidates it.
Architecture or Flow Diagram
flowchart LR
DEV["Developer"] -->|creates| COMMIT["Commit Object"]
COMMIT -->|signs with| KEY["Private Key\n(GPG or SSH)"]
KEY -->|produces| SIG["Signature Block\nembedded in commit"]
SIG -->|pushed to| PLATFORM["GitHub/GitLab"]
PLATFORM -->|verifies with| PUBKEY["Public Key\nregistered on platform"]
PUBKEY -->|result| BADGE["Verified Badge\nor Warning"]
AUDITOR["Auditor"] -->|runs| VERIFY["git verify-commit"]
VERIFY -->|checks| LOCALKEY["Local Keyring\nor allowed signers"]
Step-by-Step Guide / Deep Dive
GPG Signing Setup
1. Generate a GPG key:
# Generate a new GPG key (interactive)
gpg --full-generate-key
# Or non-interactive for scripting
gpg --batch --gen-key << EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: Your Name
Name-Email: your.email@example.com
Expire-Date: 0
%commit
EOF
2. Configure Git to use it:
# List your GPG keys
gpg --list-secret-keys --keyid-format=long
# Configure Git (use the key ID from above)
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true
# Optional: sign tags too
git config --global tag.gpgsign true
3. Add your public key to GitHub/GitLab:
# Export your public key
gpg --armor --export ABC123DEF456
# Copy the output and add it to:
# GitHub: Settings → SSH and GPG keys → New GPG key
# GitLab: Settings → GPG Keys
SSH Signing Setup (Git 2.34+)
SSH signing is simpler since most developers already have SSH keys:
# Configure Git to use SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# Create an allowed signers file
cat > ~/.gitallowed << EOF
your.email@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
EOF
# Tell Git where to find it
git config --global gpg.ssh.allowedSignersFile ~/.gitallowed
Signing Existing Commits
# Re-sign the last commit
git commit --amend --no-edit -S
# Re-sign multiple commits (interactive rebase)
git rebase -i HEAD~5
# Mark commits with 'edit', then:
git commit --amend --no-edit -S
git rebase --continue
# Sign all commits on a branch
git rebase main --exec 'git commit --amend --no-edit -S'
Verification
# Verify the last commit
git log --show-signature -1
# Verify a specific commit
git verify-commit <sha>
# Show signature status in log
git log --format="%H %G? %GS" -10
# %G? = signature status (G=good, B=bad, U=untrusted, N=no signature)
# %GS = signer information
Production Failure Scenarios + Mitigations
| Scenario | Symptoms | Mitigation |
|---|---|---|
| Expired GPG key | ”error: gpg failed to sign the data” | Extend key expiry: gpg --edit-key <id> expire |
| Missing key on platform | Commits show “Unverified” badge | Upload public key to GitHub/GitLab |
| SSH signing not supported | ”error: unsupported value for gpg.format” | Upgrade Git to 2.34+ |
| Key rotation | Old commits show as unverified | Keep old public keys; upload new ones |
| CI/CD signing | Automated commits unsigned | Use bot keys or disable signing for CI |
Trade-offs
| Aspect | GPG | SSH |
|---|---|---|
| Setup complexity | Higher (key generation, keyring) | Lower (reuse existing SSH key) |
| Platform support | Universal (GitHub, GitLab, Bitbucket) | Git 2.34+, GitHub, GitLab 15.0+ |
| Key management | GPG keyring, web of trust | SSH keys, allowed signers file |
| User familiarity | Less common among developers | Widely understood |
| Expiry handling | Built-in expiry dates | No expiry (manage manually) |
Implementation Snippets
# Enable signing for all commits
git config --global commit.gpgsign true
# Sign a single commit
git commit -S -m "Signed commit"
# Sign a tag
git tag -s v1.0 -m "Release 1.0"
# Verify all commits in a range
git log --show-signature v1.0..v2.0
# Check if commits are signed
git log --format="%H %G?" main
# Configure GUI tools to sign
git config --global gpg.program $(which gpg)
# Use specific GPG home directory
git config --global gpg.minTrustLevel ultimate
Observability Checklist
- Monitor: Percentage of signed commits in repository
- Verify: Platform shows “Verified” badge on recent commits
- Track: Key expiry dates (set calendar reminders)
- Audit: Run
git log --show-signatureon release branches - Alert: Unsigned commits on protected branches
Security/Compliance Notes
- Signed commits provide non-repudiation — the signer cannot deny authorship
- GPG keys should be stored securely (consider hardware tokens like YubiKey)
- SSH keys for signing should be separate from authentication keys
- Rotate keys periodically and update platforms
- See Git Secrets Management for comprehensive security
Common Pitfalls / Anti-Patterns
- Signing with expired keys — platforms reject them
- Not backing up GPG keys — losing your key means losing your identity
- Using weak algorithms — prefer RSA 4096 or Ed25519
- Signing in CI/CD without key management — exposes private keys
- Assuming signed = reviewed — signing proves identity, not code quality
Quick Recap Checklist
- GPG signing requires key generation and platform registration
- SSH signing (Git 2.34+) reuses existing SSH keys
- Configure
commit.gpgsign truefor automatic signing - Verify with
git log --show-signature - Platforms display “Verified” badges for recognized keys
- Keep keys secure and backed up
- Rotate keys before expiry
Interview Q&A
A signed commit proves authorship (the holder of the private key created it) and integrity (the content hasn't been modified since signing). It does NOT prove code quality, review status, or that the author name/email match the key owner — those are separate concerns.
SSH signing is simpler to set up — most developers already have SSH keys for authentication. It avoids GPG's complexity (keyring management, web of trust). However, GPG has broader platform support and built-in key expiry. Choose SSH for simplicity, GPG for compatibility.
Yes, but it rewrites history. Use git rebase -i with exec git commit --amend --no-edit -S to sign existing commits. This changes commit hashes, so you'll need to force-push. For shared branches, coordinate with your team to avoid conflicts.
GitHub stores your public GPG key when you add it in settings. When you push signed commits, GitHub extracts the signature from the commit object and verifies it against your stored public key. If it matches, GitHub displays a "Verified" badge. If the key isn't registered, the commit shows "Unverified."
Commit Signing Flow (Clean Architecture)
graph TD
DEV["Developer"] -->|1. writes| COMMIT["Commit Object"]
DEV -->|2. signs with| PRIVKEY["Private Key\nGPG or SSH"]
PRIVKEY -->|3. produces| SIG["Signature\nembedded in commit"]
SIG -->|4. pushed to| PLATFORM["GitHub / GitLab"]
PLATFORM -->|5. verifies with| PUBKEY["Public Key\nregistered on platform"]
PUBKEY -->|6. result| BADGE["Verified Badge"]
VERIFY["Local Verification"] -->|git verify-commit| RESULT["Good / Bad / Untrusted"]
RESULT -->|checks against| KEYRING["Local Keyring\nor allowed signers"]
Production Failure: Key Lifecycle Issues
Scenario: Expired GPG key breaking CI and verification
# Symptoms
$ git commit -S -m "Add feature"
error: gpg failed to sign the data
fatal: failed to write commit object
$ git log --show-signature -1
# Shows "Expired key" warning
# Root cause: GPG key has an expiry date that has passed
# Recovery steps:
# 1. Check key expiry
gpg --list-keys --keyid-format long
# Look for [expires: 2024-01-01]
# 2. Extend key expiry
gpg --edit-key YOUR_KEY_ID
> expire
# Set new expiry date (e.g., 2y for 2 years)
> save
# 3. Re-export and update platform
gpg --armor --export YOUR_KEY_ID
# Update on GitHub/GitLab with new public key
# 4. Re-sign affected commits (if any failed)
git commit --amend --no-edit -S
# === Key Revocation Scenario ===
# If key is compromised:
# 1. Generate revocation certificate (do this when creating key!)
gpg --gen-revoke YOUR_KEY_ID > revocation.crt
# 2. Import revocation
gpg --import revocation.crt
# 3. Upload revoked key to keyserver
gpg --keyserver keys.openpgp.org --send-keys YOUR_KEY_ID
# 4. Generate new key and update all platforms
# 5. Old signed commits will show "revoked" status
Trade-offs: GPG vs SSH vs X.509 Signing
| Aspect | GPG | SSH | X.509 |
|---|---|---|---|
| Setup complexity | High (keyring, web of trust) | Low (reuse existing SSH key) | Very high (certificate authority) |
| Git version required | Any | 2.34+ | 2.34+ |
| Platform support | Universal (GitHub, GitLab, Bitbucket) | GitHub, GitLab 15.0+ | Limited (enterprise GitLab) |
| Key management | GPG keyring, subkeys | SSH keys, allowed_signers file | PKI, certificate lifecycle |
| Expiry handling | Built-in expiry dates | No expiry (manual management) | Certificate validity period |
| Hardware support | YubiKey, smart cards | YubiKey, SSH agent | Smart cards, HSM |
| User familiarity | Low (most devs don’t know GPG) | High (every dev has SSH keys) | Low (enterprise IT only) |
| Best for | Open source, maximum compatibility | Modern teams, simplicity | Enterprise, compliance |
Security/Compliance: Key Management Best Practices
Key Generation:
- Use Ed25519 for SSH (or RSA 4096 minimum)
- Use RSA 4096 or Ed25519 for GPG
- Never use DSA (deprecated and weak)
Key Storage:
- Store private keys on hardware tokens (YubiKey) when possible
- Never store private keys in cloud sync (Dropbox, iCloud)
- Use separate keys for signing vs authentication
Key Rotation:
- Rotate GPG keys every 1-2 years
- Rotate SSH signing keys every 6-12 months
- Keep old public keys registered on platforms (old commits still need verification)
Backup and Recovery:
- Export and encrypt GPG secret key:
gpg --export-secret-keys --armor > backup.asc - Store backup in encrypted offline storage (not cloud)
- Generate revocation certificate immediately after key creation
- Document key IDs and expiry dates in team wiki
Compliance Notes:
- Signed commits satisfy code provenance requirements in SOC2, ISO 27001
- For SBOM and supply chain compliance, combine with SLSA framework
- Key signing ceremonies may be required for regulated environments
- Maintain an audit trail of key rotations and revocations
Resources
Category
Related Posts
Git Secrets Management and Pre-commit Hooks
Preventing secrets from entering repositories using pre-commit hooks, secret scanning tools, and automated detection. Protect API keys, tokens, and credentials from accidental commits.
Removing Sensitive Data from Git History
Using git filter-repo, BFG Repo-Cleaner, and git filter-branch to scrub secrets, passwords, and credentials from Git history. Step-by-step remediation guide.
Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each
Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.