- 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 /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
Paste the certificates from openssl s_client -showcerts. The checker shows the chain, its root, and any missing link.
Learn why
- Who do we trust? CAs, trust stores and self-signed certificates
- Certificate chains and path validation
- A CA of your own: private two-tier PKI
- See also: x509: certificate has expired or is not yet valid.
Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.