Pillar 2’s signed commits lesson covers setup: generating a key, configuring Git, getting the green Verified badge.
This page is about what that badge means — and, more usefully, what it does not.
The single most important sentence in this lesson:
A valid signature proves something about the relationship between a signing key and an identity. It does not prove the code itself is safe.
The short answer
Section titled “The short answer”Signing attaches a cryptographic signature to a commit or tag object. Verification checks that signature against a public key, and GitHub additionally checks that the key is registered to an account.
That chain answers exactly one question: was this object produced by someone holding the private key associated with this identity?
It answers nothing about whether the change is correct, safe, reviewed or intentional. A developer whose laptop is compromised signs malicious commits with their own key, and every signature verifies perfectly.
Signing is worth doing because Git’s author field is otherwise an unauthenticated free-text field that anyone can set to anything. It closes impersonation. It does not close anything else.
Why authorship in Git is a claim
Section titled “Why authorship in Git is a claim”Git has no central identity provider. When you commit, Git records user.name and user.email from
your configuration — values you chose, on your machine, with no verification step anywhere.
git -c user.name="Example Maintainer" \ -c user.email="maintainer@example.com" \ commit --allow-empty -m "Demonstration commit"What it doesCreates a commit attributed to a different name and email address without changing any stored configuration.
Why we run itIt demonstrates that the author field is input, not evidence. Run it in a disposable repository.
Expected resultA commit whose git log output shows the supplied name and address.
Push that to GitHub with an email address matching a real account, and the web interface shows that account’s avatar next to the commit. Nothing has been broken; the field simply never carried a guarantee.
This is not a defect. Git is a distributed system in which any clone must be able to create commits offline. Authorship is metadata by design, and signing is the layer that turns metadata into evidence.
What the signature covers
Section titled “What the signature covers”A Git commit object is text: tree hash, parent hashes, author, committer, message. Signing computes a signature over that content and stores it in the object.
Two consequences follow directly from that:
The signature covers the whole commit, including its parents. Changing anything — the message, the tree, the author, the parent — produces a different object with a different hash, and the old signature does not apply to it.
It says nothing about the tree’s contents beyond their hash. The signature commits you to this exact snapshot. It expresses no opinion on whether the snapshot is good.
There is one more property worth internalising: because a commit includes its parents’ hashes, a signature on a commit transitively fixes the entire history reachable from it. A signed release tag is therefore a strong statement — it pins the whole history, not just the tip.
Signing methods
Section titled “Signing methods”GitHub supports GPG, SSH and S/MIME for commit signature verification.
| SSH | GPG / OpenPGP | S/MIME | |
|---|---|---|---|
| Setup effort | Low — you may already have a key | Moderate — key generation, expiry, subkeys | Organisational — certificate issued by a CA |
| Key you upload | A signing key entry on your account | A public key on your account | Handled through the certificate chain |
| Expiry | No built-in expiry | Built in, and worth using | From the certificate |
| Revocation | Remove the key from the account | Revocation certificate plus removal | CA revocation |
| Web-of-trust or CA | Neither | Optional web of trust | CA-backed identity |
| Typical fit | Individuals and most teams | Projects with an existing OpenPGP culture | Larger organisations with PKI |
SSH signing is the pragmatic default for most people. You very likely already have an SSH key, Git has supported SSH signing for several releases, and it removes the largest practical obstacle to adoption, which was never cryptography — it was GPG’s user experience.
GPG is not obsolete or worse. It has genuine advantages: built-in expiry, a revocation mechanism designed for the purpose, subkeys so the signing key need not be the master key, and an ecosystem of existing tooling. If your project already uses it, there is no security argument for migrating. GPG / OpenPGP signing covers it properly.
Configuring SSH signing
Section titled “Configuring SSH signing”git config --global gpg.format sshgit config --global user.signingkey ~/.ssh/id_ed25519.pubgit config --global commit.gpgsign truegit config --global tag.gpgsign trueNote the fourth line. Tag signing is configured separately from commit signing and is frequently missed, which produces repositories full of signed commits and unsigned release tags — precisely backwards, since the tag is what consumers pin to.
Local verification, without GitHub
Section titled “Local verification, without GitHub”GitHub’s badge is convenient and it is not the only check available. Verifying locally matters because it is the version that works when you do not trust the platform’s rendering.
git log --show-signature -3What it doesShows each commit's signature validation result alongside the log entry.
Why we run itIt is the check that does not depend on a web interface, and the one to reach for during an incident.
Expected resultA Good signature line per signed commit, or a note that no signature was found.
git verify-commit HEADWhat it doesVerifies a single commit's signature and exits non-zero if it does not validate.
Why we run itIt is scriptable, which makes it usable in a check rather than in a habit.
Expected resultExit status 0 for a good signature; a diagnostic and non-zero status otherwise.
For SSH signatures, local verification needs an allowed signers file mapping identities to public keys — Git has no equivalent of GitHub’s account registry:
echo "engineer@example.com ssh-ed25519 AAAA_EXAMPLE_PUBLIC_KEY_MATERIAL" \ >> ~/.config/git/allowed_signersgit config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signersWithout that file, Git can confirm a signature is internally well-formed but cannot say whose it is —
reported as No principal matched. That state is easy to mistake for verification.
Some projects commit an allowed_signers file to the repository and point Git at it, which makes
“who may sign for this project” a reviewable file rather than local configuration. It is a good
pattern, with one caveat: whoever can merge a change to that file can add a key.
How GitHub verifies
Section titled “How GitHub verifies”GitHub does something Git alone cannot: it checks the signature and checks that the key belongs to an account, then relates the signer to the commit’s author and committer fields.
The statuses:
| Status | Meaning |
|---|---|
| Verified | Signed, and the signature verified against a key registered to an account |
| Partially verified | Signed and verified, but the author is a different person who has enabled vigilant mode |
| Unverified | Signed, but the signature could not be verified |
| (no badge) | Unsigned |
That last row is the gap most people miss. By default an unsigned commit shows nothing — not a warning, not a red mark. A repository where most commits are signed and one is not looks, at a glance, entirely normal.
Vigilant mode
Section titled “Vigilant mode”Vigilant mode changes the default. With it enabled on your account, commits attributed to you that are not signed are displayed as Unverified rather than as unmarked.
That is the setting that makes signing an actual anti-impersonation control. Without it, an attacker who forges your name simply produces an unsigned commit, which renders identically to every other unsigned commit in the repository. With it, the forgery is visibly marked.
Partially verified exists because Git separates author from committer. If Alice writes a commit and Bob rebases it, Bob signs it — the signature is Bob’s, and it says nothing about whether Alice consented to the final form. When the author has vigilant mode enabled, GitHub reports that distinction rather than flattening it to Verified.
Enforcement, and the merge trap
Section titled “Enforcement, and the merge trap”A ruleset or branch protection rule can require signed commits on a protected ref. Every commit reaching that branch must carry a verifiable signature.
This is where a real and widely-encountered problem appears.
Commits made through the GitHub web interface — editing a file in the browser, accepting a suggestion in review — are created by GitHub and signed with its web-flow key, whose public half is published so anyone can verify it. That is why a repository requiring signatures does not block web edits.
It is also worth understanding for what it is: those commits are signed by GitHub, attesting that GitHub made the change on the authenticated user’s behalf. That is a different claim from a commit signed on a developer’s machine, and it is exactly as strong as your trust in the platform.
When a key is compromised
Section titled “When a key is compromised”Signing introduces a failure mode that unsigned repositories do not have: what happens when the key is somebody else’s.
The uncomfortable property is that signatures do not expire retroactively in a useful way. A signature made yesterday with a key you revoke today still verifies against the key. Git records no timestamp authority, so “was this signed before or after the compromise?” has no cryptographically reliable answer from the signature alone.
Practically, the response is:
-
Remove the key from your GitHub account immediately. New commits signed with it stop showing as Verified.
-
Generate and register a replacement, so you can keep working.
-
For GPG, publish a revocation certificate — the mechanism designed for this, which is why you generate it at key creation time rather than when you need it.
-
Identify what was signed with it. Search the repository’s history for commits signed by the key and establish which are legitimate.
-
Consider re-signing a known-good point. A signed tag on a commit you have verified by other means gives consumers a reference that does not depend on the compromised key.
-
Tell downstream consumers. Anyone who has been verifying your signatures needs to know which key to stop trusting, and from when.
Step 6 is the one organisations skip. If nobody was verifying, it costs nothing; if somebody was, its absence means they are still trusting a key you know is compromised.
The threat model, stated precisely
Section titled “The threat model, stated precisely”It is worth writing this out, because “we sign our commits” is often used to answer questions signing does not address.
Threat. A commit is attributed to a trusted maintainer without that maintainer having produced it — either to slip a change past reviewers who trust the name, or to muddy attribution after an incident.
Attack surface. Anyone who can push to any branch of the repository, or open a pull request from a fork, can set the author field to anything. That includes external contributors on a public repository.
Impact. Modest on its own, and significant in combination. A reviewer skimming a diff from a trusted colleague reads it differently from the same diff from a stranger, and that difference is exactly what the forgery is buying.
Control. Signing plus vigilant mode makes an unsigned commit attributed to a signing user visibly unverified. A ruleset requiring signed commits makes it un-mergeable.
Verification. Push an unsigned commit with a colleague’s name to a test repository and look at how it renders, before and after enabling vigilant mode. This takes two minutes and is more convincing than any explanation.
Note what is absent from that model: nothing about malicious code, nothing about compromised machines, nothing about review quality. Those threats are real and they belong to other controls.
Verifying signatures in CI
Section titled “Verifying signatures in CI”Enforcement through a ruleset is the strong version, and there are situations where you want an independent check — verifying a dependency’s release tag, or confirming what landed on a branch you consume.
- name: Verify the release tag is signed by a known maintainer run: | git verify-tag "${TAG}" env: TAG: ${{ github.event.release.tag_name }}Two things to get right for this to mean anything.
Fetch enough history and the tag object itself. A shallow clone may not have the annotated tag,
and git verify-tag on a missing object is a confusing error rather than a failure.
Supply the trust anchor. For SSH signatures the job needs an allowed_signers file, and where
that file comes from is the security decision. Committed to the repository, it is protected by
whatever protects the repository. Fetched at run time, it is protected by whatever protects that
fetch.
The failure mode to avoid is a verification step that passes because it verified nothing — no principal matched, no signature present, wrong object. Test it against a deliberately unsigned tag and confirm the job fails.
What signing does not replace
Section titled “What signing does not replace”Because signing is cryptographic, it attracts more confidence than its scope supports. Four things it is regularly asked to do and does not:
It is not review. A signature says who; review says whether.
It is not access control. Anyone with a registered key and write access can sign and push. The signature does not consult a permission model.
It is not provenance for artifacts. A signed commit says something about source. The binary somebody downloads was produced by a build, and connecting the two is build provenance and artifact attestations — a separate mechanism with a separate chain of evidence.
It is not non-repudiation in any legal sense. Git records no trusted timestamp, keys are held on ordinary laptops, and revocation does not reach backwards. Treat a signature as strong operational evidence, not as proof of intent.
Organisational adoption
Section titled “Organisational adoption”Requiring signatures across an organisation is a cultural change more than a technical one, and it fails in a predictable way: mandate first, tooling second, and a month of blocked merges.
An order that works:
- Start with the people who publish. Maintainers who cut releases are a small set, and release tags are the highest-value thing to sign.
- Sign tags before commits. Fewer objects, higher value, less disruption.
- Enable vigilant mode on individual accounts. It is free, it is per-account, and it makes impersonation visible without blocking anyone.
- Run the ruleset in Evaluate mode to see who and what would break. Bots and integrations are usually the surprise.
- Fix automation. Anything that commits from CI needs a signing identity, which usually means a GitHub App or a dedicated key held as a secret. This is the step that takes real work.
- Enforce, once steps 4 and 5 are clean.
Common mistakes
Section titled “Common mistakes”Believing a signature vouches for the code. It vouches for the key. A compromised developer machine produces perfectly verified malicious commits.
Signing commits and not tags. commit.gpgsign and tag.gpgsign are separate settings, and the
tag is what consumers pin to.
Uploading an SSH key for authentication and expecting signatures to verify. The signing key entry is separate.
Requiring signed commits while allowing rebase and merge. These conflict, and the failure appears at merge time.
Treating absence of a badge as safe. Without vigilant mode, unsigned commits are unmarked, not flagged.
Storing a signing key in CI without thinking about what it means. A key in a repository secret is a key that any workflow — including one added in a pull request — can potentially reach. Prefer a GitHub App identity or attestations over shipping a private key into a runner.
Mental model
Section titled “Mental model”A signature is a statement: “the holder of this key produced exactly these bytes.” Verification checks the statement. Everything else — whether the key’s holder is who you think, whether they meant it, whether the bytes are any good — is a separate question with a separate answer.
What you learned
Section titled “What you learned”- Git’s author and committer fields are unauthenticated configuration; signing is what makes authorship verifiable
- A signature covers the whole commit object, and transitively pins the history reachable from it
- GitHub supports GPG, SSH and S/MIME; SSH signing is the lowest-friction default for most teams
- An SSH key must be registered specifically as a signing key, separately from authentication
- Unsigned commits show no badge by default; vigilant mode is what makes impersonation visible
- Partially verified reflects Git’s author/committer split, not a weaker signature
- Rebase and merge adds commits without signature verification and conflicts with a signing requirement
- Revoking a key does not invalidate signatures already made with it
- Signing proves origin and says nothing about whether the change is safe
Exercise
Section titled “Exercise”Use a disposable repository.
-
Configure SSH signing and make a signed commit. Run
git log --show-signatureand read the output. -
Register the key on GitHub as an authentication key only. Push. Predict: what status does GitHub show? Then add it as a signing key and refresh.
-
Make a commit with
-c user.nameand-c user.emailset to a colleague’s details, unsigned. Push it. Predict: does anything visually distinguish it? -
Enable vigilant mode on your account and look again.
-
Create a signed tag with
git tag -s v1.0.0 -m "Release". Verify it withgit verify-tag v1.0.0. Then check whether your ordinary commits were being signed and the tag was not. -
Add a ruleset requiring signed commits. Push an unsigned commit and read the rejection.
-
With the rule active, open a pull request and try each merge method your repository allows. Predict: which succeed?
-
Delete the repository.
Related lessons
Section titled “Related lessons”Check your understanding
3 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.
Get the repository security checklists — setup, rulesets, tokens — from the Professional Toolkit.