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.

published: reading time: 19 min read author: Geek Workbench updated: March 31, 2026

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

ScenarioSymptomsMitigation
Expired GPG key”error: gpg failed to sign the data”Extend key expiry: gpg --edit-key <id> expire
Missing key on platformCommits show “Unverified” badgeUpload public key to GitHub/GitLab
SSH signing not supported”error: unsupported value for gpg.format”Upgrade Git to 2.34+
Key rotationOld commits show as unverifiedKeep old public keys; upload new ones
CI/CD signingAutomated commits unsignedUse bot keys or disable signing for CI

Trade-off Analysis

AspectGPGSSH
Setup complexityHigher (key generation, keyring)Lower (reuse existing SSH key)
Platform supportUniversal (GitHub, GitLab, Bitbucket)Git 2.34+, GitHub, GitLab 15.0+
Key managementGPG keyring, web of trustSSH keys, allowed signers file
User familiarityLess common among developersWidely understood
Expiry handlingBuilt-in expiry datesNo 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-signature on 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 true for 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

AspectGPGSSHX.509
Setup complexityHigh (keyring, web of trust)Low (reuse existing SSH key)Very high (certificate authority)
Git version requiredAny2.34+2.34+
Platform supportUniversal (GitHub, GitLab, Bitbucket)GitHub, GitLab 15.0+Limited (enterprise GitLab)
Key managementGPG keyring, subkeysSSH keys, allowed_signers filePKI, certificate lifecycle
Expiry handlingBuilt-in expiry datesNo expiry (manual management)Certificate validity period
Hardware supportYubiKey, smart cardsYubiKey, SSH agentSmart cards, HSM
User familiarityLow (most devs don’t know GPG)High (every dev has SSH keys)Low (enterprise IT only)
Best forOpen source, maximum compatibilityModern teams, simplicityEnterprise, 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

1. What does a signed commit prove?

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.

2. Why would you choose SSH signing over GPG?

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.

3. Can you sign commits after they've been pushed?

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.

4. How does GitHub verify GPG signatures?

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."

5. How does the commit signature get embedded in a Git commit object?

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>.

6. What is the difference between signing a commit and signing a tag?

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.

7. What happens if you sign a commit with an expired GPG key?

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.

8. Can you have multiple GPG keys configured for different repositories?

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.

9. What is the "web of trust" in GPG, and why does it matter for commit signing?

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.

10. How do you configure SSH signing to use Ed25519 keys?

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.

11. What are the security implications of using SSH agent forwarding with YubiKey?

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.

12. Why might a repository require all commits to be signed at the platform level?

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.

13. How does key rotation affect historical signed commits?

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.

14. What is a revocation certificate and when should you generate it?

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.

15. How do you handle signed commits in merge commits vs rebased commits?

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.

16. What is the performance impact of signing every commit?

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.

17. Can you sign commits with a key stored in a cloud-based HSM?

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.

18. What is the difference between commit.signature and commit.gpgsign settings?

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).

19. How does signed commit verification work at the protocol level?

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.

20. Why might a team choose X.509 certificates over GPG or SSH for commit signing?

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

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.

#git #version-control #secrets

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.

#git #version-control #secrets

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.

#java #jvm #bytecode