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.

What a signature on each object vouches forA commit signature covers a change, its author and its position in history. A tag signature covers a name pointing at a commit, which makes it the natural thing to verify at release and deploy time. Both transitively cover the full tree, because each includes a hash that pins it.Signed commitSigned annotated tagcoversone change + historya name β†’ a commitmade bythe author or rewriterthe release manager or CIchecked atmerge, push, auditrelease, deploy, installlightweight formn/acannot be signedcommits answer 'who changed this'; tags answer 'is this really v2.3.0'
# 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
Where each signature is checked in a releaseCommit signatures are verified as changes merge into the main branch. When a release is cut, the tag signature is verified by the release pipeline, then again by the deploy step, so that what reaches production is the tree the release manager declared.Pull requestverify commitsMerge to mainrange verifiedSigned tagtag -s v2.3.0Release buildverify-tagDeployverify-tag againthe tag check is the one that protects what users actually run

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.0 pointing at an earlier, vulnerable commit.
  • Tag signatures only protect the release declaration: users can confirm that v2.3.0 is 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.

Which signatures a project needsProjects that publish tagged releases to users need signed tags at minimum. Projects that deploy continuously from a branch need signed commits. Projects with compliance requirements, or that do both, should sign both.How do people consume what you build?tagged releasesSign tags firstusers verify releasescontinuous deploySign commitsbranch is the releaseboth, or auditedSign bothfull chainmost mature projects end up signing both β€” the order you adopt them is the real decision

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.