Filter concepts by levelShowing all levels.

AWS · Section 42

ECR and Container Supply Chain

Level
intermediate
Read
25 min
Concepts
3

ECR is a managed private container registry fronted by IAM. Because the Docker CLI does not speak IAM, authentication works by exchanging your credentials for an authorization token — scoped to the requesting principal and valid for twelve hours — and passing it to `docker login`. Inside a repository, a tag is a mutable pointer and a digest names specific bytes, which is why tag immutability should be enabled and why anything that deploys should reference a digest: without it, two tasks in the same service can run different code under the same version number. Repositories grow indefinitely unless a lifecycle policy expires images, and the usual bulk is untagged images orphaned by repushes — but aggressive retention rules can delete the image a rollback needs, so release tags deserve longer retention than build churn. Promotion between environments means deploying the same digest onward rather than rebuilding per environment. The rest is supply-chain hygiene: a pull-through cache and managed signing give layers a known origin, a minimal base image removes hundreds of packages and the CVEs that come with them, scan-on-push catches known vulnerabilities at build time while regular rebuilds catch the ones published since, and a non-root `USER` limits what an exploited process can do.

What is true here

  1. Authentication is a 12-hour authorization token scoped to the IAM principal, not a stored password.
  2. Tags are mutable pointers; enable tag immutability and deploy by digest.
  3. Lifecycle policies bound repository growth — but must not expire an image you may need to roll back to.
  4. Promotion deploys the same digest to the next environment; rebuilding per environment breaks the guarantee.
  5. Minimal base + scan on push + regular rebuilds + non-root user are four cheap, independent controls.

What you will be able to do

  • Authenticate Docker to a private ECR registry and explain why the credential is short-lived
  • Explain the difference between a tag and a digest, and configure tag immutability
  • Write lifecycle rules that bound growth without breaking rollback
  • Promote one image through environments, including across accounts
  • Harden an image: minimal base, multi-stage build, scanning, and a non-root runtime user
From a pushed image to one you can trust in production
managed bytrustedbecause of

Repositories, tags, authentication

Lifecycle policies and promotion

Provenance, minimal base, scanning, non-root

  • Repositories, tags, authentication
    • leads to Lifecycle policies and promotion (managed by)
  • Lifecycle policies and promotion
    • leads to Provenance, minimal base, scanning, non-root (trusted because of)
  • Provenance, minimal base, scanning, non-root

ECR and Container Supply Chain

The registry object model and authentication, lifecycle and promotion, and the four controls that make an image trustworthy.

ECR Repositories, Tags, and Authentication

coreintermediate

ECR is a managed private container registry. Images live in repositories, and access is controlled with IAM — both identity policies and a resource-based repository policy. The Docker CLI does not speak IAM, so authentication works by exchanging your IAM credentials for a registry authorization token and passing it to `docker login`.

Think of it as

ECR is S3 for container images: an AWS resource with IAM in front of it, plus a translation layer for a client that only understands usernames and passwords. `get-login-password` is that translation — a temporary password minted from your IAM identity.

What we're doing: See what tag immutability actually prevents.

mutable-tag.txttext
Repository without tag immutability:
  09:00 push api:v2.4.1  -> digest sha256:9f2c...
  14:00 push api:v2.4.1  -> digest sha256:41be...  (accepted silently)

Two tasks in the same service now run different code: the ones started
before 14:00 pulled 9f2c, the ones started after pulled 41be. Both
report "running v2.4.1".

With tag immutability enabled, the 14:00 push is rejected.
1
Nothing about the second push looks unusual — repushing a tag after a small fix is a very common habit.
5
The service is healthy, the tag is right, and the fleet is inconsistent. Nothing in the console shows a discrepancy, because the console shows the tag.

Why this works: A version number that can point at two different builds makes every other traceability effort meaningless — the running task, the CI record, and the git commit no longer agree. Tag immutability makes the registry enforce what everyone already assumes.

Baking registry credentials into an image or a CI secret

Wrong

text
# Store a long-lived docker login credential as a CI variable

