증상

회사 보안 정책으로 Zscaler가 설치된 뒤, 어느 날 갑자기 Docker image pull이 실패하기 시작한다.

failed to resolve reference "asia-northeast3-docker.pkg.dev/...":
failed to do request: Head "https://asia-northeast3-docker.pkg.dev/...":
tls: failed to verify certificate: x509: certificate signed by unknown authority

macOS 키체인에 Zscaler Root CA를 신뢰하도록 등록했고, docker context ls로 보면 별 문제 없어 보인다. 그런데도 docker pull은 계속 같은 오류를 낸다.


원인: Docker daemon은 macOS가 아니라 Colima VM 안에 있다

Colima를 사용하는 환경에서 docker info를 실행해 보면 이런 결과가 나온다.

Client: Docker Engine - Community
 OS/Arch:     darwin/arm64
 Context:     colima

Server:
 Operating System: Ubuntu 24.04.4 LTS
 Name:             colima
 Docker Root Dir:  /var/lib/docker

Docker client는 macOS에서 실행되지만, Docker daemon(실제로 이미지를 pull하는 주체)은 Colima가 띄운 Ubuntu VM 내부에서 실행된다.

따라서 macOS 키체인이나 macOS의 /etc/docker/certs.d에 아무리 인증서를 추가해도 Colima VM 내부의 daemon에는 전달되지 않는다.

추가로 Zscaler는 TLS를 중간에서 재서명(SSL inspection)한다. 실제 레지스트리에 연결할 때 서버가 보내는 TLS chain에는 Root CA뿐 아니라 Intermediate CA도 포함된다.

subject=CN = *.domain.com, O = Zscaler Inc., OU = Zscaler Inc.
issuer=CN = "Zscaler Intermediate Root CA (zscalerthree.net) (t) ", ...

macOS 키체인에서 Root CA 하나만 추출하는 방식으로는 부족한 이유가 여기에 있다.


핵심 해결 원칙

  1. Colima VM 내부에 Zscaler CA를 설치해야 한다.
  2. macOS 키체인의 Root CA + 레지스트리 TLS chain의 Intermediate CA를 모두 수집한다.
  3. Docker registry 전용 CA 디렉터리(/etc/docker/certs.d/)와 시스템 CA 번들(update-ca-certificates) 두 곳 모두에 반영한다.
  4. CA 설치 후 Colima를 재시작해야 Docker daemon에 적용된다.

수정 스크립트

아래 스크립트는 위 원칙을 자동으로 수행한다.
REGISTRY 환경변수에 사용 중인 레지스트리 도메인을 지정하면 된다.

#!/usr/bin/env bash
set -euo pipefail

REGISTRY="${REGISTRY:-your-registry.example.com}"
CERT_DIR="$HOME/.certs"
BUNDLE="$CERT_DIR/zscaler-bundle.pem"

mkdir -p "$CERT_DIR"

# 1. macOS 키체인에서 Zscaler CA 추출
extract_from_keychains() {
  local tmp
  tmp="$(mktemp -d)"
  security find-certificate -a -p \
    /System/Library/Keychains/SystemRootCertificates.keychain \
    /Library/Keychains/System.keychain > "$tmp/all.pem"

  awk '/-----BEGIN CERTIFICATE-----/{n++} n{print > ("'"$tmp"'/cert" n ".pem")}' "$tmp/all.pem"
  for f in "$tmp"/cert*.pem; do
    [[ -s "$f" ]] || continue
    if openssl x509 -in "$f" -noout -subject -issuer 2>/dev/null | grep -iq Zscaler; then
      cat "$f" >> "$BUNDLE"
      printf '\n' >> "$BUNDLE"
    fi
  done
  rm -rf "$tmp"
}

