# CI 빌더가 사설 레지스트리 인증서를 신뢰하지 못할 때 - buildx와 dind

사내 레지스트리에 사설 CA로 발급한 인증서를 쓰는 환경에서 CI를 구성하면, docker login은 성공하는데 docker buildx build --pushx509: certificate signed by unknown authority로 실패하는 상황을 만납니다. 같은 러너, 같은 인증서인데 명령에 따라 결과가 갈립니다.

원인은 빌드가 실행되는 위치입니다. 이 글에서는 buildx 드라이버별로 빌드가 어디서 도는지, 왜 호스트 데몬의 인증서 설정을 물려받지 못하는지, 그리고 CA를 주입하는 두 가지 해법을 정리합니다.

# 1. 증상 - 명령마다 결과가 다르다

docker login registry.example.com          # ✅ 성공
docker pull registry.example.com/base:1.0  # ✅ 성공
docker buildx build --push -t registry.example.com/app:1.0 .
# ✗ ERROR: failed to solve: failed to push registry.example.com/app:1.0:
#   failed to do request: Head "https://registry.example.com/v2/app/blobs/...":
#   tls: failed to verify certificate: x509: certificate signed by unknown authority

앞의 두 명령은 호스트의 Docker 데몬이 처리하고, 세 번째는 BuildKit이 처리합니다. 둘은 서로 다른 프로세스이고, 신뢰 저장소도 따로 씁니다.

# 2. 빌드는 어디서 실행되는가 - 드라이버

buildx는 빌드를 실행할 백엔드를 드라이버로 고릅니다.

드라이버 BuildKit 실행 위치 호스트 데몬 인증서 설정 상속
docker 호스트 Docker 데몬에 내장된 BuildKit 상속함
docker-container 별도 컨테이너 상속하지 않음
kubernetes 클러스터의 파드 상속하지 않음
remote 원격 BuildKit 데몬 상속하지 않음

docker-container 드라이버에 대한 문서 설명이 핵심입니다. 이 드라이버는 "a managed and customizable BuildKit environment in a dedicated Docker container" 를 만들고, buildx는 그 컨테이너에 빌드를 제출합니다.

즉 빌드를 수행하는 주체는 호스트 데몬이 아니라 그 컨테이너 안의 buildkitd입니다. 호스트에 CA를 설치해도 컨테이너 안에는 없습니다. CI에서 docker/setup-buildx-action 같은 단계를 쓰면 기본으로 이 드라이버가 선택되므로, 사설 CA 환경에서 갑자기 실패하는 전형적인 조합이 만들어집니다.

# 현재 빌더와 드라이버 확인
docker buildx ls
# NAME/NODE       DRIVER/ENDPOINT   STATUS   PLATFORMS
# builder*        docker-container           linux/amd64
#  \_ builder0     \_ unix:///var/run/docker.sock  running
# default         docker                     linux/amd64

# 3. 데몬 쪽 신뢰 저장소

호스트 Docker 데몬은 레지스트리별 인증서를 디렉터리 규칙으로 읽습니다.

/etc/docker/certs.d/
└── registry.example.com/
    └── ca.crt          # 이 레지스트리에 대해 신뢰할 CA

포트가 붙은 레지스트리는 디렉터리 이름에 포트까지 포함해야 합니다(registry.example.com:5000). 이 설정을 넣으면 pull/push/login은 동작하지만, 2절의 이유로 docker-container 빌더는 여전히 실패합니다.

# 4. 해법 A - 빌더에 CA를 직접 주입한다

BuildKit은 자체 설정 파일 buildkitd.toml에서 레지스트리별 TLS 설정을 읽습니다.

# buildkitd.toml
[registry."registry.example.com"]
  ca = ["/etc/certs/ca.pem"]

# mTLS 를 요구하는 레지스트리라면
  [[registry."registry.example.com".keypair]]
    key = "/etc/certs/client-key.pem"
    cert = "/etc/certs/client.pem"
필드 의미
ca 신뢰할 CA 인증서 경로 목록
insecure 자체 서명 인증서를 검증 없이 허용
http 평문 HTTP 사용
mirrors 미러 레지스트리 목록
keypair mTLS 클라이언트 키/인증서

빌더를 만들 때 --config로 넘깁니다.

docker buildx create \
  --name ci-builder \
  --driver docker-container \
  --config ./buildkitd.toml \
  --use

컨테이너 안에서 이 파일은 rootful 모드 기준 /etc/buildkit/buildkitd.toml에 놓입니다(rootless는 ~/.config/buildkit/buildkitd.toml).

주의할 점은 ca·keypair에 적은 경로가 docker buildx create를 실행하는 호스트 쪽 경로라는 것입니다. buildx v0.7.0부터는 빌더를 만들 때 이 파일들을 읽어 컨테이너의 /etc/buildkit/certs/<registry>/ 아래로 복사하고, 컨테이너 안 buildkitd.toml의 경로도 그 위치로 바꿔 씁니다. 따로 마운트할 필요는 없지만, 생성 시점의 스냅샷이므로 인증서를 교체하면 빌더를 다시 만들어야 합니다(--config는 현재 --buildkitd-config의 숨겨진 별칭입니다).

docker buildx create \
  --name ci-builder --driver docker-container \
  --buildkitd-config ./buildkitd.toml \
  --bootstrap --use