Better

text
# Assume a role (OIDC for external CI, task role on AWS) and call
# get-login-password at build time — the token lasts 12 hours

What you see: The credential outlives the person who created it, works from anywhere, and is discovered during an audit rather than being rotated on schedule.

Why: The authorization token is deliberately short-lived and scoped to the IAM principal that requested it, which means the durable credential is the IAM role, not a password. Storing a password reintroduces exactly the long-lived secret the token model exists to avoid.

From IAM identity to a pushed image
GetAuthorizationTokenget-login-passwordpush / pullstores as

IAM principal

role or user

Authorization token

valid 12 hours, same scope as the principal

docker login

username AWS, token as password

ECR repository

IAM + repository policy

Image + digest

sha256:… — immutable

  • IAM principal — role or user
    • leads to Authorization token (GetAuthorizationToken)
  • Authorization token — valid 12 hours, same scope as the principal
    • leads to docker login (get-login-password)
  • docker login — username AWS, token as password
    • leads to ECR repository (push / pull)
  • ECR repository — IAM + repository policy
    • leads to Image + digest (stores as)
  • Image + digest — sha256:… — immutable

Tag or digest?

Tag or digest?
ReferenceResolves toUse it for
`api:latest`Whatever was pushed lastLocal development only
`api:v2.4.1`That tag, unless someone repushes itHuman-readable release naming
`api:<git-sha>`One commit's build, if tags are immutableTraceability from running task to source
`api@sha256:9f2c…`Exactly those bytes, alwaysAnything a deployment or task definition references

Together

text
aws ecr get-login-password --region eu-west-1 \
  | docker login --username AWS --password-stdin 111122223333.dkr.ecr.eu-west-1.amazonaws.com

docker build -t 111122223333.dkr.ecr.eu-west-1.amazonaws.com/api:$GIT_SHA .
docker push 111122223333.dkr.ecr.eu-west-1.amazonaws.com/api:$GIT_SHA

Remember: ECR = private registry with IAM in front. `get-login-password` mints a 12-hour token scoped to your principal. Tags are mutable pointers; digests are not. Turn on tag immutability, and reference images by digest everywhere a deployment does.

See also: lifecycle policies and promotion · image provenance and hardening · pipeline stages and artifacts

Lifecycle Policies and Promotion Between Environments

standardintermediate

A repository accumulates an image per build forever unless you tell it not to. A lifecycle policy is a set of rules that expire images automatically — keep the last N, expire untagged images after N days. Promotion is the other half: moving one already-built image through environments rather than building a separate image for each.

Think of it as

Think of the registry as a warehouse with a standing disposal rule, and promotion as a shipping label rather than a manufacturing step. Staging and production receive the same crate; only the label changes.

text
# Promotion is a deploy target change, not a build
build  -> api@sha256:9f2c...
staging: deploy api@sha256:9f2c...
prod   : deploy api@sha256:9f2c...   # the same image, promoted

Writing a lifecycle rule that can expire an image production is running

Wrong

text
# "Keep only the 5 most recent images" — applied to every tag prefix

Better

text
# Scope aggressive rules to build/branch tag prefixes, and keep release
# tags long enough to roll back to any version still deployable

What you see: A rollback fails because the previous release's image no longer exists in the registry, and the only path back is a rebuild from an old commit.

Why: Lifecycle rules select on recency, not on what is deployed — the registry has no knowledge of which digests are running. Rollback depends on the old image still existing, so retention has to be at least as long as the window in which you might want to go back.

Lifecycle rules worth having

Lifecycle rules worth having
RuleSelectionEffect
Expire untaggedUntagged images older than 7 daysRemoves the orphans left by repushes
Keep recent buildsTag prefix `sha-`, keep the most recent 30Bounds the day-to-day build churn
Keep releases longerTag prefix `v`, keep the most recent 100Old releases stay redeployable for rollback
Expire feature branchesTag prefix `pr-`, older than 14 daysBranch builds do not need to outlive the branch

Together

