# CI 빌더가 사설 레지스트리 인증서를 신뢰하지 못할 때 - buildx와 dind
사내 레지스트리에 사설 CA로 발급한 인증서를 쓰는 환경에서 CI를 구성하면, docker login은 성공하는데 docker buildx build --push만 x509: 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) 서비스를 쓰는 구성이 여기에 해당합니다.
핵심은 두 가지입니다.
- DinD 데몬 컨테이너에
/etc/docker/certs.d/<registry>/ca.crt를 넣는다. - 빌드를
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 대상뿐 아니라 베이스 이미지를 가져오는 레지스트리도 함께 등록해야 합니다.
# 참고
- Docker - Buildx docker-container driver (opens new window)
- Docker - BuildKit
buildkitd.tomlconfiguration (opens new window) - Docker - Configure BuildKit (Setting registry certificates) (opens new window)
- Docker - Buildx drivers overview (opens new window)
- Docker - Use self-signed certificates with a registry (opens new window)