x509: certificate signed by unknown authority 해결 — Docker·Go·k8s·git 복붙 런북
같은 인증서인데 브라우저·curl은 되고 docker·go만 막히는 이유
사내에 사설 CA나 자체서명 인증서를 깔아두면 꼭 겪는 일이 있습니다. 브라우저로는 잘 들어가지고 curl도 통과하는데, 유독 docker pull이나 go run, kubectl만 다음과 같이 토하는 상황이죠.
x509: certificate signed by unknown authority원인은 단순합니다. 런타임마다 신뢰하는 트러스트스토어(trust store)가 다르기 때문입니다. 브라우저는 자체 인증서 저장소를, OS의 curl은 시스템 CA 번들을 봅니다. 하지만 Docker daemon은 시스템 CA가 아니라 /etc/docker/certs.d/를 보고, Go 바이너리는 빌드 환경에 따라 시스템 스토어를 안 볼 수도 있으며, kubectl은 kubeconfig 안의 CA를 봅니다. 그래서 "한 군데 깔았다고 전부 해결"이 안 됩니다.
이 글은 unknown authority 계열, 즉 사내 CA가 해당 런타임의 트러스트스토어에 등록되지 않은 문제에 집중합니다. unable to get local issuer certificate(curl/openssl 핸드셰이크) 계열의 사전 점검은 [기존 SSL 핸드셰이크 런북]을 참고하시고, 여기서는 분기에 꼭 필요한 명령만 다룹니다.
에러 원문 verbatim 매칭표
스크롤 멈추고, 본인이 본 에러 한 줄을 아래에서 찾으세요.
| 명령 | 실제 출력 (verbatim) | 어디를 고쳐야 하는가 |
|---|---|---|
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 | 시스템 트러스트스토어 또는 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) |
핵심: Docker daemon은 시스템 CA를 보지 않습니다. 그래서 update-ca-certificates만 했다고 docker pull이 풀리지 않는 경우가 가장 흔한 함정입니다.
5분 진단 흐름: 4갈래 구분
복붙 처방 전에, 내 문제가 정말 "CA 미신뢰"인지 30초만에 확인합니다.
# 1) 서버가 내려주는 체인과 발급자 확인
openssl s_client -connect myregistry.local:5000 -showcerts </dev/null여기서 받은 CA 파일(ca.crt)을 검사해 갈래를 나눕니다.
openssl x509 -in ca.crt -noout -issuer -subject -dates- issuer == subject → 자체서명(self-signed) 인증서. 이 CA 자체를 신뢰시키면 됩니다.
- issuer != subject → 사설 CA가 발급한 인증서. 체인 최상단의 루트 CA를 신뢰시켜야 합니다.
notAfter날짜가 과거 → 만료. 신뢰 등록이 아니라 인증서 갱신이 답입니다.- 접속은 되는데 호스트명 검증 실패 → SNI/CN 불일치.
-servername으로 확인하세요.
openssl s_client -connect myregistry.local:5000 -servername myregistry.local </dev/null대부분은 첫 두 갈래(자체서명/사설 CA)입니다. 아래 처방으로 넘어가세요.
원인별 복붙 처방
OS 시스템 트러스트스토어 (Go·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 extract주의: Debian 계열은 확장자가 반드시
.crt여야 인식됩니다.
Docker daemon: certs.d가 정답
Docker daemon은 시스템 CA가 아니라 레지스트리별 디렉터리를 봅니다. 디렉터리명에 포트까지 포함해야 합니다.
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 dockermyregistry.local:5000처럼 포트가 빠지면 적용되지 않으니 꼭 확인하세요.
Go 런타임: distroless가 함정
리눅스 호스트에서 직접 빌드한다면 시스템 스토어 갱신만으로 해결됩니다. 그런데 distroless·scratch 베이스 컨테이너나 CGO 비활성 빌드에서는 CA 번들 파일 자체가 없어 계속 실패합니다. 두 가지 방법이 있습니다.
# 방법 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은 kubeconfig 안의 CA를 봅니다.
# kubeconfig의 CA 데이터 확인
kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d | openssl x509 -noout -issuer노드에서 사설 레지스트리를 당기는 경우(예: kubelet의 이미지 pull)는 노드의 시스템 트러스트스토어 + /etc/docker/certs.d(또는 containerd의 certs.d)를 함께 동기화해야 합니다.
git은 전역 또는 환경변수로 지정합니다.
git config --global http.sslCAInfo /etc/ssl/certs/myca.crt
# 또는
export GIT_SSL_CAINFO=/etc/ssl/certs/myca.crt⛔ 절대 하지 말 것
다음은 MITM(중간자 공격)에 그대로 노출되는 위험한 회피책입니다.
InsecureSkipVerify: true(Go)curl -k/git -c http.sslVerify=false- Docker
insecure-registries의 영구 사용TLS 검증을 끄는 순간 누가 인증서를 위조해 끼어들어도 막을 수 없습니다. 사내망이라도 안전하지 않습니다. 정말 급한 일회성 디버깅에서만 쓰고, 절대 코드·설정 파일에 커밋하지 마세요.
실무 경험상 가장 사고가 잦은 패턴이 이겁니다. "일단 InsecureSkipVerify로 풀고 나중에 고치자"가 그대로 운영에 올라가는 것. 저는 PR 리뷰 단계에서 이 문자열을 grep으로 막는 룰을 CI에 넣고 나서야 재발이 멈췄습니다.
검증 체크리스트
처방 후 각 도구로 실제 성공을 확인합니다.
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재발 방지: 조직 차원에서 굽고 배포하기
한 번 고치고 끝내지 말고 자동화하세요.
베이스 이미지에 CA 굽기:
FROM debian:stable-slim
COPY myca.crt /usr/local/share/ca-certificates/myca.crt
RUN update-ca-certificates- 노드 부트스트랩 스크립트나 Ansible 플레이북으로 트러스트스토어를 일괄 배포합니다.
- CA 만료 모니터링을 걸어둡니다(
openssl x509 -enddate로 cron 체크 또는 Prometheus exporter).
제로트러스트·mTLS 도입과 사내 Harbor/Nexus/GitLab Registry 보편화로 이 이슈는 앞으로 더 자주 만나게 됩니다. 한 번 표준화해두면 신규 노드·신규 이미지가 늘어도 같은 함정에 빠지지 않습니다.
참고: 공식 문서
이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.
자주 묻는 질문 (FAQ)
Q. update-ca-certificates를 했는데도 docker pull이 계속 막혀요.
A. Docker daemon은 시스템 CA를 보지 않습니다. /etc/docker/certs.d/<registry:port>/ca.crt에 인증서를 배치하고 systemctl restart docker를 하세요. 디렉터리명에 포트를 꼭 포함해야 합니다.
Q. 리눅스에서는 됐는데 distroless 컨테이너의 Go 바이너리만 실패합니다.
A. distroless·scratch에는 CA 번들 파일이 없습니다. 이미지에 ca-certificates를 포함시키거나 SSL_CERT_FILE로 번들 경로를 지정하고, 안 되면 코드에서 SystemCertPool()에 AppendCertsFromPEM으로 사내 CA를 추가하세요.
Q. 급한데 그냥 InsecureSkipVerify: true로 넘기면 안 되나요?
A. 임시 디버깅이 아니라면 절대 권장하지 않습니다. TLS 검증을 끄면 MITM 공격에 무방비가 됩니다. CA를 트러스트스토어에 등록하는 정공법이 결국 가장 빠르고 안전합니다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.