사설 CA 로 발급한 인증서를 쓰는 레지스트리에 붙을 때 다음 오류가 난다.
failed to authorize: failed to fetch oauth token
Post "https://registry.example.com:8443/service/token":
tls: failed to verify certificate: x509: certificate signed by unknown authority
docker login 은 되는데 docker push 에서만 실패하는 경우가 흔하다. 로그인은 CLI 가 처리하지만 push 는 데몬이 토큰 엔드포인트를 다시 호출하기 때문이다. 즉 CLI 쪽 신뢰 설정과 데몬 쪽 신뢰 설정이 따로 논다는 신호다.
Docker 데몬은 /etc/docker/certs.d/<호스트:포트>/ca.crt 를 문자열 그대로 찾는다. 포트를 뺀 디렉터리, 밑줄로 바꾼 디렉터리, ca.crt 가 아닌 파일 이름은 모두 무시된다. 레지스트리를 IP 로 접근한다면 디렉터리 이름도 IP 여야 한다.
ls -l /etc/docker/certs.d/registry.example.com:8443/
서버 인증서를 그대로 복사해 두는 실수가 잦다. 자체 서명 인증서라면 자기 자신이 CA 이므로 그대로 써도 되지만, 별도 CA 가 서명한 경우에는 CA 인증서를 넣어야 한다.
openssl x509 -in /etc/docker/certs.d/registry.example.com:8443/ca.crt -noout -text | grep -A1 'Basic Constraints'
CA:TRUE 가 보여야 한다. CA:FALSE 면 그 파일은 서버 인증서다. 서버가 보내는 체인에서 서명자를 꺼낸다.
openssl s_client -connect registry.example.com:8443 -showcerts </dev/null \
| sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > chain.crt
openssl x509 -in chain.crt -noout -subject -issuer
unable to get local issuer certificate 만 나오고 인증서가 하나뿐이라면 서버가 중간 CA 를 빼고 leaf 인증서만 제공하는 것이다. 이때는 클라이언트를 고칠 일이 아니라 서버 쪽에 fullchain 을 설정해야 한다.
certs.d/<호스트:포트>/ 에 *.cert 와 *.key 쌍이 함께 있으면 Docker 는 그 레지스트리가 클라이언트 인증(mTLS)을 요구한다고 보고 그 인증서를 제시한다. 서버가 클라이언트 인증을 쓰지 않으면 핸드셰이크가 어긋난다. 이 디렉터리에는 ca.crt 만 둔다.
cd /etc/docker/certs.d/registry.example.com:8443
mv *.cert *.key /root/ # ca.crt 만 남긴다
systemctl restart docker
인증서의 SAN 에 DNS:registry 만 있고 IP 가 없는데 클라이언트가 IP 로 접속하면 검증이 실패한다. Docker 는 SAN 불일치도 certificate signed by unknown authority 로 뭉뚱그려 낸다.
openssl x509 -in ca.crt -noout -text | grep -A1 'Subject Alternative Name'
curl -v --cacert ca.crt https://registry.example.com:8443/v2/
curl 이 성공하고 Docker 만 실패하면 경로·파일 문제이고, curl 도 실패하면 인증서나 주소 문제다. 운영 환경에서는 인증서를 다시 발급해 SAN 에 호스트명과 IP 를 모두 넣는 편이 깔끔하다.
[ alt_names ]
DNS.1 = registry.example.com
DNS.2 = registry
IP.1 = 172.25.102.197
containerd 는 Docker 의 certs.d 를 보지 않는다. Kubernetes 노드는 containerd 쪽에 따로 등록해야 이미지를 받는다. 디렉터리는 /etc/containerd/certs.d/<호스트:포트>/ 이고 hosts.toml 이 필요하다.
# /etc/containerd/certs.d/registry.example.com:8443/hosts.toml
server = "https://registry.example.com:8443"
[host."https://registry.example.com:8443"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/registry.example.com:8443/ca.crt"
이 디렉터리를 쓰려면 /etc/containerd/config.toml 에서 config_path 가 설정돼 있어야 한다.
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
systemctl restart containerd
Docker 와 containerd 는 신뢰 저장소가 다르므로 각각 확인한다.
docker push registry.example.com:8443/library/pause:3.10
ctr -n k8s.io images pull registry.example.com:8443/library/pause:3.10
crictl pull registry.example.com:8443/library/pause:3.10
crictl pull 이 성공하면 kubelet 도 같은 경로로 이미지를 받는다. 실패 메시지로 원인을 가른다 — x509 면 인증서, connection refused 면 네트워크, 401 Unauthorized 면 계정·권한 문제다.