Skip to content

CONCEPT Cited by 1 source

Tag protection

Tag protection is a Git-server-side invariant that, once a named tag is published, the tag cannot be deleted or re-pointed to a different commit. It is the Git-ref clause of publish-time immutability and the structural defence against the tag-hijack supply-chain attack class.

The tag-hijack attack it blocks

Without tag protection, a maintainer (or a compromised maintainer credential) can:

  1. Publish v1.4.2 pointing to commit abc123.
  2. Consumers clone / pin / vendor the release at v1.4.2 → abc123 and record that hash.
  3. Later, the maintainer force-pushes v1.4.2 to point to def456 — a different commit, possibly with a backdoor.
  4. New consumers cloning v1.4.2 get def456. Old consumers who re-clone also get def456. Only consumers who verified the commit SHA see a mismatch — most don't.

The attack works because Git tags are mutable refs by defaultgit push --force --tags is a supported operation. Platforms that host Git repos have historically followed the protocol default and let tag-force-push through.

Tag protection structurally removes the force-push capability: the server's ref-update path rejects any update to a protected tag that would change its commit pointer or delete it.

Relationship to asset locking

On a release-oriented platform, tag protection is necessary but not sufficient for supply-chain integrity:

  • Tag protection alone secures the Git-side name → commit pointer. An attacker who can modify release assets (the compiled binaries, tarballs, attached files) can still swap them even while the tag itself is frozen.
  • Asset locking alone secures the binary artifacts but leaves the tag name mutable — an attacker can re-point v1.4.2 to a different release object entirely.
  • Tag protection + asset locking together freeze the complete name-to-bytes binding.

GitHub immutable releases ship both clauses as one feature, plus a signed release attestation as the portable verification receipt. (Source: sources/2025-10-31-github-immutable-releases-ga)

Canonical instance: GitHub immutable releases (2025-10-28)

"Tags for new immutable releases are protected and can't be deleted or moved."

Scoping notes:

  • Tags for new immutable releases only — tags created manually (git push --tags), or tags for non-immutable releases, are not automatically protected. Repos that want manual-tag protection rely on GitHub's separate branch/tag-protection-rule system.
  • Non-retroactive: disabling immutability on the repo/org setting doesn't unprotect tags for already-immutable releases.

Architectural shape

Tag protection is enforced on the ref-update control-plane path, not as a periodic reconciliation job. The Git server rejects the update at push time:

$ git push --force origin v1.4.2
! [remote rejected] v1.4.2 (tag is protected on immutable release)
error: failed to push some refs

The rejection has to happen synchronously in the push path, because once a force-push lands even briefly, any consumer fetching in that window gets the wrong commit and the compromise has already happened.

This makes tag protection a write-path invariant rather than a policy-check; it is architecturally the same shape as CHECK constraints in a database vs triggers — the former are part of the write contract, the latter are observed after the fact.

Why Git's protocol permits force-pushing tags in the first place

Git tags were designed with two use cases in mind:

  • Lightweight tags: local bookmarks, essentially throwaway.
  • Annotated tags: release markers with author + message + signature.

Git's protocol doesn't enforce the second meaning — both tag types are mutable refs. The convention is that annotated tags on a published repo are immutable, but convention is not enforcement. Platforms (GitHub, GitLab, Gitea) layer tag-protection enforcement on top of the mutable-by-default protocol.

Seen in

Last updated · 200 distilled / 1,178 read