Signed Commits with GPG: Ensuring Authenticity
AI generated
git
HEAD
Git · Security · GPG · Commit Signing
Signed Commits with GPG: Ensuring Authenticity
Why author metadata alone proves nothing

Anyone can set git config user.name and user.email to any value and create commits that appear to come from a colleague, without Git preventing it. This article shows how GPG- and SSH-based commit signatures cryptographically prove who actually created a commit, how to set up keys, verify signatures, and enforce signed commits across a team through branch protection rules.

13 min. read GPG · SSH Signing · Verified Badge Branch Protection · YubiKey · Key Management

1. Why author metadata offers no security

Every Git commit carries author metadata such as name and email, but these fields are set entirely client-side, via git config user.name and git config user.email, without any verification by Git itself. Anyone with write access to a repository, or even someone who just clones a repository locally, can set these values to any name they like, say a tech lead's or a maintainer's, and produce commits that look exactly as if they came from that person in every log and every web interface. Git makes no distinction here between "real" and "made up".

That is exactly what makes commit spoofing a trivial attack rather than a theoretical edge case. An attacker with access to a fork, a compromised CI pipeline, or a loosely configured merge workflow can inject commits under a trusted colleague's name, for example to mislead reviewers or make malicious code look already reviewed. Without cryptographic signatures, every claim about "who committed this" remains a bare assertion, not a verifiable fact. This is precisely where commit signing with GPG or SSH comes in: it ties a commit to a private key, and unlike a name, a private key cannot simply be copied.

2. What a signature actually proves cryptographically

A signature does not prove that a specific person created a commit. It proves something more precise: that the holder of a specific private key signed the exact commit object hash. When signing, Git computes a cryptographic signature over the full commit content, including the tree hash, parent hash, author, committer, and message, so that any later change to any of these fields invalidates the signature. This binding is mathematical, not social: verifying a signature confirms a key, not an identity.

The link between a key and an actual person only comes from a second, separate trust model: for GPG, through the Web of Trust or a central platform like GitHub that associates an uploaded public key with an account; for SSH signing, through an allowed_signers file that maps key fingerprints to specific email addresses. This two-layer structure, cryptographic proof on one side and key-to-identity mapping on the other, is essential to understanding what a signature does and does not achieve: it proves key possession, not automatically the trustworthiness of the code.

3. Generating a GPG key and configuring Git

The first step is generating a dedicated GPG key pair with gpg --full-generate-key. RSA with at least 4096 bits or a modern Ed25519 key is recommended, combined with a sensible expiration of one to two years instead of "never expires". The key's email address must exactly match a verified email address on the Git account, otherwise later verification by GitHub or GitLab fails even if the signature is technically correct.

After generation, gpg --list-secret-keys --keyid-format=long yields the key ID, which is entered into git config --global user.signingkey. With git config --global commit.gpgsign true, Git then signs every commit automatically, without having to append -S to every command manually. The same applies to tags via git config --global tag.gpgSign true. Anyone who only wants to sign individual commits leaves commit.gpgsign at false and uses git commit -S selectively.


# Generate a dedicated GPG key pair for commit signing
$ gpg --full-generate-key
# Choose: (1) RSA and RSA, keysize 4096, expires in 1y

# List secret keys to get the key ID
$ gpg --list-secret-keys --keyid-format=long
sec   rsa4096/3AA5C34371567BD2 2026-07-12 [SC] [expires: 2027-07-12]
uid                 Jane Doe <jane@mironsoft.de>

# Tell Git which key to use and enable signing by default
$ git config --global user.signingkey 3AA5C34371567BD2
$ git config --global commit.gpgsign true
$ git config --global tag.gpgSign true

# Sign an individual commit explicitly (if gpgsign is not global)
$ git commit -S -m "Add signed commit example"

4. Exporting the public key and uploading it to GitHub/GitLab

The public key must be exported in ASCII-armored format with gpg --armor --export <key-id> and uploaded to the relevant platform, under Settings, SSH and GPG keys on GitHub, or under Preferences, GPG Keys on GitLab. Only from that point on can the platform verify incoming signatures against the stored key and display the "Verified" badge in the web interface. Without an uploaded public key, a technically valid signature remains invisible to the platform and is shown as unverified.

It is important to never share the private key or copy it unprotected across multiple machines. Instead, generate a dedicated subkey for each working environment, one that can be revoked through the master key without invalidating the entire identity. A backup of the private key, and above all of the revocation certificate, belongs somewhere outside the working machine, such as an encrypted USB drive or a password manager with file attachments, since a lost key without a revocation certificate can no longer be cleanly withdrawn.


# ~/.gitconfig: signing configuration after key generation
[user]
	name = Jane Doe
	email = jane@mironsoft.de
	signingkey = 3AA5C34371567BD2

[commit]
	gpgsign = true

[tag]
	gpgSign = true

