Signed tags vs signed commits: what each proves Jump to heading
Teams often enable one kind of signing and assume it covers the other. Some sign every commit and never sign a release tag, so the thing users actually download has no signature tied to it. Others sign release tags but not commits, so every change between releases is unattributed. The two prove different things. A commit signature says this person produced this change on top of this history. A tag signature says this person declares that this exact tree is release 2.3.0. This page lays out the difference, shows how to create and verify each, and explains when one is enough. It extends GPG vs SSH commit signing, and everything here works with either format.
When to use this approach Jump to heading
- You publish releases from tags and want users or deploy systems to verify them.
- You sign commits already and are deciding whether tag signing adds anything.
- You sign tags only and are deciding whether to require commit signatures too.
- Your release automation creates tags, and you need to decide who or what signs them β see signing release tags from a pipeline.
Step 1 β Know what each object covers Jump to heading
A commit object contains a tree hash, parent hashes, author, committer and message. Its signature covers all of that, so by extension it covers the entire tree and, through the parent hash, the entire history behind it. An annotated tag object contains the hash of the object it points to (usually a commit), the tag name, the tagger and a message. Its signature covers those.
# Look at the raw objects to see what is covered
git cat-file -p HEAD | sed -n '1,6p'
git cat-file -p v2.3.0 | sed -n '1,6p' Step 2 β Create signed annotated tags, never lightweight ones Jump to heading
A lightweight tag is just a ref pointing at a commit; there is no object to sign. Only annotated tags can carry a signature.
git config --global tag.gpgSign true # sign every annotated tag
git tag -a v2.3.0 -m "Release 2.3.0" # signed because of the setting
git tag -s v2.3.0 -m "Release 2.3.0" # or explicitly with -s
git cat-file -t v2.3.0 # tag (a lightweight tag would say: commit) Release tooling sometimes creates lightweight tags by default. Check what your tooling produces before relying on tag signatures.
# Verification: list tags that are lightweight (no tag object) β these cannot be verified
git for-each-ref refs/tags --format='%(objecttype) %(refname:short)' | awk '$1=="commit"{print $2}' Step 3 β Verify each where it is consumed Jump to heading
Commit signatures are verified where changes are integrated: in commit verification gates on pull requests and pushes. Tag signatures are verified where releases are consumed: in the release pipeline before it builds, in deploy tooling before it rolls out, and by users before they install.
# Release pipeline guard: refuse to build from an unsigned or untrusted tag
git verify-tag "$GITHUB_REF_NAME" || { echo "tag not signed by a trusted key"; exit 1; }
# Also confirm the tag points at a commit on the release branch
git merge-base --is-ancestor "$GITHUB_REF_NAME^{commit}" origin/main Step 4 β Decide whether you need both Jump to heading
The answer depends on what you are protecting against.
- Commit signatures only protect the integration process: nobody can push changes as someone else. They do not tell a user which commit is the release; anyone could create a tag
v2.3.0pointing at an earlier, vulnerable commit. - Tag signatures only protect the release declaration: users can confirm that
v2.3.0is what the maintainer declared. They say nothing about who wrote the changes in between, which matters for audits and insider-risk scenarios. - Both give an attributable chain from every change to every release, which is what supply-chain frameworks expect.
For open-source projects that publish releases, tag signing is the higher-value of the two if you can only do one. For internal services deployed continuously from the main branch without tags, commit signing is the one that matters.
Step 5 β Make tag signatures discoverable for users Jump to heading
A signature users cannot verify is decoration. Publish the public keys that sign releases somewhere stable, and document the one command that checks a release.
# Document this in your release notes or README
git fetch --tags
git verify-tag v2.3.0
# For SSH-signed tags, users need the allowed signers file you publish:
git -c gpg.ssh.allowedSignersFile=release-signers verify-tag v2.3.0 Keep the release-signing keys separate from personal commit-signing keys, and keep the list short. If releases are tagged by automation, the release key belongs to that automation, with its own custody rules.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does signing a tag sign all the commits it points to? Jump to heading
It vouches for them, because the tag pins a commit hash that pins the whole history. But it does not attribute each change to an author. A signed tag over unsigned commits says βthis is the releaseβ, not βeach change came from who it claimsβ.
Can I sign an existing lightweight tag? Jump to heading
Not in place. Replace it with an annotated, signed tag of the same name pointing at the same commit, then force-push the tag. Moving or replacing a published tag has consequences for anyone who already fetched it; see moving or deleting a published tag.
Do forges show tag signatures? Jump to heading
Yes; signed annotated tags show a verified badge on the tags and releases pages when the forge knows the key. Release pages created from lightweight tags have nothing to show.
Related Jump to heading
- GPG vs SSH Commit Signing β the parent topic.
- Signing and Verifying Release Tags β the release-process view of tag signing.
- Annotated vs Lightweight Tags β why only one kind can carry a signature.
- Generating Provenance for a Tagged Release β linking the signed tag to the artefact built from it.