Skip to content
Latchkey

Docker "x509: certificate signed by unknown authority" - Fix Registry TLS

The Docker daemon could not verify the registry’s TLS certificate because the signing CA is not in the runner’s trust store. Common with self-hosted or corporate registries using a private CA.

What this error means

A docker pull/push against a private registry fails during the TLS handshake with x509: certificate signed by unknown authority. Public registries work; only the private host fails - and it fails the same way every run until trust is fixed.

docker pull/push output
Error response from daemon: Get "https://registry.internal:5000/v2/":
x509: certificate signed by unknown authority

Diagnose it: separate auth from naming from rate limits

Registry errors look alike and have unrelated causes. Work out which of the three you have before changing credentials, because a malformed image reference produces an error that reads like an authentication failure.

Terminal
# 1. is the reference even valid? (lowercase, no spaces, valid tag)
docker image inspect "$IMAGE" 2>&1 | head -2

# 2. are you authenticated to the right registry?
cat ~/.docker/config.json | grep -o '"[^"]*\.[^"]*"' | head

# 3. are you rate limited? (Docker Hub anonymous pulls)
curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimit-preview/test:pull" \
  | grep -o '"token"' >/dev/null && echo "token ok"

Common causes

Registry uses a self-signed or private-CA certificate

A self-hosted registry signed by an internal CA presents a certificate the runner has never seen. Without the CA in the trust store, verification fails.

The CA bundle is missing on a minimal runner

A slim runner image without up-to-date ca-certificates may not trust even a publicly-signed registry, surfacing the same x509 error.

How to fix it

Install the registry’s CA for the daemon

Place the CA certificate where the Docker daemon looks for per-registry certs, then restart the daemon.

Terminal
sudo mkdir -p /etc/docker/certs.d/registry.internal:5000
sudo cp ca.crt /etc/docker/certs.d/registry.internal:5000/ca.crt
sudo systemctl restart docker

Add the CA to the system trust store

For tools beyond the daemon, trust the CA system-wide.

Terminal
sudo cp corporate-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

Authenticate in the job, not in the image

.github/workflows/ci.yml
- uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

# GHCR needs this on the job or the push is rejected as unauthorised
permissions:
  contents: read
  packages: write

How to prevent it

  • Bake the private registry’s CA into the runner image or trust store.
  • Keep ca-certificates current on runner images.
  • Prefer per-registry certs.d trust over a global insecure-registry setting.

Frequently asked questions

What causes Docker "x509: certificate signed by unknown authority"?
There are 2 common causes: registry uses a self-signed or private-ca certificate and the ca bundle is missing on a minimal runner. A self-hosted registry signed by an internal CA presents a certificate the runner has never seen.
How do I fix Docker "x509: certificate signed by unknown authority"?
There are 2 fixes depending on which cause you have: install the registry’s ca for the daemon and add the ca to the system trust store. Work through them in order, since the first is the most common.
What does Docker "x509: certificate signed by unknown authority" actually mean?
A docker pull/push against a private registry fails during the TLS handshake with x509: certificate signed by unknown authority.
How do I stop Docker "x509: certificate signed by unknown authority" happening again?
Bake the private registry’s CA into the runner image or trust store. The prevention section lists 3 changes that keep it from recurring.
Can Latchkey fix this automatically?
Yes. Latchkey runs your GitHub Actions on managed runners that detect this failure, apply the fix, and retry the job automatically - self-healing is on by default.

Related guides

References

This is a transient network failure, not a bug in your code. Latchkey detects, repairs, and retries it for you. Start free → 30-day trial · No credit card