Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXIX · Tags and ReleasesTags and Releases

Signed tags — git tag -s, git tag -u, and verification with git verify-tag

Intermediate⏱ ~20 mingitgpg

What you'll learn

  • Create a signed annotated tag with git tag -s and with git tag -u <keyid>
  • Read the gpgsig block in a tag object and explain what it signs
  • Verify a signed tag with git verify-tag and interpret the exit code and key fingerprint
  • Explain the difference between signing a tag and verifying a tag
  • Identify the role of signed tags in the chain of trust from commit to release artifact

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A signed tag is the unit of release identity in a production supply chain. The signature is a GPG signature over the tag bytes, embedded in the tag payload as a gpgsig block. The signer is identified by the key fingerprint, and the signature can be verified by anyone who has the signer’s public key in their keyring. The difference between a tag that is annotated and a tag that is signed is the difference between a release with a human-recorded message and a release with a cryptographically-attested author.

Creating a signed tag

The -s flag creates an annotated tag and signs it with the default GPG key. The -u flag accepts a key ID or fingerprint and signs with that specific key:

# Sign with the default key (user.signingkey or the only secret key)
git tag -s v1.0.0 -m "Release v1.0.0" "$COMMIT_OID"

# Sign with a specific key
git tag -u "ops-release@example.com" v1.0.0 -m "Release v1.0.0" "$COMMIT_OID"

The -m flag is required by -s and -u — there is no “sign a tag without a message” form, because a signed tag without a message is a signature over an empty body. The discipline is the same as for an unsigned annotated tag: the message is the release notes, and the signature is the attestation.

# Inspect the signed tag object
git cat-file -p v1.0.0
# object 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e
# type commit
# tag v1.0.0
# tagger Ops <ops@example.com> 1730000000 +0000
#
# Release v1.0.0
# -----BEGIN PGP SIGNATURE-----
#
# iQEzBAABCgAdFiEE... (base64 signature)
# =/WvN
# -----END PGP SIGNATURE-----

The gpgsig block is the signature. Git parses it transparently: git show v1.0.0, git cat-file -p v1.0.0, and git tag -n all print it; git verify-tag validates it.

flowchart LR
    A["git tag -s v1.0.0 -m Release $COMMIT"] --> B["annotated tag object\npayload + gpgsig block"]
    B --> C["refs/tags/v1.0.0\ntag-oid"]

Verifying a signed tag

git verify-tag &lt;tag&gt; validates the signature against the signer’s public key. The exit code is the ground truth:

# Verify a signed tag
git verify-tag v1.0.0
# gpg: Good signature from "Ops <ops@example.com>"
# gpg: WARNING: This key is not certified with a trusted signature!
# gpg:          There is no indication that the signature belongs to the owner.
echo $?
# 0

Exit 0 means “the signature is mathematically valid”. The “WARNING: This key is not certified with a trusted signature!” line is about trust, not validity: GPG distinguishes “the signature was made by this key” (a mathematical fact) from “this key belongs to someone I trust” (a web-of-trust or trustdb fact). The two are independent. For production use, the trust comes from publishing the key fingerprint out-of-band (in a team doc, in a CI configuration, in a verified social-media post) and configuring the keyring to mark the key as trusted.

# Verify and require trust
git verify-tag v1.0.0 2>&1 | tail -n 1
# gpg: Good signature from "Ops <ops@example.com>"

# Inspect the key fingerprint
git verify-tag --raw v1.0.0
# ... [GNUPG:] VALIDSIG <fingerprint> <date> ...

The --raw flag prints the GPG status messages, including the VALIDSIG line with the key fingerprint. This is the right form for a CI step that needs to log which key fingerprints signed which tags.

Sign versus verify

The two operations have different roles in the supply chain:

  • Sign. The release engineer runs git tag -s (or -u) to create a signed tag. The signature attests that the engineer — identified by the key fingerprint — tagged this commit with this message. The signature is made with the engineer’s secret key, and the secret key never leaves the engineer’s machine or the engineer’s HSM.
  • Verify. The consumer (the CI pipeline, the audit team, the downstream deploy job) runs git verify-tag. The verification uses the signer’s public key. The public key can be distributed freely; the verification confirms that the signature was made by the holder of the corresponding secret key.