# 인증서가 복사되고 설정 경로가 바뀌었는지 확인
docker exec buildx_buildkit_ci-builder0 ls /etc/buildkit/certs/registry.example.com/
docker exec buildx_buildkit_ci-builder0 cat /etc/buildkit/buildkitd.toml

insecure = true는 검증 자체를 건너뛰므로 빠르게 우회할 수는 있지만, 중간자 공격에 그대로 노출됩니다. 사내망이라도 CA를 넣는 쪽이 정석입니다.

# 5. 해법 B - CA를 신뢰하는 데몬에서 빌드한다

빌더에 설정을 주입하는 대신, CA를 이미 신뢰하는 Docker 데몬을 띄우고 그 데몬으로 빌드하는 방법입니다. CI에서 DinD(Docker-in-Docker) 서비스를 쓰는 구성이 여기에 해당합니다.

핵심은 두 가지입니다.

  1. DinD 데몬 컨테이너에 /etc/docker/certs.d/<registry>/ca.crt를 넣는다.
  2. 빌드를 docker-container 빌더가 아니라 그 데몬이 수행하게 한다. 즉 docker build를 쓰거나, buildx를 쓰더라도 docker 드라이버를 사용한다.
# 개념 예시 - 러너에서 dind 데몬을 띄우고 CA 를 심는다
services:
  docker:
    image: docker:27-dind
    options: >-
      --privileged
    volumes:
      - ./ca.crt:/etc/docker/certs.d/registry.example.com/ca.crt:ro
export DOCKER_HOST=tcp://docker:2375
docker buildx use default          # docker 드라이버 = 데몬 내장 BuildKit
docker buildx build --push -t registry.example.com/app:1.0 .

이 방식의 장점은 신뢰 설정이 한 곳에 모인다는 것입니다. pull·push·build가 전부 같은 데몬을 거치므로, 레지스트리를 추가하거나 인증서를 교체할 때 손댈 곳이 하나입니다. 반면 docker 드라이버는 멀티 플랫폼 빌드나 일부 캐시 백엔드 같은 BuildKit 고급 기능에 제약이 있습니다.

# 6. 어느 쪽을 고를 것인가

기준 A: 빌더에 CA 주입 B: 신뢰하는 데몬에서 빌드
멀티 플랫폼 빌드 가능 제약 있음
원격 캐시 백엔드 폭넓게 지원 일부 제약
설정 지점 데몬 + 빌더 두 곳 데몬 한 곳
레지스트리 추가 시 두 곳 모두 갱신 한 곳만 갱신
권한 요구 낮음 DinD는 특권 컨테이너가 필요한 경우가 많음

멀티 아키텍처 이미지를 만들어야 한다면 A가 사실상 강제됩니다. 단일 플랫폼이고 사설 레지스트리·미러·프록시 캐시가 여러 개 얽혀 있다면 B가 운영이 단순합니다. 어느 쪽이든 "빌드가 어느 프로세스에서 도는가" 를 먼저 확정해야 설정할 위치가 정해집니다.

# 7. 트러블슈팅

증상 원인 해결
login·pull은 되는데 buildx build --push만 x509 빌드가 별도 BuildKit 컨테이너에서 실행 빌더에 CA 주입 또는 데몬 드라이버 사용
buildkitd.toml을 줬는데 그대로 실패 빌더 생성 후 인증서를 교체했거나 레지스트리 이름 불일치 빌더 재생성, 컨테이너 안 /etc/buildkit/certs 확인
빌더 재생성 후 설정이 사라짐 빌더 인스턴스에 설정이 묶여 있음 빌더 생성 단계를 CI에 코드화
포트가 붙은 레지스트리만 실패 디렉터리·설정 키에 포트 누락 host:port 형태로 정확히 기입
base 이미지 pull 단계에서 실패 빌드 컨텍스트 안의 pull도 BuildKit이 수행 push 뿐 아니라 pull 대상 레지스트리도 등록

마지막 항목이 자주 누락됩니다. FROM registry.example.com/base:1.0처럼 사설 레지스트리에서 베이스 이미지를 가져오면, 그 pull도 BuildKit이 수행하므로 push 대상과 별개로 신뢰 설정이 필요합니다.

진단은 빌더가 실제로 무엇을 보고 있는지 확인하는 것부터 시작합니다.

docker buildx inspect ci-builder          # 드라이버·엔드포인트 확인
docker buildx build --progress=plain .    # 어느 단계에서 끊기는지

# 8. 마무리

  • docker 계열 명령이 되는데 buildx build만 인증서 오류가 난다면, 원인은 인증서가 아니라 빌드 실행 위치입니다.
  • docker-container 드라이버는 별도 컨테이너에서 BuildKit을 돌리므로 호스트 데몬의 /etc/docker/certs.d를 보지 않습니다.
  • 해법은 두 갈래입니다. buildkitd.toml로 빌더에 CA를 넣거나, CA를 이미 신뢰하는 데몬에서 빌드하게 하거나. 멀티 플랫폼이 필요하면 전자, 설정 지점을 줄이고 싶으면 후자입니다.
  • push 대상뿐 아니라 베이스 이미지를 가져오는 레지스트리도 함께 등록해야 합니다.

# 참고