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
| 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-off Analysis
| 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 Considerations
- 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
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
Interview Questions
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."
Git stores the signature in the commit's GPG header fields (for GPG) or SSH signature header (for SSH). The signature covers the commit's content: tree hash, parent hash, author timestamp, committer timestamp, and message. Any modification to these fields invalidates the signature. The signature is stored as base64-encoded data in the commit object itself, viewable with git cat-file -p <sha>.
Commit signatures verify authorship at a point in time for a specific commit. Tag signatures verify an immutable release point across a entire tree of commits. Tags are typically used for releases (v1.0.0) where you want a single verifiable point representing many changes. Configure tag signing with git config --global tag.gpgsign true.
Git will successfully create the signature, but verification will fail on platforms (GitHub/GitLab) and local verification will show "Expired key" status. The signature itself is valid at the time of signing — the expiry only affects future verification. To fix: extend the key expiry with gpg --edit-key <id>, then re-sign affected commits.
Yes. Use repository-level Git config instead of global: cd /repo && git config user.signingkey <key-id>. This allows different keys per repository. For more complex setups, use git config gpg.program to point to wrapper scripts that select keys based on directory or git configuration.
The Web of Trust is GPG's decentralized key validation system where keys can sign other keys, creating trust chains. For Git verification, platforms like GitHub use an explicit trust model — they trust keys you've uploaded directly. The web of trust matters more for email encryption where you may encounter keys from key servers. For Git signing, explicit platform registration is sufficient.
Ed25519 is the recommended SSH key type for signing. Generate with: ssh-keygen -t ed25519 -C "git-signing@example.com". Configure Git: git config --global gpg.format ssh, git config --global user.signingkey ~/.ssh/id_ed25519.pub. Create allowed signers file: echo "your@email.com ssh-ed25519 AAAA..." > ~/.gitallowed, then git config --global gpg.ssh.allowedSignersFile ~/.gitallowed.
Agent forwarding allows the local SSH agent (containing YubiKey-backed keys) to be accessed on remote machines. Security implications: remote machines can request signatures from your agent (potentially signing unexpected things), and traffic can be sniffed if agent forwarding is intercepted. Mitigation: use Confirm constraints on YubiKey to require physical button press, and limit forwarding to trusted machines only.
Platform-level branch protection (Require signed commits) ensures that any commit entering protected branches (main, release) must have a valid signature from a recognized key. This prevents: collaborators accidentally pushing unsigned commits, CI systems using untrusted keys, and attackers attempting to impersonate team members. It complements GPG setup — even if someone has a valid local GPG key, they must register it with the platform.
Historical signed commits remain verifiable with the old public key as long as the old public key is available on the platform. When rotating keys: keep old public keys registered, update your local keyring with old public keys for local verification, and note that old commits show "Unverified" only if the signing key is removed from the platform. New commits will use the new key.
A revocation certificate is a special GPG key that invalidates your main key if it's compromised. Generate it immediately after key creation, before uploading to any platform: gpg --gen-revoke YOUR_KEY_ID > revocation.crt. Store it in a secure offline location (encrypted USB). If you lose access to your key or it gets stolen, import and broadcast the revocation to key servers to notify others the key is no longer valid.
Merge commits inherit the signature of the commit being merged and sign the merge commit itself with the merging user's key. Rebased commits create new commit objects with new hashes, so the original signature is lost and must be re-signed (use git rebase -x 'git commit --amend --no-edit -S' to re-sign during rebase). Squash merges also lose individual commit signatures.
Signing has negligible performance impact for most use cases. Ed25519 signatures are ~64 bytes and compute in under 1ms on modern hardware. RSA 4096 signatures are larger (~512 bytes) and may take 10-50ms on slower hardware. The main overhead is key access (YubiKey adds ~100ms per signature due to USB communication). For typical repositories with hundreds of commits, the total signing time is not noticeable.
Yes, cloud HSMs (AWS CloudHSM, Google Cloud KMS, Azure Key Vault) can store signing keys. Setup typically involves: configuring the cloud KMS to allow the compute instance, installing the KMS SDK/tooling, and pointing Git's GPG or SSH to use the cloud KMS for signing. This provides centralized key management, audit logs, and automatic key rotation. Downsides: requires cloud credentials on build machines and introduces external dependencies.
commit.gpgsign (or commit.gpgsign true) globally enables signing for all commits. commit.signature is an older, deprecated alias. The setting user.signingkey specifies which key to use. You can override global settings per repository or per command with -S (sign) or --no-gpg-sign (don't sign a specific commit).
When you push a signed commit: (1) Git creates the commit object with the signature embedded in headers, (2) Platform receives the push and extracts the signature from commit headers, (3) Platform looks up the signer's public key from uploaded GPG/SSH keys, (4) Platform verifies the signature against the commit content using the public key, (5) If verification succeeds and the key is registered, the commit displays "Verified." Local verification uses git verify-commit <sha> which checks against your local GPG keyring or SSH allowed signers file.
X.509 certificates are preferred in enterprise environments with existing PKI (Public Key Infrastructure). Benefits: centralized key lifecycle management through Active Directory or certificate authorities, automatic enrollment and renewal through corporate PKI, integration with smart card infrastructure already deployed, and compliance with regulatory frameworks that mandate X.509 for all cryptographic operations. However, X.509 signing in Git requires Git 2.34+ and has limited platform support (primarily GitLab enterprise).
Further Reading
Additional Resources
- GitHub: Managing commit signature verification
- GitLab: Signed commits
- GnuPG: Best Practices
- SSH.com: Ed25519 for SSH
- YubiKey: GPG Guide
Topic-Specific Deep Dives
YubiKey for Git Signing
Hardware tokens provide the strongest protection for private keys. YubiKey supports both GPG and SSH signing.
GPG with YubiKey:
# Ensure GPG recognizes the YubiKey
gpg --card-status
# Generate key on the YubiKey (takes ~2 minutes)
gpg --edit-card
> generate
# Choose 4096-bit RSA, set expiry, save
# Configure Git to use the card
git config --global gpg.program $(which gpg)
# Sign commits normally - key is on the hardware token
git commit -S -m "Signed with YubiKey"
SSH with YubiKey:
# YubiKey 5+ supports SSH agent forwarding
# Add to ~/.ssh/config:
Host github.com
ForwardAgent yes
# Configure Git for SSH signing with YubiKey
git config --global gpg.format ssh
git config --global user.signingkey /path/to/yubikey.pub
Important: The private key never leaves the YubiKey. If you lose it, you cannot recover the key — this is by design. Always generate a revocation certificate when setting up hardware tokens.
CI/CD Pipeline Integration
Signing commits in automated pipelines requires careful key management to avoid exposing private keys.
GitHub Actions:
name: Signed Commits
on: [push, pull_request]
jobs:
sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git config --global user.signingkey KEY_ID
- name: Set up Git
run: |
git config --global commit.gpgsign true
- name: Make changes
run: echo "changes" >> file.txt
- name: Commit and push
run: |
git add .
git commit -m "Automated signed commit"
git push
GitLab CI:
variables:
GIT_STRATEGY: clone
before_script:
- echo "$GPG_PRIVATE_KEY" | gpg --import
- git config --global commit.gpgsign true
- git config --global user.signingkey $GPG_KEY_ID
signed-commits:
script:
- echo "changes" >> file.txt
- git add .
- git commit -m "CI signed commit"
- git push
artifacts:
paths:
- file.txt
SSH signing in CI (GitHub Actions):
- name: Configure SSH signing
run: |
echo "$SSH_SIGNING_KEY" > signing_key
echo "your@email.com ssh-ed25519 KEY" >> ~/.gitallowed
git config --global gpg.format ssh
git config --global gpg.ssh.allowedSignersFile ~/.gitallowed
git config --global user.signingkey signing_key
chmod 600 signing_key
Security considerations for CI:
- Use separate signing keys for CI (not your personal key)
- Store private keys in encrypted secrets
- Rotate CI keys more frequently than user keys
- Consider using ephemeral compute (CI runners) that don’t persist after jobs
- For high-security environments, use temporary keys per pipeline run
Continue to Git Secrets Management for protecting sensitive data in repositories.
Conclusion
Signed commits provide cryptographic proof of authorship — they tell the world “this commit was really made by me.” In open-source projects and compliance-sensitive environments, signing is the difference between trusting a name on screen and verifying an identity cryptographically.
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.
JVM Bytecode Verification: Type Checking and Stack Map Frames
A technical deep dive into the JVM bytecode verifier, covering type checking, stack map frames, the four verification stages, and what happens when verification fails.