[gpg]
	program = gpg2

5. SSH-based commit signing as a modern alternative

Since Git 2.34, commit signing can also be implemented entirely without GPG by setting git config --global gpg.format ssh and pointing user.signingkey at an already existing SSH public key, for example ~/.ssh/id_ed25519.pub. For many teams this is the more pragmatic choice, since an SSH key for repository access already exists and there is no need to build a separate GPG ecosystem with its own key format, its own expiration handling, and its own Web of Trust.

For local verification, Git needs an allowed_signers file that maps key fingerprints to email addresses, configured via gpg.ssh.allowedSignersFile. GitHub and GitLab maintain their own mapping automatically on the server side as soon as the same SSH public key is already stored as a signing key on the account, so no extra configuration is needed for the "Verified" badge. Locally, for example in CI pipelines, the allowed_signers file must be maintained explicitly, otherwise verification fails despite a correct signature.


# Use SSH keys instead of GPG for commit signing (Git >= 2.34)
$ git config --global gpg.format ssh
$ git config --global user.signingkey ~/.ssh/id_ed25519.pub

# Point Git at a local allowed_signers file for verification
$ git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers

# allowed_signers file: maps email addresses to trusted SSH public keys
$ cat ~/.ssh/allowed_signers
jane@mironsoft.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZ8k... jane-laptop
john@mironsoft.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH3xy... john-yubikey

# Sign a commit with the configured SSH key
$ git commit -S -m "Switch to SSH-based commit signing"

6. Verifying signatures locally with git log and verify-commit

git log --show-signature shows directly in the log for every commit whether a signature is present and valid, including the key ID and trust status. For checking a single commit, git verify-commit <hash> returns a compact pass/fail result with an exit code, which fits well into pre-push hooks or CI jobs to automatically reject unsigned or invalidly signed commits before a merge.

For GPG signatures, the output also shows the key's trust level, unknown, marginal, or fully trusted, depending on whether the key was signed locally or just imported. SSH signatures drop this trust layer entirely: Git only checks whether the fingerprint in the allowed_signers file matches the given email address. The same logic applies to tags via git verify-tag, which is especially useful for release tags in automated deployment pipelines to ensure that only signed releases get rolled out.


# Show signature status directly in the log
$ git log --show-signature -1
commit 9f2a1c4e8b3d5f6a7c8e9b0d1f2a3b4c5d6e7f80
gpg: Signature made Sun Jul 12 10:14:22 2026 CEST
gpg:                using RSA key 3AA5C34371567BD2
gpg: Good signature from "Jane Doe <jane@mironsoft.de>" [ultimate]
Author: Jane Doe <jane@mironsoft.de>
Date:   Sun Jul 12 10:14:22 2026 +0200

    Add signed commit example

# Verify a single commit and check the exit code (useful in CI)
$ git verify-commit 9f2a1c4
gpg: Good signature from "Jane Doe <jane@mironsoft.de>" [ultimate]
$ echo $?
0

# SSH signature verification looks slightly different
$ git log --show-signature -1
Good "git" signature for jane@mironsoft.de with ED25519 key SHA256:AbCdEf...

7. The "Verified" badge: what it checks and its limits

The green "Verified" badge on GitHub or GitLab confirms only that the signature cryptographically matches a public key stored in the displayed author's account, and that this email address was verified at the time of the commit. It does not confirm that the code is correct, safe, or free of vulnerabilities, and it does not confirm that the person who created the commit was actually authorized to do so on behalf of the project.

A common misconception: a stolen but not yet revoked private key keeps producing valid, "Verified" marked signatures until the associated public key is removed or the key is invalidated via a revocation certificate. The badge is therefore an indicator of key possession at signing time, not a security seal for the content. Teams that rely on the badge alone and relax code reviews as a result merely shift the actual security problem instead of solving it.

8. Enforcing signed commits with branch protection rules

For signing to actually matter, rather than remaining an optional courtesy some developers happen to follow, it must be enforced through branch protection rules. On GitHub, the "Require signed commits" option under a protected branch's Branch Protection Rules causes pushes containing unsigned or invalidly signed commits to be rejected server-side, regardless of whether the local developer forgot to set commit.gpgsign or deliberately tried to bypass it.

GitLab offers the same enforcement through Push Rules under "Reject unsigned commits" at the project or group level. The rule can be controlled even more granularly through a compliance pipeline or a server-side hook, for example applying it only to certain branches or only to merge commits. Important for rolling this out on a team: existing, unsigned history is unaffected, since the rule only applies to new pushes, so retroactively rewriting history with git filter-repo or rebase is generally not necessary.


# .gitlab-ci.yml: reject unsigned commits as a CI gate
# (in addition to Settings > Repository > Push Rules > Reject unsigned commits)
stages:
  - verify