flowchart LR
    S["signer holds secret key\nruns git tag -s"] --> T["signed tag"]
    T --> V["consumer holds public key\nruns git verify-tag"]
    V --> R["valid signature\n+ trusted fingerprint\n= attested tag"]

The architecture is asymmetric: signing requires the secret key, verification requires only the public key. Production discipline is to keep the secret key on a dedicated machine (or HSM) and to publish the public key fingerprint through a channel that is independent of Git (the team doc, the company website, a signed social-media post).

The gpgsig block

The gpgsig block is part of the tag object’s payload, and the signature covers the other parts of the payload (the object, type, tag, tagger, and message lines). The signature itself is excluded from the signed bytes — otherwise the signature would be self-referential. Git computes the signed bytes by stripping the gpgsig block from the payload and signing what remains.

# The tag payload with the gpgsig block
git cat-file -p v1.0.0
# object 8a3f9d2...
# type commit
# tag v1.0.0
# tagger Ops <ops@example.com> 1730000000 +0000
#
# Release v1.0.0
# -----BEGIN PGP SIGNATURE-----
# iQEzBAABCgAdFiEE...
# -----END PGP SIGNATURE-----

# The signed bytes are the payload minus the gpgsig block
git verify-tag v1.0.0
# gpg: Good signature from "Ops <ops@example.com>"

This is why a change to the tag’s message invalidates the signature. The message is part of the signed bytes, so editing the message after signing produces a tag object whose bytes no longer match the signature. The discipline is to write the message, sign the tag, and not edit afterwards.

Production discipline

  1. Use -s or -u for every release tag. The signature is the cryptographic ground truth of who tagged the commit; an unsigned release tag is a tag the audit cannot attest.
  2. Specify the signing key with -u &lt;keyid&gt; in CI. The -s flag uses whatever the runner’s default key is, which may not be the team’s release key. The -u flag makes the key explicit and the pipeline deterministic.
  3. Publish key fingerprints out-of-band. A signed tag by an unknown fingerprint is cryptographically valid and operationally meaningless. The trust step is the publication of the fingerprint through a channel that is independent of Git.
  4. Never edit a tag after signing. The message is part of the signed bytes; an edit invalidates the signature and the tag has to be re-created. A correction is a new tag, not a modification of the old one.

Cross-course references

  • Git, CI/CD & GitOps — Part XIII (Signing) — GPG key generation, key management, and the trust model that underpins git tag -s and git verify-tag.
  • Git, CI/CD & GitOps — Part XIX-05 (Tag protection and releases) — the GitHub/GitLab settings that forbid tag deletion and the release-notes workflow that consumes signed tags.
  • Git, CI/CD & GitOps — Part XIX-06 (Release workflows) — the git tag -s invocation inside a tag-on-merge release pipeline.
  • Ansible for Production Sysadmins — Part XXXVIII (Review) — signed release tags as the boundary between reviewed code and deployed code.

Quiz

Knowledge check · 4 questions

  1. Q1. What does `git tag -s v1.0.0 -m "Release" $COMMIT_OID` produce, and what is the role of the `-s` flag?

  2. Q2. A `git verify-tag` exit code of 0 means the tag is signed by a key the verifier trusts.

  3. Q3. What is the difference between `git tag -s` and `git tag -u &lt;keyid&gt;`, and when is `-u` required?

  4. Q4. Diagnose a release pipeline whose signed tag is rejected by downstream verification, and propose the corrected signing step.

    A release pipeline runs `git tag -s v3.0.0 -m 'Release v3.0.0' $COMMIT_OID && git push origin v3.0.0`. The push succeeds. The downstream deploy pipeline runs `git verify-tag v3.0.0` and exits 0, but the audit team rejects the release because the key fingerprint does not match the team's published release fingerprint. The runner's `user.signingkey` was not configured, and the `-s` flag fell back to the only secret key in the keyring, which is the engineer's personal key, not the team's release key.

Passing score: 75%. Answers are checked in this browser.