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:
x509: certificate signed by unknown authorityThe 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.
| Command | Actual output (verbatim) | What to fix |
|---|---|---|
docker pull | x509: certificate signed by unknown authority | /etc/docker/certs.d/<registry:port>/ca.crt |
go run | tls: failed to verify certificate: x509: certificate signed by unknown authority | System trust store or SSL_CERT_FILE |
git clone | fatal: unable to access ...: SSL certificate problem: self-signed certificate in certificate chain | git config http.sslCAInfo |
kubectl | Unable to connect to the server: x509: certificate signed by unknown authority | kubeconfig 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.
# 1) 서버가 내려주는 체인과 발급자 확인
openssl s_client -connect myregistry.local:5000 -showcerts </dev/nullInspect the CA file (ca.crt) you just captured and split on the result.
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.
notAfteris in the past → expired. The answer is certificate renewal, not trust registration.- Connection works but hostname verification fails → SNI/CN mismatch. Confirm with
-servername.
openssl s_client -connect myregistry.local:5000 -servername myregistry.local </dev/nullMost 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:
sudo cp myca.crt /usr/local/share/ca-certificates/myca.crt
sudo update-ca-certificatesRHEL/CentOS/Fedora:
sudo cp myca.crt /etc/pki/ca-trust/source/anchors/myca.crt
sudo update-ca-trust extractNote: On Debian-family distros the extension must be
.crtor 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.
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 dockerIf 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:
# 방법 A: 환경변수로 번들 경로 지정
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_DIR=/etc/ssl/certs// 방법 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.
# kubeconfig의 CA 데이터 확인
kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d | openssl x509 -noout -issuerWhen 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.
git config --global http.sslCAInfo /etc/ssl/certs/myca.crt
# 또는
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-registriesThe 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.
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 # gitPreventing recurrence: bake it in and deploy at org scale
Don't just fix it once — automate it.
Bake the CA into the base image:
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 -enddatevia 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.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.