ECR tag immutability: the basics
What does "mutable" actually mean for a container tag?
An Elastic Container Registry (ECR) repository holds container image manifests, each addressed by a content-hash digest (sha256:...). Tags like :v1.2.3 or :latest are just human-readable labels that point at one of those digests. The image itself is immutable — the digest is a hash of the bytes — but in the default MUTABLE mode the tag-to-digest mapping is a writable pointer. Anyone with ecr:PutImage can push a new image, reuse the same tag, and silently re-aim the label at a different digest.
What "bad" looks like: your build pipeline pushes myapp:v1.2.3 on Monday, you audit it, and your Kubernetes deployment pulls that exact tag. On Friday a compromised CI token (or a sloppy engineer) pushes a different image under the same :v1.2.3 tag. The deployment restarts, pulls the tag, and runs new bytes. Nothing in your audit log or your manifest history changes — from the workload's perspective it's still :v1.2.3.
AWS Security Hub flags this exact pattern as control ECR.2 ("ECR private repositories should have tag immutability configured"). The control simply checks the repo's imageTagMutability setting. IMMUTABLE passes; MUTABLE fails. It's a one-flag fix — but the discipline change around how you tag images is the actual work.
In this lesson you'll learn why mutable image tags are a supply-chain vulnerability, how to identify which of your ECR repositories still allow them, and exactly which CLI calls flip the setting. You'll also see the tagging discipline that has to come with the change — because flipping the flag without retraining your pipelines is how you brick a deploy at 3am.
The :latest tag broke an entire airline
In 2021 a regional carrier traced an outage to a base image push: their build system kept pulling python:3.9 from a public registry under a mutable tag, the upstream maintainer rebuilt the tag against a newer Debian, and a glibc dependency drift broke a native module in production. Total downtime: 6 hours. The remediation wasn't "never use Python" — it was "never trust a tag you don't control the immutability of." Pinning to a digest or to an immutable internal mirror is the only way to know your :v1.2.3 is the :v1.2.3 you audited.
Locking down a mutable ECR repo in action
Marco is the platform lead at a fintech. A weekly Security Hub digest lands in his inbox: 14 ECR.2 findings across the production account. Severity: MEDIUM. Affected resources: every repository his team owns, plus a handful left over from a contractor project.
He doesn't flip them all at once. Some repos are pushed to by build pipelines that re-use tags like :dev or :staging deliberately — those will start failing the moment he turns immutability on. He needs to see which repos still have mutable settings, what their last-pushed-tag patterns look like, and then make the change repo by repo.
He starts by listing every repository's mutability state in one shot.
First, list every private repo with its mutability flag. The query slices to just the three fields that matter for triage.
Repo-by-repo mutability state for the production account.
Now flip payments-api to IMMUTABLE. The call is idempotent — re-running it on an already-immutable repo is a no-op.
One flag flip. The next push that tries to re-tag an existing label returns TagAlreadyExistsException.
Tag immutability under the hooddeep dive
Every push to ECR is an ecr:PutImage API call that sends a manifest, a digest, and zero or more tags. With imageTagMutability = MUTABLE, the registry accepts the call and overwrites whatever the tag previously pointed at — the old manifest is still in the repo (referenced by its digest), but the tag now resolves to the new one. With IMMUTABLE, the same call returns TagAlreadyExistsException if any of the tags are already in use. The image bytes are accepted (the digest is still pushed), but the human-readable label refuses to move.
Flipping the flag is a metadata-only change on the repository — it does not rewrite history. Tags that were mutated yesterday stay where they last pointed; you just can't mutate them again. This is why IMMUTABLE is safe to enable on a live repo: existing deployments pulling current tags don't see anything different, but the next attempt to re-use a tag fails fast.
Security Hub's ECR.2 control runs against AWS Config's recorded state for each ECR repository and re-evaluates within minutes of a configuration change. The fix is real-time visible — by the time you finish reading the Security Hub finding, the repo you just flipped will already show as compliant.
# The exact check ECR.2 performs — any MUTABLE repo fails the control.
aws ecr describe-repositories \
--query "repositories[?imageTagMutability=='MUTABLE'].repositoryName" \
--output text What is the impact of leaving tags mutable?
The primary impact is a supply-chain attack vector. An attacker with ecr:PutImage on a repo — stolen CI credentials, a compromised developer laptop, an over-permissive IAM role on a build server — can replace :v1.2.3 or :latest with a malicious image. Every workload pulling that tag on its next restart runs the attacker's bytes. Because the tag string didn't change, nothing in your deployment manifests, your audit records, or your monitoring suggests anything happened.
The second-order impact is forensic. After an incident you need to prove what code was running where, and when. With mutable tags you can't — the same label has pointed at multiple digests over time, and ECR doesn't keep an audit trail of who pushed which digest under which tag. With immutable tags every release is a permanent record: :v1.2.3 always means the digest it meant on Monday.
The regulatory impact is now explicit. The CIS Docker Benchmark, NIST SP 800-190 (Application Container Security), and the SLSA framework all call out tag immutability and digest pinning as baseline controls. SOC 2 and PCI auditors increasingly ask for evidence that container artefacts are immutable end-to-end — a MUTABLE ECR repo is the simplest way to fail that question.
The cost impact is small but real: a mutated tag leaves the old manifest in the repository (still billable by storage), and most teams never garbage-collect. Repos that have been overwritten on :latest weekly for two years routinely hold hundreds of orphan manifests. Lifecycle policies cleaning these up usually shave a small but non-zero amount off the storage line.
How do you safely enable tag immutability?
Flipping immutability is a four-step loop. The flag itself is one CLI call — the real work is making sure your build pipelines stop re-using tags before you turn it on.
1. Inventory which repos are still MUTABLE
Run aws ecr describe-repositories across every account and region and filter on imageTagMutability=='MUTABLE'. Multi-account orgs should script this via AWS Organizations and a cross-account read role — ECR is per-region, so a single account easily holds dozens of repos across us-east-1, eu-west-1, and friends.
2. Retire :latest from your build pipelines
Switch your CI to tag every image with an immutable identifier — a git commit SHA is the canonical choice (myapp:7e3f2a1), with optional semver (myapp:v1.2.3) for human-readable releases. :latest can still exist if your registry only pushes to it once and never re-tags. The discipline is: a tag is a name for one specific build, forever.
3. Flip the flag and pin downstream consumers to digests
Run aws ecr put-image-tag-mutability --image-tag-mutability IMMUTABLE on every cleared repo. For the highest-trust paths — production deploys, base images in your Dockerfiles — go one step further and pin to the digest (FROM myapp@sha256:...). Pair this with image signing (Notary v2 or Sigstore/cosign) so the digest can be cryptographically verified at pull time.
4. Prevent recurrence with AWS Config and SCPs
Enable the AWS Config managed rule ecr-private-tag-immutability-enabled to alert within minutes if any new ECR repo is created without immutability set. For prevention, attach a Service Control Policy that denies ecr:CreateRepository calls unless ecr:imageTagMutability=IMMUTABLE is specified — engineers can still create repos, but only immutable ones.
# Flip every MUTABLE repo in the current account/region to IMMUTABLE in one pass.
aws ecr describe-repositories \
--query "repositories[?imageTagMutability=='MUTABLE'].repositoryName" \
--output text \
| tr '\t' '\n' \
| while read repo; do
echo "Flipping $repo"
aws ecr put-image-tag-mutability \
--repository-name "$repo" \
--image-tag-mutability IMMUTABLE
done
# Verify the change took effect.
aws ecr describe-repositories \
--query "repositories[?imageTagMutability=='MUTABLE'].repositoryName" Quick quiz
Question 1 of 5You enable IMMUTABLE on an ECR repo whose CI still pushes :latest on every build. What happens to the next build?
You scored
0 / 5
Keep learning
Dig deeper into ECR hardening and container supply-chain security.
- AWS Security Hub control ECR.2 The exact rule definition, severity, and remediation guidance from AWS.
- Amazon ECR image tag mutability The service docs for the imageTagMutability setting and how it affects PutImage calls.
- NIST SP 800-190 — Application Container Security Guide Federal baseline for container security, including image provenance and tag discipline.
- SLSA — Supply-chain Levels for Software Artifacts Framework for end-to-end build provenance, with digest pinning and signing as core controls.
You've completed Enable ECR tag immutability. You can now inventory mutable ECR repositories across an account, retire :latest in favour of commit-SHA tags, flip immutability safely, and prevent regressions with AWS Config and SCPs. The next time Security Hub fires an ECR.2 finding, you'll have a four-step loop ready to run.
Back to the library