verify-signed-commits:
  stage: verify
  image: alpine/git:latest
  script:
    # Fail the pipeline if any commit in this MR is not signed
    - |
      for sha in $(git log --pretty=%H origin/main..HEAD); do
        git verify-commit "$sha" || { echo "Unsigned commit: $sha"; exit 1; }
      done
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

9. Key management: expiration, revocation, hardware tokens

A GPG key without an expiration date might seem convenient, but it is a risk: if the private key is lost or compromised, it stays valid indefinitely without any active action. An expiration of one to two years, renewed regularly with gpg --edit-key and expire, automatically limits the damage of an unnoticed loss. The revocation certificate, created right after key generation with gpg --output revoke.asc --gen-revoke <key-id>, must be stored separately from the key itself, since it is the only way to invalidate a key once access to the private key is gone.

For production teams, a hardware token such as a YubiKey is worth considering: it stores the private key directly on the device and never exposes it as a file on disk. Every signature then requires a physical touch on the token, which renders stolen laptops or malware on a developer's machine ineffective as an attack vector against the signing function. The table below compares GPG, SSH, and no signing across the criteria that matter in practice.

Criterion No signing GPG signing SSH signing
Setup effort No effort, but no protection Key generation, expiration, Web of Trust Reuses an existing SSH key, minimal extra effort
Local verification No verification possible git verify-commit with trust level git verify-commit against allowed_signers
Platform badge No Verified badge Verified badge after public key upload Verified badge after signing key upload
Key loss / rotation No process needed, but also no protection Requires revocation certificate and expiration Simple: add new SSH key, remove the old one
Hardware token support Not relevant YubiKey with OpenPGP applet, well established YubiKey as FIDO2/SSH resident key, growing

Mironsoft

Git security, commit signing, and CI/CD pipelines for PHP and Magento teams

Ready to roll out commit signing on your team?

We help development teams set up GPG- or SSH-based commit signing, configure branch protection rules, and establish key management processes that hold up through staff changes and key loss.

Signing rollout

Set up and document GPG or SSH signing across the team and CI

Branch protection audit

Review existing repository rules and add signature enforcement

CI/CD integration

Set up verification gates in pipelines, including release tag signing

10. Summary

Signed commits with GPG or SSH solve a concrete security problem: author metadata like git config user.name and user.email are bare assertions with no cryptographic backing, and anyone can set them to whatever they like. A signature instead ties a commit to possession of a private key, verifiable with git verify-commit or git log --show-signature, turning commit spoofing into something technically provable rather than merely suspected.

Whether GPG or the newer SSH-based signing is the better choice depends on the team: GPG offers an established ecosystem with a Web of Trust and broad hardware token support, while SSH signing wins on setup effort, since it reuses keys that already exist. Either way, the same chain matters: generate a key, upload the public key to the platform, enforce signing through branch protection rules, and consistently protect the private key, ideally on a hardware token.

Signed Commits with GPG: The Essentials at a Glance

What signatures prove

Possession of a private key at signing time, not automatically code quality or authorization.

GPG setup

gpg --full-generate-key, user.signingkey, commit.gpgsign true, upload the public key to GitHub/GitLab.

SSH signing

gpg.format ssh, reuse an existing SSH key as signingkey, maintain an allowed_signers file locally.

Enforcement & protection

Branch protection rules enforce signatures server-side; expiration dates, revocation certificates, and YubiKeys protect the key.

11. FAQ: Signed Commits with GPG

1What does a signed commit signature actually prove?
It proves the holder of a specific private key signed the exact commit hash. The mapping to a person happens separately via Web of Trust or platform upload.
2Why isn't the author name enough as proof of identity?
user.name and user.email are set client-side and never verified by Git. Anyone can set these fields to whatever they want.
3How do I create a GPG key for commit signing?
gpg --full-generate-key with RSA 4096 or Ed25519 and an expiration date, find the key ID, register it via user.signingkey and commit.gpgsign true.
4How do I set up SSH-based commit signing?
Set gpg.format ssh, reuse an existing SSH public key as signingkey, and configure an allowed_signers file for local verification.
5How do I verify signatures locally?
git log --show-signature in the log, git verify-commit for individual commits with an exit code, git verify-tag for tags.
6What does the Verified badge actually mean?
Only confirms key match and a verified email address. Says nothing about code quality or actual authorization.
7Can a stolen key still produce valid signatures?
Yes, until the public key is removed or revoked via a revocation certificate. That's why expiration dates and a fast response matter.
8How do I enforce signed commits across a team?
GitHub Branch Protection with Require signed commits, GitLab Push Rules with Reject unsigned commits, plus CI gates using git verify-commit.
9What is a revocation certificate?
The only way to invalidate a key once access to the private key is gone. Store it separately from the key itself.
10Is a YubiKey worth it for commit signing?
For production teams, yes. The private key stays on the device, every signature requires a physical touch, stolen machines or malware become ineffective.