• Go

x509: certificate signed by unknown authority

Also seen in: Docker, Kubernetes (kubectl), Helm, Terraform, containerd, any Go program

In one sentence

A Go program, such as Docker or kubectl, could not link the server's certificate to a trusted root: a private or proxy CA, a missing intermediate, or no CA bundle.

Where you see it

In any program written in Go: Docker, kubectl, Helm, Terraform, containerd and many CLIs. The text is the UnknownAuthorityError of Go’s crypto/x509 package. Programs built with Go 1.20 or later add the prefix tls: failed to verify certificate:; older builds print the message alone. Each tool adds its own context in front, for example Docker:

Error response from daemon: Get "https://registry.example.com:5000/v2/": tls: failed to verify certificate: x509: certificate signed by unknown authority

and kubectl, in this example from the Kubernetes troubleshooting guide:

Unable to connect to the server: x509: certificate signed by unknown authority (possibly because of "crypto/rsa: verification error" while trying to verify candidate authority certificate "kubernetes")

The part in brackets is an optional hint (Go source): Go found a CA certificate with the right name, but its key does not match. That typically happens after a cluster or private CA was rebuilt; replace the old CA certificate in your store or kubeconfig with the new one.

What it means

Go builds a certificate chain from the server’s certificate to a root certificate in its trust store. On Linux it reads the system CA bundle file; on Windows and macOS it asks the operating system. It found no trusted root for this chain. On Linux, Go does not download a missing intermediate, so a chain that works in a browser can still fail here.

Likely causes

A private CA or TLS-inspection proxy root is not trusted

Internal registries, Kubernetes API servers and Git servers often use a company CA, and a TLS-inspection proxy re-signs public sites. See who issued the certificate:

openssl s_client -connect registry.example.com:5000 -servername registry.example.com -showcerts </dev/null 2>/dev/null | grep -E '^ [0-9]+ s:|^   i:'

Fix on a Linux host: add the root to the system store, which Go reads:

# Debian, Ubuntu and Alpine (the file must end in .crt)
sudo cp corp-root.pem /usr/local/share/ca-certificates/corp-root.crt
sudo update-ca-certificates

# RHEL, Fedora and similar
sudo cp corp-root.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract

On Linux, Go reads the first bundle it finds, such as /etc/ssl/certs/ca-certificates.crt or /etc/pki/tls/certs/ca-bundle.crt, and the SSL_CERT_FILE and SSL_CERT_DIR environment variables override where it looks. From Go 1.27 these variables also work on Windows and macOS, where they replace the operating system’s verifier.

Docker daemon and a private registry: put the registry’s CA in a folder named after the registry host and port, as described in Docker’s registry certificate documentation. On Linux the folder is under /etc/docker/certs.d/; Docker Desktop uses ~/.docker/certs.d/ on the host:

sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000
sudo cp corp-root.pem /etc/docker/certs.d/registry.example.com:5000/ca.crt

The daemon treats every .crt file in that folder as a CA root. If you use the registry without a port number, leave the port out of the folder name too.

Your container image has no CA certificates

Images built FROM scratch, and some minimal images, contain no CA bundle at all, so every HTTPS call fails. Copy one in:

FROM alpine:3.20 AS certs
RUN apk add --no-cache ca-certificates

FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY app /app
ENTRYPOINT ["/app"]

To trust a company root inside Kubernetes pods, mount a bundle that contains the public roots and your root (for example from a ConfigMap) and point SSL_CERT_FILE at it:

env:
  - name: SSL_CERT_FILE
    value: /etc/ssl/custom/ca-bundle.crt
volumeMounts:
  - name: ca-bundle
    mountPath: /etc/ssl/custom
    readOnly: true
# in the pod spec
volumes:
  - name: ca-bundle
    configMap:
      name: ca-bundle   # holds the key ca-bundle.crt

kubectl has an outdated cluster CA

When your kubeconfig contains a cluster CA (certificate-authority-data), kubectl trusts only that CA, not the system store. After a cluster is rebuilt or its CA rotated, the old CA no longer matches. Compare the two:

kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d | openssl x509 -noout -subject -enddate
openssl s_client -connect api.cluster.example.com:6443 -showcerts </dev/null 2>/dev/null | grep -E '^ [0-9]+ s:|^   i:'

Fix: download a fresh kubeconfig from your provider or cluster administrator. If a TLS-inspection proxy sits in the way, ask for the API server to be excluded from inspection.

The server leaves out its intermediate

Count the certificates the server sends (grep -c 'BEGIN CERTIFICATE' on the -showcerts output). If it is 1, configure the server with its full chain: see verify error:num=21.

Check yours

Try it: Chain checkerOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere.

Every certificate the server sends, leaf first. The output of openssl s_client -showcerts -connect example.com:443 works as it is: we ignore the text around the certificates.

Paste the certificates from openssl s_client -showcerts. The checker shows the chain, its root, and any missing link.

Learn why

In the gameLevel 12: Who do we trust?Registry of Roots, Keystone. In development

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9