/인프라/ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단
인프라ImagePullBackOffimagePullSecrets

ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단

Pod가 ImagePullBackOff·ErrImagePull에서 멈췄나요? x509 unknown authority·unauthorized 등 Harbor·Nexus 에러를 Events로 원인별 분기하고 imagePullSecrets·사설 CA·crictl 복붙 명령어로 5분 만에 해결합니다.

ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단

ImagePullBackOff·x509·unauthorized 사설 레지스트리 에러 5분 진단

K8s_Troubleshooting_Guide 15편

배포 매니페스트는 분명히 잘 적용됐는데, Pod가 ImagePullBackOff에서 한 발짝도 못 나가고 빨갛게 멈춰 있는 상황. 특히 Harbor, Nexus, 사내 registry 같은 사설 레지스트리를 쓰면 이 에러가 유독 자주 터집니다. 이 글은 에러 문구를 그대로 검색해 들어온 분들을 위해, kubectl describe pod Events 한 줄로 원인을 분기하고 원인별 복붙 명령어로 5분 안에 푸는 루틴을 정리했습니다.

ErrImagePull과 ImagePullBackOff는 무엇이 다른가

둘은 같은 문제의 서로 다른 단계입니다.

  • ErrImagePull: kubelet이 이미지를 당기다가 방금 실패한 상태.
  • ImagePullBackOff: 실패가 반복돼 kubelet이 재시도 간격을 지수적으로 늘리며 대기 중인 상태(최대 5분).

즉 ImagePullBackOff는 "계속 실패 중"이라는 뜻이지 새로운 원인이 아닙니다. 진짜 원인은 항상 Events의 Failed 메시지 안에 있습니다.

사설 레지스트리에서 자주 터지는 이유는 세 가지가 겹치기 때문입니다. ① 사내 PKI 사설 CA로 발급한 인증서를 노드가 신뢰하지 않음(x509), ② 인증 정보(imagePullSecrets) 누락(401), ③ K8s 1.24+에서 dockershim이 제거되고 containerd가 표준이 되면서 레지스트리 설정 위치가 /etc/docker에서 /etc/containerd/certs.d로 바뀐 점을 놓침. 여기에 Docker Hub 무료 티어 레이트리밋 강화로 사내 미러를 도입하면서 설정이 한 겹 더 늘어났죠.

1단계 — Events에서 정확한 실패 사유 식별

가장 먼저 할 일은 단 하나입니다.

Bash
kubectl describe pod <pod-name> -n <namespace> | sed -n '/Events:/,$p'

Warning Failed 줄에 나오는 단어로 원인을 분기합니다.

Events에 보이는 메시지원인분기
manifest unknown / manifest for ... not found이미지 태그·경로 오타
pull access denied ... unauthorized / 401 Unauthorized인증 누락
x509: certificate signed by unknown authority사설 CA 미신뢰
toomanyrequests: You have reached your pull rate limitDocker Hub 레이트리밋
dial tcp: lookup harbor.example.com: no such host / i/o timeoutDNS·네트워크·프록시

이 표만 옆에 띄워 두면 "이 줄이 보이면 ③으로" 식으로 곧장 점프할 수 있습니다.

① manifest unknown — 태그·경로 오타

가장 허무하지만 가장 흔합니다. 레지스트리에 실제로 그 태그가 있는지부터 확인하세요.

Bash
# 태그 목록 조회 (Harbor/Nexus의 v2 API)
curl -sk -u <user>:<pw> https://harbor.example.com/v2/app/image/tags/list

latest로 밀어 놓고 정작 푸시는 v1.2.3만 한 경우, 프로젝트 경로(app/)를 빠뜨린 경우가 대부분입니다. 매니페스트의 image: 값을 위 응답과 한 글자씩 대조하세요.

② unauthorized — imagePullSecrets 생성

401이 보이면 인증 정보가 없거나 틀린 겁니다. docker-registry 타입 secret을 만듭니다.

Bash
kubectl create secret docker-registry regcred \
  --docker-server=harbor.example.com \
  --docker-username=<user> \
  --docker-password=<pw> \
  --docker-email=<mail> \
  -n <namespace>

