- Published on
Kubernetes ErrImagePull x509 인증서 오류 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 정상인데도 Pod가 ErrImagePull/ImagePullBackOff로 멈추고, 이벤트에 x509: certificate signed by unknown authority가 찍히는 순간부터 운영자는 시간이 급격히 소모됩니다. 특히 사설 레지스트리(하버/아티팩토리), 프록시가 낀 네트워크, 혹은 기업 보안장비가 TLS를 가로채는 환경에서는 “클러스터는 멀쩡한데 특정 노드에서만 실패” 같은 비대칭 증상이 흔합니다.
이 글은 Kubernetes 이미지 Pull 시점의 x509 오류를 “어디에서 TLS 검증이 일어나는지” 관점으로 쪼개서, 가장 빠른 진단 루트 → 원인별 해결 → 재발 방지 체크리스트 순서로 정리합니다.
증상 정리: 에러 문자열로 범위를 좁히기
대표적인 이벤트/로그 패턴은 아래와 같습니다.
Failed to pull image ... x509: certificate signed by unknown authorityx509: certificate has expired or is not yet validx509: certificate is valid for ..., not <registry-host>tls: failed to verify certificate: x509 ...
핵심은 “누가” 레지스트리와 TLS 핸드셰이크를 하느냐입니다.
- Docker 사용 노드:
dockerd가 검증 - containerd 사용 노드(EKS, 대부분의 최신 배포판): containerd가 검증
- 노드가 아니라 Pod 내부에서
curl이 실패하는 경우도 있지만, ErrImagePull은 기본적으로 노드 런타임 레벨 문제입니다.
1분 진단: kubectl로 이벤트부터 확인
먼저 Pod 이벤트에서 정확한 레지스트리 호스트명과 에러를 확보합니다.
kubectl describe pod <pod> -n <ns>
출력 중 다음 섹션을 봅니다.
Events:Failed to pull image ...Back-off pulling image ...
여기서 레지스트리가 registry.example.com:5000인지, 혹은 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com인지에 따라 해결책이 달라집니다.
어디서 깨지는가: 노드에서 직접 확인
ErrImagePull이면 거의 항상 노드에서 레지스트리로 나가는 TLS가 실패합니다. 문제 재현을 위해 해당 Pod가 스케줄된 노드로 들어가 확인합니다.
kubectl get pod <pod> -n <ns> -o wide
# NODE 컬럼 확인
노드에 접속한 뒤(SSH 혹은 SSM), 레지스트리 인증서를 직접 확인합니다.
# 레지스트리 인증서 체인 확인
openssl s_client -connect registry.example.com:5000 -showcerts </dev/null
# 호스트네임 검증까지 포함해 확인(권장)
curl -v https://registry.example.com:5000/v2/
openssl s_client에서 서버가 어떤 인증서 체인(중간 CA 포함)을 내리는지 확인curl에서 시스템 CA 번들로 검증이 되는지 확인
여기서 curl도 같은 x509 에러가 나면, 거의 확정적으로 노드 OS의 CA 번들 문제 또는 프록시/보안장비의 MITM 인증서 미신뢰입니다.
원인 1) 사설 레지스트리 CA(사내 Root/Intermediate) 미설치
가장 흔한 케이스입니다.
- 사설 레지스트리가 사내 CA로 발급된 인증서를 사용
- 노드 OS는 해당 CA를 신뢰하지 않음
- containerd/dockerd가 시스템 CA를 기반으로 검증하다 실패
해결 A: 노드 OS에 CA 추가 (가장 정석)
배포판별로 CA 설치 위치가 다릅니다.
Ubuntu/Debian
sudo cp corp-root-ca.crt /usr/local/share/ca-certificates/corp-root-ca.crt
sudo update-ca-certificates
# 확인
grep -R "Corp Root" -n /etc/ssl/certs | head
Amazon Linux 2 / RHEL 계열
sudo cp corp-root-ca.crt /etc/pki/ca-trust/source/anchors/corp-root-ca.crt
sudo update-ca-trust extract
# 확인
trust list | grep -i corp -n
이후 containerd/dockerd 재시작이 필요할 수 있습니다.
sudo systemctl restart containerd
# 또는
sudo systemctl restart docker
해결 B: containerd에 레지스트리별 CA 지정
노드 전체 CA를 건드리기 어렵거나, 특정 레지스트리만 별도 체인을 쓰는 경우 containerd에 호스트별 CA를 지정할 수 있습니다.
containerd는 보통 다음 경로를 사용합니다.
/etc/containerd/config.toml/etc/containerd/certs.d/<registry-host>:<port>/hosts.toml
hosts.toml 방식이 관리에 유리합니다.
sudo mkdir -p /etc/containerd/certs.d/registry.example.com:5000
sudo tee /etc/containerd/certs.d/registry.example.com:5000/hosts.toml > /dev/null <<'EOF'
server = "https://registry.example.com:5000"
[host."https://registry.example.com:5000"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/registry.example.com:5000/ca.crt"
EOF
sudo cp corp-root-ca.crt /etc/containerd/certs.d/registry.example.com:5000/ca.crt
sudo systemctl restart containerd
> 환경에 따라 capabilities에 push가 필요할 수 있습니다.
원인 2) 레지스트리가 중간 인증서(Intermediate)를 누락
서버는 리프(leaf) 인증서만 주고, 중간 인증서를 제대로 제공하지 않으면 어떤 클라이언트는 체인 빌드에 실패합니다. 브라우저는 AIA로 중간 인증서를 가져와서 “되는 것처럼” 보이기도 하지만, 런타임은 실패할 수 있습니다.
진단
openssl s_client -connect registry.example.com:443 -showcerts </dev/null | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' | wc -l
인증서가 1개만 나오거나, 기대하는 중간 CA가 없다면 레지스트리(혹은 앞단의 LB/Ingress) 설정 문제입니다.
해결
- NGINX/Envoy/LB에서 fullchain.pem(leaf + intermediate)을 사용
- Harbor/Artifactory 등 레지스트리 제품의 TLS 설정에서 체인을 포함하도록 구성
원인 3) 인증서 만료/시간 불일치 (Not Yet Valid 포함)
노드 시간이 틀어져도 x509: certificate has expired or is not yet valid가 발생합니다. 특히 프라이빗 서브넷에서 NTP가 막혀 있거나, 부팅 직후 시간이 튀는 경우에 재현됩니다.
진단
date -u
# systemd-timesyncd/chrony 상태 확인(환경별)
timedatectl status
chronyc tracking 2>/dev/null || true
해결
- 노드가 NTP에 접근 가능하도록 라우팅/보안그룹/방화벽 점검
- chrony 사용 시 내부 NTP 서버를 지정
원인 4) 인증서 SAN 불일치 (호스트명/도메인 문제)
x509: certificate is valid for ... not registry.example.com 형태라면,
- 레지스트리에 접근하는 주소(예:
registry.internal)와 - 인증서 SAN에 들어있는 도메인(예:
registry.example.com)
이 다릅니다.
해결
- 이미지 레퍼런스의 레지스트리 호스트명을 인증서 SAN에 맞추기
- 혹은 인증서를 재발급하여 SAN에 실제 접근 도메인을 포함
- 내부 DNS(CNAME)로 우회하는 경우에도 SAN 포함이 필요
원인 5) 프록시/보안장비의 TLS 가로채기(MITM)
기업 네트워크에서 흔합니다.
- 노드가 외부로 나갈 때 프록시가 TLS를 재서명
- 노드가 프록시의 Root CA를 신뢰하지 않음
- 결과적으로 레지스트리 Pull이 x509로 실패
진단 포인트
openssl s_client로 본 인증서 발급자가 공인 CA가 아니라 사내 프록시/보안장비로 표시- 같은 레지스트리도 사내망에서만 실패
해결
- 노드 OS에 프록시 Root CA 설치(원인 1의 방법과 동일)
- containerd에 프록시 CA를 레지스트리별로 지정
- 프록시를 우회하도록 네트워크 정책/라우팅 구성(가능한 경우)
추가로, 프록시 환경에서는 인증서 문제 외에도 gRPC/HTTP2 레벨에서 문제가 증폭될 수 있습니다. 서비스 통신에서 UNAVAILABLE나 RST_STREAM이 함께 보인다면 네트워크 계층 튜닝도 같이 확인하는 편이 좋습니다: Kubernetes gRPC UNAVAILABLE·RST_STREAM 원인과 Envoy·NGINX 대응
원인 6) EKS/Managed Node Group에서 “노드별 편차”가 나는 경우
EKS에서 특히 헷갈리는 지점은, 같은 클러스터인데 특정 노드그룹/특정 노드만 ErrImagePull이 나는 케이스입니다.
가능성이 높은 순서:
- 노드 AMI/OS 이미지가 달라 CA 번들이 다름
- UserData/부트스트랩 스크립트에서 CA 설치가 누락됨
- 특정 서브넷만 프록시 경로를 타거나, NAT/방화벽이 다름
- 노드그룹 롤링 도중 일부 노드만 최신 설정 적용
해결: UserData로 CA 설치를 “항상” 적용
Managed Node Group이라도 부트스트랩에 CA 설치를 넣어야 재발을 막을 수 있습니다. 예시는 Amazon Linux 2 기준입니다.
#!/bin/bash
set -euo pipefail
# 1) CA 배치
cat >/etc/pki/ca-trust/source/anchors/corp-root-ca.crt <<'EOF'
-----BEGIN CERTIFICATE-----
...생략...
-----END CERTIFICATE-----
EOF
# 2) CA 반영
update-ca-trust extract
# 3) containerd 재시작
systemctl restart containerd || true
EKS 운영 중 다른 인증/권한 이슈(예: IRSA 전면 실패)가 함께 터지면 원인이 섞여 보이기도 합니다. OIDC Provider 삭제로 IRSA가 전부 실패했던 복구 시나리오는 아래 글이 도움이 됩니다: EKS OIDC Provider 삭제로 IRSA 전부 실패했을 때 복구
(비권장) insecure registry 설정은 최후의 수단
급한 장애 대응에서 insecure_skip_verify 또는 insecure registry로 우회하고 싶은 유혹이 큽니다. 하지만 이는 공격 표면을 크게 늘리고, 공급망 보안 관점에서 매우 위험합니다.
그래도 내부 폐쇄망/임시 대응으로 꼭 필요하다면, 최소한 아래 원칙을 지키세요.
- 특정 레지스트리 호스트에만 제한
- 임시 조치임을 이슈/런북에 명확히 남기고 만료일 설정
- 가능한 한 빨리 CA 배포로 정상화
containerd의 hosts.toml 예(정말 최후의 수단):
server = "https://registry.example.com:5000"
[host."https://registry.example.com:5000"]
capabilities = ["pull", "resolve"]
skip_verify = true
재발 방지 체크리스트
운영 환경에서 ErrImagePull x509는 “한 번 고치고 끝”이 아니라, 노드 교체/오토스케일/롤링 업데이트 때 재발합니다. 아래를 표준화하면 장애 확률이 크게 줄어듭니다.
- CA 배포 자동화: AMI 베이크(Packer) 또는 Node UserData로 강제
- 레지스트리 인증서 체인 점검: leaf + intermediate 포함(fullchain) 상시 검증
- 노드 시간 동기화 보장: NTP 접근 경로/보안그룹/프록시 정책 문서화
- 노드 런타임 표준화: containerd 버전/설정 경로 통일
- 네트워크 경로 가시화: 서브넷별 프록시/NAT 차이를 다이어그램으로 관리
- 진단 커맨드 런북화:
kubectl describe,openssl s_client,curl -v를 템플릿으로 공유
빠른 결론: 가장 많이 먹히는 해결 순서
- (1)
kubectl describe pod로 레지스트리 호스트/정확한 x509 메시지 확보 - (2) 문제 노드에서
openssl s_client로 “실제 내려오는 인증서 체인” 확인 - (3) 노드 OS에 사내 Root/Intermediate CA 설치 후 containerd 재시작
- (4) 여전히 실패하면 레지스트리의 중간 인증서 누락과 SAN 불일치를 점검
- (5) 기업 프록시/MITM 환경이면 프록시 Root CA 배포가 정답
이미지 Pull은 클러스터의 첫 관문입니다. x509 오류를 단순히 “인증서 문제”로 뭉뚱그리기보다, 노드 런타임(containerd) 관점에서 체인/시간/SAN/프록시를 분해해 보면 대부분 10~20분 안에 원인까지 도달할 수 있습니다.