Home/Security/Fixing x509: certificate signed by unknown authority — Copy-paste runbook for Docker, Go, k8s, and git
SecurityDocker certs.dGo SSL_CERT_FILE

Fixing x509: certificate signed by unknown authority — Copy-paste runbook for Docker, Go, k8s, and git

A copy-paste runbook that matches the 'x509: certificate signed by unknown authority' error verbatim across Docker, Go, Kubernetes, and git so you can diagnose and recover in five minutes. Also covers how to trust a private CA safely withou

Fixing x509: certificate signed by unknown authority — Copy-paste runbook for Docker, Go, k8s, and git
Contents
  1. Why the same certificate works in the browser and curl but fails in docker and go
  2. Error text verbatim matching table
  3. 5-minute diagnosis flow: four-way split
  4. Copy-paste fixes by cause
  5. ⛔ Never do this
  6. Verification checklist
  7. Preventing recurrence: bake it in and deploy at org scale
  8. Reference: official docs
  9. FAQ

Fixing x509: certificate signed by unknown authority — Copy-paste runbook for Docker, Go, k8s, and git

Why the same certificate works in the browser and curl but fails in docker and go

If you run a private CA or a self-signed certificate internally, you will hit this. The browser loads the site fine, curl succeeds, but docker pull, go run, and kubectl spit this out:

CODE
x509: certificate signed by unknown authority

The cause is simple. Each runtime trusts a different trust store. Browsers use their own certificate store; the OS curl uses the system CA bundle. The Docker daemon does not look at the system CA — it looks at /etc/docker/certs.d/. Go binaries may not see the system store at all depending on the build environment, and kubectl looks at the CA inside kubeconfig. Installing the CA in one place does not fix everything.

This post focuses on the unknown authority family — the internal CA is not registered in that runtime's trust store. For the unable to get local issuer certificate (curl/openssl handshake) family of pre-checks, see the [existing SSL handshake runbook]; here we only cover the commands you actually need for each branch.

Error text verbatim matching table

Stop scrolling and find the exact error line you saw.

CommandActual output (verbatim)What to fix
docker pullx509: certificate signed by unknown authority/etc/docker/certs.d/<registry:port>/ca.crt
go runtls: failed to verify certificate: x509: certificate signed by unknown authoritySystem trust store or SSL_CERT_FILE
git clonefatal: unable to access ...: SSL certificate problem: self-signed certificate in certificate chaingit config http.sslCAInfo
kubectlUnable to connect to the server: x509: certificate signed by unknown authoritykubeconfig certificate-authority(-data)

Key point: The Docker daemon does not look at the system CA. The most common trap is docker pull still failing after you only ran update-ca-certificates.

5-minute diagnosis flow: four-way split

Before the copy-paste fix, confirm in 30 seconds that the problem really is an untrusted CA.

Bash
# 1) check
openssl s_client -connect myregistry.local:5000 -showcerts </dev/null

Inspect the CA file (ca.crt) you just captured and split on the result.

Bash
openssl x509 -in ca.crt -noout -issuer -subject -dates
  • issuer == subject → self-signed certificate. Trust this CA itself.
  • issuer != subject → certificate issued by a private CA. You must trust the root CA at the top of the chain.
  • notAfter is in the past → expired. The answer is certificate renewal, not trust registration.
  • Connection works but hostname verification fails → SNI/CN mismatch. Confirm with -servername.
Bash
openssl s_client -connect myregistry.local:5000 -servername myregistry.local </dev/null

Most cases are the first two branches (self-signed / private CA). Move on to the fixes below.

Copy-paste fixes by cause

OS system trust store (the foundation for Go and git)

Debian/Ubuntu:

Bash
sudo cp myca.crt /usr/local/share/ca-certificates/myca.crt
sudo update-ca-certificates

RHEL/CentOS/Fedora:

Bash
sudo cp myca.crt /etc/pki/ca-trust/source/anchors/myca.crt
sudo update-ca-trust extract

Note: On Debian-family distros the extension must be .crt or it will not be recognized.

Docker daemon: certs.d is the answer

The Docker daemon does not look at the system CA; it looks at per-registry directories. The directory name must include the port.

