컨테이너 취약점 제로를 향해: Chainguard(cgr.dev)와 Docker Hardened Images(dhi.io) 비교
CI/CD 파이프라인에서 컨테이너 이미지 보안 스캔을 돌리다 보면 수십, 수백 개의 취약점(CVE) 경고를 마주하게 됩니다.
기존 Debian이나 Ubuntu, 심지어 경량화된 Alpine 기반 이미지를 사용하더라도 불필요한 패키지나 쉘 때문에 검출되는 CVE를 완전히 피하기는 어렵습니다.
이러한 문제를 해결하기 위해 최근 많은 엔터프라이즈 환경에서 '0-CVE'를 지향하는 보안 베이스 이미지 도입을 적극 검토하고 있습니다.
이번 글에서는 컨테이너 공급망 보안의 선두 주자인 Chainguard(cgr.dev)와 Docker의 자체 보안 이미지인 Docker Hardened Images(dhi.io)를 비교해 보고, 실무 적용 시 겪을 수 있는 혼란과 올바른 멀티스테이지 빌드 패턴을 분석해 보겠습니다.
cgr.dev는 왜 Docker Hub 공식 인증이 없을까?
Chainguard 이미지를 접할 때 가장 먼저 드는 의문 중 하나는 "Docker Hub에서 왜 공식 배지(Docker Official Image)가 보이지 않는가?"입니다.
확고한 업계 인지도에 비해 공신력이 부족한 것은 아닌지 의아할 수 있습니다.
결론부터 말하자면 이는 공신력의 문제가 아니라 레지스트리 도메인의 분리와 Docker의 비즈니스 정책 때문입니다.
Docker Hub 배지 체계의 이해
Docker Hub의 생태계는 크게 세 가지로 나뉩니다.
Docker Official Images (DOI): alpine, python, node처럼 네임스페이스 없이 단일 단어로 호출되는 오픈소스 커뮤니티 주관 표준 이미지입니다.
Docker Verified Publisher (DVP): 검증된 엔터프라이즈 기업이 Docker Hub에 유료 호스팅하는 서드파티 이미지입니다 (chainguard/* 네임스페이스).
독립 OCI 레지스트리 (cgr.dev): Chainguard는 핵심 이미지를 Docker Hub가 아닌 자체 레지스트리에서 배포합니다.
Chainguard의 핵심 개발진은 Google에서 Distroless, Sigstore, Cosign 등을 주도한 엔지니어들입니다.
이들은 매일 수천 개의 이미지를 자동으로 재빌드하고, 전자 서명과 SBOM을 실시간으로 연동하며 엄격한 SLA를 제공하기 위해 자체 인프라(cgr.dev)를 사용합니다.
즉, cgr.dev는 Docker Hub 외부의 독립 레지스트리이기 때문에 기술적으로 Docker Hub 배지가 붙지 않는 것이 자연스러운 구조입니다.
Chainguard vs Docker Hardened Images (DHI)
최근 Docker사에서도 엔터프라이즈 하드닝 이미지 제품군인 DHI(dhi.io)를 내놓으면서 본격적인 경쟁 구도가 형성되었습니다.
두 솔루션의 핵심 차이를 정리하면 다음과 같습니다.
Chainguard (cgr.dev)
기반 OS: Wolfi / Chainguard OS (자체 빌드)
설계 철학: 완전한 Distroless (공격 표면 원천 배제)
카탈로그 규모: 3,000+ (광범위한 런타임 및 FIPS 지원)
CVE 패치 주기: 유료 기준 Critical 평균 20시간 이내 자체 패치
디버깅 환경: 극도로 제한적 (Shell 부재, 임시 컨테이너 필수)
전환 공수: 패키지 경로/이름 변경에 따른 Dockerfile 수정 필요
Docker Hardened Images (dhi.io)
기반 OS: Debian / Alpine 기반 하드닝
설계 철학: 기존 배포판 호환성 유지 및 패키지 경량화
카탈로그 규모: 500+ (주요 런타임 및 핵심 도구 중심)
CVE 패치 주기: DHI 구독 티어별 SLA 차등 (최대 7일 이내)
디버깅 환경: 보통 (익숙한 Linux 환경 및 디버그 도구 선택)
전환 공수: FROM 교체 위주의 낮은 전환 장벽
wolfi-base에 쉘이 존재하는 이유
Chainguard를 처음 사용할 때 흔히 겪는 혼선이 있습니다.
"Distroless라 쉘이 전혀 없다고 들었는데, wolfi-base에는 왜 쉘(/bin/sh)과 패키지 관리자(apk)가 들어있을까?"라는 점입니다.
이는 Chainguard의 이미지 계층 구조를 이해하면 쉽게 풀립니다.
이미지 계층 구조 흐름
Plain Text
1
2
3
4
[OS 베이스] cgr.dev/chainguard/wolfi-base ➔ Shell(O), apk(O) : 빌드 및 패키징용
[런타임 (Dev)] cgr.dev/chainguard/python:*-dev ➔ Shell(O), apk(O) : 개발/로컬 테스트용
[런타임 (Prod)] cgr.dev/chainguard/python:latest ➔ Shell(X), apk(X) : 배포 전용 Distroless
[초경량 바이너리] cgr.dev/chainguard/static ➔ Shell(X), glibc/musl : Go/Rust 정적 빌드용wolfi-base는 Alpine의 기본 이미지처럼 애플리케이션을 컴파일하고 의존성을 다운로드하기 위한 빌더(Builder) 역할입니다. 따라서 빌드 명령어를 실행할 최소한의 쉘과 패키지 관리자가 필수적입니다.
반면 실제 프로덕션 런타임 태그(:latest)에는 쉘과 패키지 관리자가 완전히 제거되어 있어 엔트리포인트 실행만 가능합니다.
실무 적용: 멀티스테이지 빌드 패턴
운영 환경에서 취약점을 0에 수렴하게 만들려면 wolfi-base를 최종 이미지로 사용해서는 안 됩니다.
빌더 단계에서만 wolfi-base를 활용하고, 최종 결과물은 쉘이 없는 런타임 이미지에 담는 멀티스테이지 빌드(Multi-stage Build)를 구성해야 합니다.
Spring Boot 애플리케이션을 예시로 살펴보겠습니다.
Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# ----------------------------------------------------------------
# 1단계: 빌드 환경 (Shell과 apk를 사용해 빌드 진행)
# ----------------------------------------------------------------
FROM cgr.dev/chainguard/wolfi-base AS builder
# 필요한 빌드 도구 및 OpenJDK 21 설치
RUN apk update && apk add --no-cache openjdk-21 git
WORKDIR /build
COPY gradlew .
COPY gradle ./gradle
COPY build.gradle settings.gradle ./
COPY src ./src
# 실행 가능한 Fat Jar 빌드
RUN ./gradlew bootJar --no-daemon
# ----------------------------------------------------------------
# 2단계: 프로덕션 런타임 환경 (Shell과 패키지 관리자가 완전히 제거된 단계)
# ----------------------------------------------------------------
FROM cgr.dev/chainguard/jre:latest
WORKDIR /app
# 빌더 단계의 아티팩트만 복사
COPY --from=builder /build/build/libs/*.jar app.jar
# 보안 강화를 위한 non-root 사용자 실행
USER nonroot
# 쉘이 없으므로 반드시 Exec 형식(JSON Array)으로 선언
ENTRYPOINT ["java", "-jar", "app.jar"]업그레이드의 기대 효과와 주의점
기대 효과
공격 표면(Attack Surface) 원천 차단: 컨테이너 내부에 쉘(/bin/sh)이 아예 존재하지 않으므로, 원격 코드 실행(RCE) 취약점이 발생하더라도 공격자가 쉘을 획득하거나 추가 악성 도구를 다운로드(curl, wget)하는 행위 자체가 불가능해집니다.
보안 스캔 통과: Trivy, Grype 등의 보안 스캐너 파이프라인에서 취약점 탐지 건수를 즉시 제거하거나 최소화할 수 있습니다.
주의점
디버깅 방식의 변화: docker exec -it <container> /bin/sh 명령어로 컨테이너에 직접 진입해 문제를 분석하던 기존 방식이 불가능합니다. 로그 중앙화(ELK, CloudWatch 등)가 선행되어야 하며, Kubernetes 환경이라면 kubectl debug를 통해 임시 디버그 컨테이너를 부착하는 패턴에 익숙해져야 합니다.
엔트리포인트 문법 주의: 쉘이 없기 때문에 ENTRYPOINT java -jar app.jar 형태로 작성하면 내부적으로 /bin/sh -c를 호출하다가 컨테이너 기동이 즉시 실패합니다. 반드시 ["java", "-jar", "app.jar"] 형태의 Exec 문법을 준수해야 합니다.
마무리
컨테이너 보안은 단순히 보안 스캐너의 경고 숫자를 지우는 요식 행위가 아니라, 시스템 침해 가능성을 사전에 차단하는 기본 설계입니다.
조직의 규정 준수 요건과 외부 감사 기준이 매우 엄격하고 빠른 패치 SLA가 최우선이라면 Chainguard(cgr.dev)가 탁월한 선택지가 될 것입니다.
반면 이미 Docker Enterprise 라이선스를 적극 활용 중이고 기존 Linux 배포판의 호환성을 유지하면서 전환 공수를 줄이고 싶다면 Docker Hardened Images(dhi.io)가 현실적인 대안이 됩니다.
어떤 기술을 선택하든, 빌드 도구와 런타임 환경을 분리하는 멀티스테이지 빌드의 기본 원칙을 지킨다면 한층 더 견고하고 안전한 운영 환경을 만들어갈 수 있을 것입니다.
해당 글은 AI를 통해 제작되었습니다.

Vue 3.5 신규 기능 파헤치기: Reactive Props Destructure와 useTemplateRef
Vue 3.5에서 정식 지원되는 Reactive Props Destructure의 컴파일 원리와 useTemplateRef를 통한 안전한 템플릿 참조 패턴을 정리합니다.
Vue
