Error message
x509: certificate signed by unknown authority
Go crypto/x509 — returned by Certificate.Verify and TLS dials
What it means
Go tried to build a chain from the server's certificate to a root it trusts and failed: no path ends at a certificate in the configured root pool. The certificate may be perfectly valid — Go just has no reason to trust its issuer. This is the single most-hit TLS error in Go deployments.
Why it happens
Server does not send the intermediate certificate
commonRoots in trust stores sign intermediates, intermediates sign leaves. A server configured with only the leaf (cert.pem instead of fullchain.pem) leaves Go unable to bridge leaf → root. Browsers often paper over this via cached intermediates; Go does not.
Internal/private CA not in the pool
commonCorporate CAs, self-signed development certificates, and mesh-issued certs are unknown to the system store by design. The client must be told about them explicitly.
Minimal container image without CA certificates
commonscratch and slim images ship no /etc/ssl/certs. Every TLS dial then fails with this error regardless of how public and valid the target's certificate is.
Intercepting proxy re-signs traffic
occasionalCorporate TLS-inspection middleboxes replace certificates with ones signed by the company CA. Machines without that CA installed see every external site as untrusted.
How to fix it
- 1.
Add the CA to the client's root pool
Extend the system pool rather than replacing it, so public sites keep working alongside your internal CA.
go roots, err := x509.SystemCertPool() if err != nil { roots = x509.NewCertPool() } pem, _ := os.ReadFile("internal-ca.pem") roots.AppendCertsFromPEM(pem) client := &http.Client{Transport: &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: roots}, }} - 2.
Fix the server: serve the full chain
Check what the server actually sends; if only one certificate appears, point the server at the full-chain file (for example Let's Encrypt's fullchain.pem).
bash openssl s_client -connect api.example.com:443 -showcerts </dev/null \ | grep -c "BEGIN CERTIFICATE" # 1 = leaf only (broken), 2+ = chain present - 3.
Ship CA certs in the image
For minimal images, copy a CA bundle in at build time. Never "solve" this with InsecureSkipVerify — that disables verification entirely.
docker FROM alpine AS certs RUN apk add --no-cache ca-certificates FROM scratch COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/