Bash
sudo mkdir -p /etc/docker/certs.d/myregistry.local:5000
sudo cp myca.crt /etc/docker/certs.d/myregistry.local:5000/ca.crt
sudo systemctl restart docker

If you omit the port from a name like myregistry.local:5000, it will not apply — double-check this.

Go runtime: distroless is the trap

If you build directly on a Linux host, updating the system store is enough. In distroless/scratch base containers or CGO-disabled builds, there is no CA bundle file at all, so it keeps failing. Two approaches:

Bash
# A: as an env var path set
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs
Go
// B:
package main

import (
    "crypto/tls"
    "crypto/x509"
    "net/http"
    "os"
)

func newClient() *http.Client {
    pool, _ := x509.SystemCertPool()
    if pool == nil {
        pool = x509.NewCertPool()
    }
    ca, _ := os.ReadFile("/path/to/myca.crt")
    pool.AppendCertsFromPEM(ca)

    return &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{RootCAs: pool},
        },
    }
}

Kubernetes & git

kubectl looks at the CA inside kubeconfig.

Bash
# kubeconfig CA check
kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d | openssl x509 -noout -issuer

When nodes pull from a private registry (e.g. kubelet image pull), you must sync both the node's system trust store and /etc/docker/certs.d (or containerd's certs.d).

git is set globally or via an environment variable.

Bash
git config --global http.sslCAInfo /etc/ssl/certs/myca.crt
# or
export GIT_SSL_CAINFO=/etc/ssl/certs/myca.crt

⛔ Never do this

The following are dangerous workarounds that leave you fully exposed to MITM (man-in-the-middle) attacks.

  • InsecureSkipVerify: true (Go)
  • curl -k / git -c http.sslVerify=false
  • Permanent use of Docker insecure-registries

The moment you disable TLS verification, anyone who forges a certificate and intercepts traffic can succeed. Even an internal network is not safe. Use these only for truly urgent one-off debugging, and never commit them to code or config files.

In practice, the most common incident pattern is this: "Let's just get it working with InsecureSkipVerify and fix it later" — and that later never comes; it ships to production. Recurrence only stopped after I added a CI rule that greps for this string at PR review time.

Verification checklist

After the fix, confirm actual success with each tool.

Bash
docker pull myregistry.local:5000/myimage:latest   # Docker
go run main.go                                      # Go
kubectl get nodes                                   # k8s
git ls-remote https://gitlab.local/group/repo.git   # git

Preventing recurrence: bake it in and deploy at org scale

Don't just fix it once — automate it.

Bake the CA into the base image:

Dockerfile
FROM debian:stable-slim
COPY myca.crt /usr/local/share/ca-certificates/myca.crt
RUN update-ca-certificates
  • Roll out the trust store in bulk with a node bootstrap script or an Ansible playbook.
  • Set up CA expiry monitoring (openssl x509 -enddate via cron, or a Prometheus exporter).

With zero-trust and mTLS adoption, and the spread of internal Harbor/Nexus/GitLab Registry, you will hit this issue more often. Standardize once and new nodes and new images will not fall into the same trap.

Reference: official docs

The primary sources for the behavior, settings, and errors covered in this post are the following official docs. Check them for version-specific options and exact behavior.

FAQ

Q. I ran update-ca-certificates but docker pull still fails. A. The Docker daemon does not look at the system CA. Place the certificate at /etc/docker/certs.d/<registry:port>/ca.crt and run systemctl restart docker. The directory name must include the port.

Q. It works on Linux but the Go binary in a distroless container still fails. A. distroless and scratch images have no CA bundle file. Include ca-certificates in the image, or point SSL_CERT_FILE at the bundle path; if that is not enough, append the internal CA to SystemCertPool() with AppendCertsFromPEM in code.

Q. I'm in a hurry — can I just set InsecureSkipVerify: true? A. Not unless this is temporary debugging. Disabling TLS verification leaves you wide open to MITM attacks. Registering the CA in the trust store is ultimately the fastest and safest path.

✦ ✦ ✦
Sources and verification

Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us

This article has no recorded human check (date and scope) yet.

Published ·
Topic-related official docs (auto-linked · not per-claim sources)Kubernetes documentation ↗Docker documentation ↗Git documentation ↗OWASP documentation ↗

Comments

Be the first to comment.