# Docker x509: certificate signed by unknown authority: fix

> docker pull or login fails with x509: certificate signed by unknown authority when the daemon does not trust the registry's CA. Use certs.d, not insecure mode.

Source: https://ssl.sh/errors/docker-x509-certificate-signed-by-unknown-authority/
Last updated: 2026-11-10

**`x509: certificate signed by unknown authority` from `docker pull`, `docker push` or `docker login` means the Docker daemon could not verify the registry's certificate against any root it trusts.** Usually the registry (Harbor, Nexus, GitLab, `registry:2`) uses an internal or self-signed CA, or a proxy re-signs the connection. Give the daemon that CA through `certs.d`.

Newer Docker versions print `tls: failed to verify certificate: x509: certificate signed by unknown authority`; it is the same error.

## Quick fix

On a Linux Docker host, trust the registry's CA for that registry only:

```bash
sudo mkdir -p /etc/docker/certs.d/registry.example.com
sudo cp registry-ca.crt /etc/docker/certs.d/registry.example.com/ca.crt
docker pull registry.example.com/team/app:latest
```

If the registry uses a port, the directory name includes it: `/etc/docker/certs.d/registry.example.com:5000/`. Files ending in `.crt` in that directory are treated as CA roots. If the pull still fails, restart the daemon with `sudo systemctl restart docker`.

To see which CA signed the registry certificate, run:

```bash
openssl s_client -connect registry.example.com:443 -servername registry.example.com -showcerts </dev/null | grep -E 's:|i:'
```

## Causes

### 1. Registry uses an internal or self-signed CA

An internal or self-signed CA is the default for most self-hosted registries.

- **Confirm:** the issuer is your company CA or the certificate is self-signed (`s:` equals `i:`).
- **Fix:** distribute the CA (not the server certificate, unless it is self-signed) via `certs.d` as above, or system-wide.

### 2. Corporate TLS-inspecting proxy

Pulls from Docker Hub or other public registries fail on office networks because the proxy presents its own certificate.

- **Confirm:** `docker pull hello-world` fails too; the issuer is the proxy.
- **Fix:** add the proxy root to the host's system trust store and restart Docker:

```bash
sudo cp proxy-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
sudo systemctl restart docker
```

On Linux, Docker merges `certs.d` roots with the system roots, so this covers every registry.

### 3. Registry missing its intermediate certificate

A registry behind Nginx that sends only the leaf certificate fails in Docker (written in Go, which does not fetch intermediates) while browsers work. See [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/); the [chain builder](https://ssl.sh/tools/chain-builder/) produces the full chain file.

- **Confirm:** `openssl s_client -showcerts` lists only one certificate.
- **Fix:** serve the full chain on the registry; check with the [chain builder](https://ssl.sh/tools/chain-builder/).

### 4. Certificate without SANs

If the message instead says `relies on legacy Common Name field`, the registry certificate lacks SANs. See [legacy Common Name](https://ssl.sh/errors/go-x509-certificate-relies-on-legacy-common-name/).

## Fixes by environment

Every runtime needs the registry's CA; only the place where it reads that CA differs.

**Rootless Docker:** the certificate root is `$XDG_CONFIG_HOME/docker/certs.d` (by default `~/.config/docker/certs.d`), not `/etc/docker/certs.d`.

**Docker Desktop (macOS/Windows):** Docker Desktop can use CAs from the host's certificate store. Add the root to the macOS System keychain or the Windows "Trusted Root Certification Authorities" store, then restart Docker Desktop. Per-registry client certificates go in `~/.docker/certs.d` on the host.

**Windows containers on Windows Server:** use `%PROGRAMDATA%\docker\certs.d\registry.example.com\ca.crt`; drop the colon if the registry has a port.

**Kubernetes nodes with containerd:** the kubelet pulls through containerd, which does not read Docker's `certs.d`. Configure the registry CA in containerd's own registry host configuration (`hosts.toml` under the directory set by `config_path`), then restart containerd.

**Podman:** uses `/etc/containers/certs.d/<registry>/ca.crt` with the same layout.

**Inside `docker build`:** each image has its own trust store; for proxies, add the root in the Dockerfile:

```dockerfile
COPY proxy-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
```

## Don't do this

- **`"insecure-registries": ["registry.example.com"]` in `daemon.json`.** It turns off certificate verification for that registry, so a spoofed registry can hand you a backdoored image that you then run with your production secrets.
- **Copying the server certificate into `certs.d` instead of the CA.** It breaks again as soon as the registry certificate is renewed.
- **Disabling TLS on the registry** and serving plain HTTP inside the company network. Internal networks are where image tampering is easiest.