json
{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Expire untagged images after 7 days",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 7
      },
      "action": { "type": "expire" }
    }
  ]
}

Remember: Lifecycle policies are the only thing that bounds a repository's growth — start with expiring untagged images, then scope retention by tag prefix so releases outlive build churn. Promotion means deploying the same digest onward, never rebuilding per environment.

See also: ecr repositories tags and authentication · pipeline stages and artifacts

Provenance, Minimal Base Images, Scanning, and Non-Root

coreintermediate

A container image is code you did not write, shipped alongside code you did. Provenance is knowing where each layer came from. A minimal base image reduces how much of that unwritten code exists at all. Scanning tells you which of it has known vulnerabilities. Running as a non-root user limits what an exploit can do once it is inside.

Think of it as

Four controls at four points: what you start from, how much of it you keep, what you know about it, and what it can do if compromised. Each is cheap on its own, and the last one — non-root — is a one-line change that many images still get wrong.

What we're doing: Understand why "we scan our images" is not the same as "our images are clean".

scan-freshness.txttext
March: image built from a full OS base, scanned on push, 0 critical
       findings. Deployed to production.

September: the same image is still running. Six critical CVEs have been
published since March affecting packages in that base image — none of
which the application uses, all of which are present.

The scan result in the console still says what it said in March, unless
continuous rescanning is enabled.
1
A scan on push is a statement about the vulnerability database on that day, not a durable property of the image.
5
This is where a minimal base pays off twice: fewer packages means fewer of these, and the ones that do appear are more likely to be relevant.

Why this works: Scanning has a shelf life and images do not expire on their own. The two habits that actually keep a fleet current are rebuilding regularly — so base image updates are picked up — and keeping the base small enough that most published CVEs simply do not apply.

Passing a secret as a build argument

Wrong

text
ARG NPM_TOKEN
RUN npm ci && rm -f ~/.npmrc

Better

text
# Use a build secret mount, or install dependencies in a build stage
# that is discarded:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

What you see: The token is recoverable from the image layers by anyone who can pull it, despite the file having been deleted in the same RUN line or a later one.

Why: Each instruction produces a layer, and layers are additive — deleting a file in a later layer hides it from the merged filesystem but leaves it in the layer that added it. Build arguments are also recorded in the image history. Only never writing the secret into a retained layer removes it.

Four controls, four points in the chain

Provenance

Pull-through cache + signing — you know where each layer came from

Minimal base

Fewer packages means fewer CVEs and less attack surface

Scanning

Scan on push, and rescan as new CVEs are published

Non-root runtime

Limits what an exploited process can reach

  1. Provenance — Pull-through cache + signing — you know where each layer came from
  2. Minimal base — Fewer packages means fewer CVEs and less attack surface
  3. Scanning — Scan on push, and rescan as new CVEs are published
  4. Non-root runtime — Limits what an exploited process can reach

What each control actually prevents

What each control actually prevents
ControlPreventsDoes not help with
Pull-through cacheA public base image disappearing or being repointedVulnerabilities already in that image
Minimal base imageHundreds of CVEs in packages you never useVulnerabilities in your own dependencies
Scan on pushShipping a known-vulnerable image todayCVEs published after the scan
Non-root userAn exploited process modifying the filesystem or escalatingThe exploit itself
Multi-stage buildBuild secrets and toolchains surviving into the runtime imageSecrets injected at runtime

Together

text
# Multi-stage: the build toolchain and any build secrets stay behind
FROM python:3.13-slim AS build
WORKDIR /src
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt

FROM python:3.13-slim
RUN useradd --create-home --uid 10001 app
COPY --from=build /install /usr/local
COPY --chown=app:app . /app
USER app
CMD ["python", "/app/main.py"]

Remember: Know where the base image came from (pull-through cache, signing), keep it small, scan on push and keep rescanning as CVEs are published, and run as a non-root user. Build secrets survive in layers unless a multi-stage build or a secret mount keeps them out.

See also: ecr repositories tags and authentication · security services landscape · secrets manager and parameter store

Advertisement