Enforcing Immutable Image Tags in Harbor: Decision Guide and Practical Steps
Harbor 2.6+ lets you mark image tags immutable via a native API, blocking any overwrite at the registry level. This guide compares native immutability with CI gates and Notary signing, shows the exact curl commands to enable and verify a tag, and lists the operational limits you must consider.
30 Oct 2026, 08:13 UTC

The problem
Container registries are a common attack surface: a compromised CI token or a mistaken docker push can overwrite a production tag, causing rollouts of untested or malicious images. Harbor 2.6 introduced a native immutable‑tag API that makes a tag read‑only at the registry level, blocking any subsequent push that would change its digest. This article helps you decide whether to adopt that feature, compares it with alternative enforcement methods, and shows how to enable and verify immutability for a specific tag.
Decision constraints
- Harbor version – immutable tags require Harbor 2.6 or later. Older deployments must upgrade or rely on work‑arounds.
- Operational overhead – the API works per tag; there is no bulk‑immutable operation in the UI or CLI.
- Security impact – native enforcement is client‑agnostic, so it protects against direct registry pushes that bypass CI pipelines.
- Repository settings – if Allow untagged images is enabled, pushes without a tag (manifest‑only) are not blocked by the immutable‑tag flag.
Option comparison
| Approach | Enforcement point | Setup effort | Bypass risk | Version requirement |
|---|---|---|---|---|
| Harbor native immutable tag | Registry (API) | Low – one API call per tag | None (registry rejects overwrite) | Harbor 2.6+ |
CI pipeline gate (e.g., docker manifest inspect + policy check) | CI runner | Medium – script per pipeline | High – user can push directly to registry | Any |
| Notary / cosign signing + policy controller | Admission controller / verification step | High – key management, policy config | Medium – unsigned images can still be pushed if policy not enforced everywhere | Any (extra components) |
Trade‑offs
Native immutability gives the strongest guarantee with the least moving parts, but you must remember to mark each release tag individually. CI gates are flexible and can incorporate custom logic (e.g., only allow tags matching v*), yet they only protect the pipeline path. Notary/cosign provides cryptographic provenance, which is valuable for supply‑chain attestation, but adds key‑rotation overhead and requires every consumer to verify signatures.
Concrete implementation: marking a tag immutable
Assume you have a Harbor project myproject and a repository myapp. The tag to protect is v1.4.0. You need a Harbor admin or a user with Repository Admin permission on that repository.
1. Call the immutable‑tag endpoint
curl -X POST "https://harbor.example.com/api/v2.0/repositories/myproject/myapp/tags/v1.4.0/immutable" \
-H "Authorization: Bearer $HARBOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"immutable": true}'
Where to run: any host with network access to Harbor (workstation, CI runner, or Harbor CLI container).
Permissions: the token must belong to a user with Repository Admin role on myproject/myapp.
Placeholders: replace harbor.example.com, myproject, myapp, v1.4.0, and $HARBOR_TOKEN with your values.
Expected response: HTTP 200 with JSON body {"immutable":true}.
2. Verify the flag persisted
curl -s -H "Authorization: Bearer $HARBOR_TOKEN" \
"https://harbor.example.com/api/v2.0/repositories/myproject/myapp/tags/v1.4.0" | jq .immutable
The command should output true. If it returns false or the field is missing, the tag was not marked immutable (possible version mismatch or insufficient permissions).
3. Test enforcement
# Attempt to push a new image with the same tag
docker pull alpine:3.19
docker tag alpine:3.19 harbor.example.com/myproject/myapp:v1.4.0
docker push harbor.example.com/myproject/myapp:v1.4.0
The push should fail with a 403 Forbidden error similar to:
denied: tag v1.4.0 is immutable
If the push succeeds, either the tag is not immutable or the repository has Allow untagged images enabled and the push used a manifest‑only reference.
Limitations and gotchas
- Per‑tag only – you must repeat the API call for every release tag (e.g.,
v1.4.0,v1.4.1). Automate with a script that runs after a successful build. - No bulk UI action – Harbor’s web UI does not expose a “make all tags immutable” button as of 2.10.
- Untagged pushes – if the repository setting Allow untagged images is on, a client can push a manifest without a tag (
docker push harbor.example.com/myproject/myapp@sha256:…) and the immutable flag does not apply. Disable that setting if you need full protection. - Version dependency – on Harbor < 2.6 the endpoint returns 404; you must upgrade or use an external gate.
Practical verification checklist
- Confirm Harbor version ≥ 2.6 (
GET /api/v2.0/systeminfo). - Run the immutable‑tag POST and capture the JSON response.
- GET the tag details and assert
immutable === true. - Attempt an overwrite push; expect HTTP 403 with a message containing “immutable”.
- Document the tag‑immutable step in your release runbook so future maintainers repeat it.
When to choose an alternative
If you cannot upgrade Harbor, or you need policy logic that depends on tag patterns, branch names, or vulnerability scan results, a CI gate remains the pragmatic choice. For organizations requiring cryptographic proof of image provenance across multiple registries, invest in Notary/cosign despite the higher operational cost.
Bottom line
Harbor’s native immutable‑tag feature is the simplest, most reliable way to prevent accidental or malicious tag overwrites—provided you run Harbor 2.6+ and can automate the per‑tag API call. Use the verification steps above to confirm enforcement before you rely on it in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.