Skip to main content
RunBook Academy

Docker & ContainersXII Β· Supply ChainRegistry trust

Registry trust β€” pulling from who you think you are

Intermediate⏱ ~22 mindockeropenssl

What you'll learn

  • Trace the six steps of a pull and name what each one authenticates
  • Diagnose a trust failure to the specific link that broke
  • Trust a private CA correctly instead of disabling verification
  • Explain why content addressing makes transport trust replaceable

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12

Not yet marked complete on this device.

When you docker pull nginx, six things happen in order:

  1. registry-1.docker.io is resolved to an address via DNS.
  2. A TLS connection is established.
  3. The certificate chain is validated β€” trusted issuer, hostname in the SAN, not expired.
  4. Authentication, if required: a token is obtained and presented.
  5. The manifest is requested.
  6. Each layer blob is downloaded.

Each step authenticates something different, and the single most useful thing to internalise is that steps 2 and 3 authenticate the server, not the image. A perfectly valid certificate on a registry that is serving a backdoored image passes every TLS check there is.

What each control actually protects

StepProtected byAgainst
DNS resolutionNothing, on its ownβ€”
TLS handshakeCertificate validationTalking to the wrong server
AuthenticationCredentials and scoped tokensUnauthorised push and pull
Manifest fetchThe digest, if you pinned oneGetting a different artefact
Layer downloadContent addressingCorrupted or substituted bytes
The image itselfA signatureAn artefact your publisher did not produce

DNS is worth a line of its own. Cache poisoning, a compromised resolver or a BGP hijack can point your pull at an attacker’s server. That is a real attack and TLS is the reason it usually fails: the attacker reaches step 2 and cannot produce a certificate your daemon accepts for that hostname.

Usually. It fails unless somebody has widened the trust store, added the host to insecure-registries, or the attacker has obtained a certificate a public CA will issue for a name they control.

Diagnosing a trust failure

The errors look similar in a pipeline log and mean very different things. Work out which link broke before you change anything.

Read-only / Safex509: certificate signed by unknown authority
$ docker pull registry.example.com/myorg/myapp:1.4.0
Error response from daemon: Get "https://registry.example.com/v2/": x509: certificate signed by unknown authority

Illustrative output

That is a private CA the host does not know about, or an intercepting proxy. It is not a network problem and it is not the registry being down.

Read-only / Safex509: certificate is valid for ...
$ docker pull registry.example.com/myorg/myapp:1.4.0
Error response from daemon: Get "https://registry.example.com/v2/": x509: certificate is valid for registry.internal.example.com, not registry.example.com

Illustrative output

Usually a load balancer terminating TLS with the wrong certificate, or a CNAME added without reissuing. Occasionally it is the interesting case.

Read-only / Safesee the certificate the registry is actually serving
REGISTRY=registry.example.com

openssl s_client -connect "${REGISTRY}:443" -servername "$REGISTRY" </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates -ext subjectAltName
issuer=C=US, O=Let's Encrypt, CN=R11
subject=CN=registry.example.com
notBefore=Jul 14 08:11:02 2026 GMT
notAfter=Oct 12 08:11:01 2026 GMT
X509v3 Subject Alternative Name:
  DNS:registry.example.com

Illustrative output

The -servername flag matters: a registry behind a shared load balancer selects its certificate by SNI, and omitting it gets you the default certificate rather than the one your daemon will see.

Read-only / Safeconfirm where the name resolves
REGISTRY=registry.example.com

getent hosts "$REGISTRY"
docker run --rm --network host busybox nslookup "$REGISTRY" 2>/dev/null | tail -5
203.0.113.40    registry.example.com

Illustrative output

A registry that resolves differently inside a container than on the host is the signature of a split-horizon DNS setup or a container DNS override, and it produces failures that look like intermittent registry outages.

Trusting a private CA

The correct fix for a certificate your daemon does not recognise is to teach it the CA. The daemon looks for CA material in a directory named for the registry β€” the documentation gives the layout directly: β€œa copy of its CA certificate is placed on the Docker host at /etc/docker/certs.d/myregistry:5000/ca.crt.”

Configuration changetrust a private CA for one registry
REGISTRY=registry.example.com

sudo mkdir -p "/etc/docker/certs.d/${REGISTRY}"
sudo cp internal-ca.crt "/etc/docker/certs.d/${REGISTRY}/ca.crt"
sudo chmod 0644 "/etc/docker/certs.d/${REGISTRY}/ca.crt"

docker pull "${REGISTRY}/myorg/myapp:1.4.0"