주의: --docker-server에는 스킴(https://)을 빼고 레지스트리 호스트만 적습니다. Docker Hub라면 https://index.docker.io/v1/이 표준값입니다.

생성한 secret을 Pod가 쓰게 연결하는 방법은 두 가지입니다.

방법 A — Deployment(Pod spec)에 직접 지정:

YAML
spec:
  template:
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: app
          image: harbor.example.com/app/image:v1.2.3

방법 B — ServiceAccount에 붙여 네임스페이스 전체에 적용:

Bash
kubectl patch serviceaccount default -n <namespace> \
  -p '{"imagePullSecrets":[{"name":"regcred"}]}'

방법 B를 추천합니다. 워크로드마다 매번 imagePullSecrets를 적지 않아도 같은 ServiceAccount를 쓰는 모든 Pod에 자동 적용되거든요. 단, 기존에 떠 있던 Pod는 재생성해야 반영됩니다.

③ x509 certificate signed by unknown authority — 사설 CA 등록

CODE
failed to pull and unpack image ... x509: certificate signed by unknown authority

이 줄이 보이면 인증이 아니라 인증서 신뢰 문제입니다. 사내 PKI로 발급한 인증서를 노드의 containerd가 신뢰하지 않는 것이죠. K8s 1.24+ containerd 표준 환경에서는 certs.d 디렉터리를 사용합니다.

먼저 /etc/containerd/config.toml에 config_path가 설정돼 있는지 확인합니다.

TOML
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"

그다음 호스트별 디렉터리에 hosts.toml을 만듭니다.

TOML
# /etc/containerd/certs.d/harbor.example.com/hosts.toml
server = "https://harbor.example.com"

[host."https://harbor.example.com"]
  capabilities = ["pull", "resolve"]
  ca = "/etc/containerd/certs.d/harbor.example.com/ca.crt"
  # skip_verify = true  ← 인증서 검증을 끄는 옵션. 운영 환경에서는 절대 비권장!

사내 CA의 루트 인증서를 위 ca.crt 경로에 복사한 뒤 containerd를 재시작합니다.

Bash
cp my-internal-ca.crt /etc/containerd/certs.d/harbor.example.com/ca.crt
systemctl restart containerd

구버전 Docker(dockershim) 환경이라면 경로가 다릅니다. 포트까지 디렉터리명에 포함된다는 점에 주의하세요.

Bash
mkdir -p /etc/docker/certs.d/harbor.example.com:443
cp my-internal-ca.crt /etc/docker/certs.d/harbor.example.com:443/ca.crt
systemctl restart docker

registry.mirrors(미러 호스트 지정)와 config_path(certs.d 사용)는 목적이 다릅니다. 미러는 풀 트래픽을 다른 호스트로 보내는 것이고, certs.d는 호스트별 인증서·인증 설정을 담는 곳입니다. 둘을 혼동하면 "분명 미러를 걸었는데 여전히 x509"가 나옵니다.

④ toomanyrequests — Docker Hub 레이트리밋

CODE
toomanyrequests: You have reached your pull rate limit

익명 IP당 6시간 100~200회 제한에 걸린 경우입니다. 임시방편으로는 Docker Hub 인증 secret을 붙여 한도를 늘리고, 근본 해결로는 Harbor proxy cache 프로젝트를 만들어 harbor.example.com/dockerhub-proxy/library/nginx처럼 사내 미러를 경유시키세요. 한 번 당긴 이미지는 캐시되어 외부 풀 자체가 사라집니다.

⑤ no such host / i/o timeout — 네트워크·프록시

DNS나 방화벽, 프록시 문제입니다. 노드에서 직접 끊어 봅니다.

Bash
getent hosts harbor.example.com          # DNS 해석 확인
curl -vk https://harbor.example.com/v2/  # TLS 핸드셰이크·연결 확인

containerd가 사내 프록시 뒤에 있다면 systemd drop-in에 HTTPS_PROXY/NO_PROXY를 설정하고, 사내 레지스트리는 반드시 NO_PROXY에 넣어야 합니다.

노드에서 직접 검증 — crictl로 kubelet 우회 재현

원인을 고쳤다면 kubelet을 거치지 않고 노드에서 바로 풀을 재현해 보는 게 가장 확실합니다.

Bash
# 인증·인증서·네트워크를 한 번에 검증
crictl pull --creds <user>:<pw> harbor.example.com/app/image:v1.2.3

# 캐시에 들어왔는지 확인
crictl images | grep harbor.example.com

crictl pull이 성공하면 인증서·인증·네트워크는 모두 정상이고 문제는 secret 연결이나 매니페스트 쪽이라는 뜻입니다. 반대로 crictl pull도 실패하면 노드 레벨(③⑤) 문제가 남아 있는 겁니다. 이 분리 진단이 시간을 가장 많이 아껴 줍니다.

secret이 실제로 맞게 들어갔는지 확인

imagePullSecrets를 붙였는데도 401이 계속되면, secret 내용이 실제 레지스트리와 일치하는지 디코드해서 눈으로 확인하세요.

Bash
kubectl get secret regcred -n <namespace> \
  -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .

출력의 auths 키 아래 server 주소가 매니페스트 image:의 호스트와 정확히 같은지, auth 값(base64 인코딩된 user:pw)이 유효한지 봅니다. 흔한 실수는 secret은 harbor.example.com인데 이미지 경로는 harbor.example.com:443처럼 포트가 붙어 둘이 매칭되지 않는 경우입니다.

실무 경험 한 토막

현장에서 가장 많이 본 함정은 "노드 한 대만 안 되는" 케이스였습니다. 새로 조인한 워커 노드에 사내 CA 배포 자동화가 빠져, 그 노드에 스케줄된 Pod만 x509로 죽었죠. Events의 노드명을 꼭 확인하고, CA·hosts.toml 배포는 Ansible이나 DaemonSet, 또는 노드 부트스트랩 스크립트로 전 노드에 강제하는 걸 권합니다. 한 대만 손으로 고치면 다음 스케일아웃 때 똑같은 사고가 재발합니다.

5분 진단 체크리스트

단계명령어/행동판단
1kubectl describe pod Events 확인메시지로 ①~⑤ 분기
2manifest unknown → 태그 v2 API 조회오타·경로 수정
3unauthorized → create secret docker-registry + imagePullSecretsSA에 붙이고 Pod 재생성
4x509 → certs.d/hosts.toml에 CA 등록 후 restart containerd전 노드 적용
5crictl pull --creds로 노드 직접 재현성공 시 secret/매니페스트 문제

재발 방지 팁: 사내 CA·certs.d는 노드 프로비저닝 자동화에 포함, Docker Hub 의존 이미지는 Harbor proxy cache로 일원화, ServiceAccount에 imagePullSecrets를 기본 탑재하세요.

다음 편(16편)에서는 kubectl top·OOMKilled — Pod가 메모리 한계로 죽을 때 진단을 다룹니다.

자주 묻는 질문 (FAQ)

Q. ErrImagePull과 ImagePullBackOff 중 어느 쪽을 봐야 하나요? A. 둘은 같은 실패의 다른 단계라 원인이 동일합니다. 무조건 kubectl describe pod의 Events Failed 메시지를 보고 분기하세요. 상태 이름 자체는 진단 정보가 아닙니다.

Q. 인증서 검증을 끄는 skip_verify를 써도 되나요? A. 급할 때 재현용으로만 쓰고 운영 환경엔 절대 남기지 마세요. 중간자 공격에 노출됩니다. 사내 CA의 루트 인증서를 ca 키로 등록하는 것이 정석입니다.

Q. secret을 만들었는데도 계속 unauthorized가 나옵니다. A. ① secret이 Pod와 같은 네임스페이스에 있는지, ② imagePullSecrets가 실제로 연결됐는지, ③ base64 디코드한 server 주소가 이미지 호스트(포트 포함 여부까지)와 일치하는지 확인하세요. 기존 Pod는 secret 적용 후 재생성해야 반영됩니다.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.

작성 · Content Reviewer·검토 · 사람 편집자·발행 · 2026년 6월 17일

댓글

불러오는 중...