/보안/Fixing x509: certificate signed by unknown authority — Copy-paste runbook for Docker, Go, k8s, and git
Securityx509 에러 해결사설CA 신뢰

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

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) 서버가 내려주는 체인과 발급자 확인
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: 환경변수로 번들 경로 지정
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 데이터 확인
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
# 또는
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.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.