If the registry listens on a non-standard port, the directory name includes the port, exactly as in the documented example.

Mirrors and the trust they inherit

A registry-mirrors entry redirects Docker Hub pulls through a server you nominate. That server now sees every pull you make and is in a position to answer them.

Read-only / Safewhat your daemon has been told to trust
docker info --format 'mirrors: {{.RegistryConfig.Mirrors}}'
mirrors: [https://hub-cache.example.com/]

Illustrative output

The mirror is a genuine trust dependency: an operator of the mirror can serve different content for a tag, and nothing in the pull command or the resulting docker images output would show it.

And here is the payoff of everything in this part of the course: pin by digest and that stops being true. Content addressing means the daemon verifies the bytes it received against the digest it asked for. A hostile mirror can refuse to serve you, and it cannot substitute. Trust in the transport becomes replaceable by trust in the content, which is a much smaller thing to have to defend.

Docker Content Trust is going away

Older guidance recommends DOCKER_CONTENT_TRUST=1, which enables Notary v1 signature verification at pull time.

That mechanism is being retired. The documentation states: β€œDocker Content Trust (DCT) is being retired. The Notary v1 service at notary.docker.io will shut down on December 8, 2026.”

If you have DOCKER_CONTENT_TRUST=1 set anywhere β€” a CI image, a profile script, a systemd unit β€” find it now rather than on the day it starts failing:

Read-only / Safefind Content Trust settings
printenv DOCKER_CONTENT_TRUST || echo 'not set in this shell'
grep -R 'DOCKER_CONTENT_TRUST' /etc/environment /etc/profile.d/ /etc/systemd/system/ 2>/dev/null \
|| echo 'not set persistently'
not set in this shell
not set persistently

The replacement is Cosign signatures verified at deploy time, covered in the signing lesson. There is no drop-in daemon-level equivalent β€” enforcement moves into your admission controller or your deploy tooling, which is a real change in where the control lives.

Public registries and the names you type

Docker Hub is a public registry. Anyone can push to it. The library/* namespace holds Docker Official Images, reviewed by Docker; everything else is whoever registered the name.

The risks are less about cryptography than about typing:

  • Typosquatting. A name one character away from a popular image, published by somebody else, waiting for a Dockerfile typo.
  • The implicit library/ prefix. docker pull alpine is an official image. docker pull someuser/alpine is not, and the two differ by an easily overlooked prefix in a review diff.
  • Namespace transfer. A repository that was trustworthy for years can change hands. Nothing about the reference changes.
  • Abandonment. Popularity is not maintenance, and a download count is a measure of the past.

None of those are defeated by TLS, by authentication, or by pulling from a mirror. They are defeated by pinning a digest you evaluated and by verifying a signature from a publisher you named.

Vendor registries

Cloud registries β€” AWS ECR, Google Artifact Registry, Azure Container Registry β€” authenticate through the cloud provider’s IAM rather than through a registry-local account.

Configuration changeECR login
REGION=us-east-1
ACCOUNT=123456789012
REGISTRY="${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com"

aws ecr get-login-password --region "$REGION" \
| docker login --username AWS --password-stdin "$REGISTRY"
Login Succeeded

Illustrative output

This is a genuine improvement β€” the credential on the host is an instance role rather than a password, and it rotates automatically. It also moves the interesting question elsewhere: the audit is now of IAM policy, not of a registry user list, and the common finding is a role with pull permission scoped far more broadly than anybody intended.

  1. Fully qualify registry hostnames in anything that is not an interactive shell, so a reference means one thing everywhere.
  2. **Trust private CAs through /etc/docker/certs.d/**, never through insecure-registries.
  3. **Audit every host for insecure-registries and unexpected registry-mirrors** β€” both are invisible in the image reference.
  4. Pin production references by digest, which makes transport integrity non-load-bearing.
  5. Verify a signature from a named publisher identity before deploying, since that is the only control that survives a compromised registry.
  6. **Retire DOCKER_CONTENT_TRUST usage** ahead of the Notary v1 shutdown rather than after it.

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. An attacker with push access publishes a backdoored image to your own registry, and you pull it over a valid TLS connection with correct authentication. Which control would have stopped you deploying it?

  2. Q2. Why does an image referenced by digest make transport integrity non-load-bearing, while a tag reference does not?

  3. Q3. Which are accurate about `insecure-registries` in daemon.json? Select all that apply.

  4. Q4. `registry-auths` is the daemon.json key for storing registry credentials on the host.

  5. Q5. Docker Content Trust is being retired, and there is no drop-in daemon-level replacement for pull-time signature enforcement.

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