# 2. 실제 TLS chain에서 Intermediate CA 추출 (CA:TRUE 인증서만)
extract_from_tls_chain() {
  local tmp
  tmp="$(mktemp -d)"
  echo | openssl s_client -connect "${REGISTRY}:443" \
    -servername "$REGISTRY" -showcerts 2>/dev/null > "$tmp/chain.txt"
  awk '/-----BEGIN CERTIFICATE-----/{n++} n{print > ("'"$tmp"'/cert" n ".pem")}' "$tmp/chain.txt"
  for f in "$tmp"/cert*.pem; do
    [[ -s "$f" ]] || continue
    local text
    text="$(openssl x509 -in "$f" -noout -text 2>/dev/null || true)"
    if printf '%s\n' "$text" | grep -iq Zscaler \
       && printf '%s\n' "$text" | grep -q 'CA:TRUE'; then
      cat "$f" >> "$BUNDLE"
      printf '\n' >> "$BUNDLE"
    fi
  done
  rm -rf "$tmp"
}

# 3. Colima VM 내부에 설치
install_to_colima() {
  colima status >/dev/null 2>&1 || colima start
  colima ssh -- bash -lc 'cat > /tmp/zscaler-bundle.pem' < "$BUNDLE"
  colima ssh -- bash -s <<EOS
set -euo pipefail
REG_DIR="/etc/docker/certs.d/${REGISTRY}"
SYS_DIR="/usr/local/share/ca-certificates/zscaler"

sudo mkdir -p "\$REG_DIR" "\$SYS_DIR"
sudo cp /tmp/zscaler-bundle.pem "\$REG_DIR/ca.crt"

# 인증서 낱장 분리 후 fingerprint 파일명으로 저장
awk '/-----BEGIN CERTIFICATE-----/{n++} n{print > ("/tmp/zca-" n ".crt")}' /tmp/zscaler-bundle.pem
for f in /tmp/zca-*.crt; do
  fp="\$(openssl x509 -in "\$f" -noout -fingerprint -sha256 | sed 's/.*=//' | tr -d ':')"
  sudo cp "\$f" "\$SYS_DIR/\${fp}.crt"
done

sudo update-ca-certificates
EOS
  colima restart
}

: > "$BUNDLE"
extract_from_keychains
extract_from_tls_chain
install_to_colima

echo "완료: docker pull로 검증하세요."

실행 및 검증

# 스크립트 저장 후 실행
REGISTRY=your-registry.example.com bash fix-zscaler-ca.sh

# 검증
docker pull your-registry.example.com/your-image:latest

성공하면 Colima VM 내부 CA 적용 상태를 이렇게 확인할 수 있다.

colima ssh -- bash -lc \
  'ls /etc/docker/certs.d/your-registry.example.com/ && \
   grep -c Zscaler /etc/ssl/certs/ca-certificates.crt'

Rancher Desktop / OrbStack 환경

Colima 대신 다른 Docker 런타임을 쓴다면 인증서를 복사하는 방법만 다르다.

런타임VM 접근재시작
Colimacolima ssh -- bash -lc '...'colima restart
Rancher Desktoprdctl shell bash -lc '...'rdctl shutdown && open -a "Rancher Desktop"
OrbStackorb ssh docker bash -lc '...'orb restart docker

재발 체크리스트

Zscaler CA 인증서는 보안 정책 변경 시 교체되거나 만료될 수 있다. 같은 오류가 다시 나타나면 순서대로 점검한다.

# 1. Docker context 확인
docker context show
docker info | grep -E 'Operating System|Name'

# 2. Colima VM 내부 CA 상태
colima ssh -- bash -lc \
  'ls -la /etc/docker/certs.d/your-registry.example.com/ca.crt && \
   grep -c Zscaler /etc/ssl/certs/ca-certificates.crt 2>/dev/null || echo 0'

# 3. TLS 검증
colima ssh -- bash -lc \
  'echo | openssl s_client -connect your-registry.example.com:443 \
   -CAfile /etc/ssl/certs/ca-certificates.crt 2>/dev/null | \
   grep "Verify return code"'

Verify return code: 0 (ok) 가 나오지 않으면 스크립트를 다시 실행한다.


핵심 요약

  • macOS에서 Colima를 사용하면 Docker daemon은 VM 내부 Linux에서 실행된다.
  • Zscaler가 TLS를 재서명하므로 Root CA 하나만으로는 부족하다. Intermediate CA도 수집해야 한다.
  • 인증서는 /etc/docker/certs.d/<registry>/ca.crt 시스템 CA 번들 양쪽에 설치한다.
  • CA 설치 후에는 반드시 Colima를 재시작한다.