왜 베이스 이미지가 문제가 되는가

운영 중인 컨테이너 이미지는 대부분 FROM debian:12-slim 같은 베이스 이미지 위에 애플리케이션 레이어를 얹은 구조다. 문제는 이 베이스에 OpenSSL, glibc, zlib 같은 시스템 패키지가 포함되어 있고, 여기서 CVE가 끊임없이 발견된다는 점이다. 애플리케이션 코드는 한 줄도 바뀌지 않았는데 이미지 스캔에서 High/Critical 취약점이 뜬다. 전통적으로는 애플리케이션을 통째로 재빌드해서 해결하지만, 이는 빌드 시간을 낭비하고 애플리케이션 레이어의 다이제스트까지 바꿔 불필요한 롤아웃을 유발한다.

리베이스와 재빌드의 차이

핵심 통찰은 "베이스만 바뀌었으면 베이스 레이어만 교체하면 된다"는 것이다. 애플리케이션 레이어는 베이스 위에 독립적으로 쌓이므로, 하위 레이어(old base)를 새 베이스(new base)로 스왑하는 것이 리베이스다.

항목전체 재빌드리베이스
소요 시간수 분(의존성 설치 포함)수 초(레이어 재조립)
앱 레이어 다이제스트변경됨유지됨
빌드 환경 필요필요불필요(레지스트리 조작만)
재현성툴체인 상태에 의존결정적

베이스 갱신 자동 감지

파이프라인의 트리거는 "업스트림 베이스의 다이제스트가 바뀌었는가"다. 태그(:12-slim)는 같아도 다이제스트는 패치될 때마다 변한다. 레지스트리 매니페스트를 조회해 저장된 다이제스트와 비교한다.

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

BASE="debian:12-slim"
STORED_DIGEST=$(cat .base-digest 2>/dev/null || echo "")

# 원격 매니페스트의 실제 다이제스트 조회 (pull 없이)
NEW_DIGEST=$(crane digest "$BASE")

if [[ "$NEW_DIGEST" == "$STORED_DIGEST" ]]; then
  echo "base unchanged, skip"
  exit 0
fi

echo "$NEW_DIGEST" > .base-digest
echo "changed=true" >> "$GITHUB_OUTPUT"

crane를 이용한 실제 리베이스

google/go-containerregistry의 crane rebase는 old-base와 new-base를 지정하면 그 사이 레이어를 교체해준다. 빌드 데몬 없이 레지스트리에서 직접 수행되므로 CI 러너가 가볍다.

APP_IMG=registry.example.com/api:1.4.2
OLD_BASE=debian:12-slim@sha256:OLD...
NEW_BASE=debian:12-slim@sha256:NEW...

crane rebase "$APP_IMG" \
  --old_base "$OLD_BASE" \
  --new_base "$NEW_BASE" \
  --tag "registry.example.com/api:1.4.2-rebased"

이미지를 FROM debian:12-slim 방식으로 빌드했다면, 원본 이미지 매니페스트의 어노테이션에 old-base 정보가 기록되어 있어 --old_base를 생략하고 --rebase 옵션만으로도 동작하게 만들 수 있다.

파이프라인으로 엮기

감지 → 리베이스 → 스캔 게이트 → 배포를 하나의 워크플로로 묶는다. 스캔에서 통과하지 못하면 새 이미지를 태그하지 않는 것이 핵심이다.

name: base-rebase
on:
  schedule: [{ cron: "0 * * * *" }]   # 매시간 업스트림 확인
jobs:
  rebase:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: detect
        id: detect
        run: ./scripts/detect-base.sh
      - name: rebase
        if: steps.detect.outputs.changed == 'true'
        run: ./scripts/rebase.sh
      - name: scan-gate
        if: steps.detect.outputs.changed == 'true'
        run: |
          trivy image --exit-code 1 --severity CRITICAL,HIGH \
            registry.example.com/api:1.4.2-rebased
      - name: promote
        if: steps.detect.outputs.changed == 'true'
        run: crane tag registry.example.com/api:1.4.2-rebased 1.4.2

주의점

ABI/호환성 경계

리베이스는 "앱 레이어가 새 베이스에서도 동작한다"는 가정에 의존한다. 베이스가 마이너 업데이트(패치)라면 안전하지만, glibc 메이저 버전이 바뀌거나 배포판이 올라가면(12→13) 동적 링크가 깨질 수 있다. 이때는 리베이스가 아니라 재빌드가 맞다. 감지 스크립트에서 태그가 아니라 다이제스트만 추적하되, 메이저 태그 변경은 별도 재빌드 경로로 분기해야 한다.

스캔 게이트를 프로모션 앞에

리베이스한 결과를 곧바로 운영 태그로 밀면, 새 베이스가 오히려 다른 취약점을 들고 올 수도 있다. 반드시 임시 태그로 만들어 스캔을 통과한 뒤에만 운영 태그로 승격한다.

서명과 검증

리베이스는 이미지 다이제스트를 바꾸므로 기존 cosign 서명이 무효화된다. 프로모션 직전에 재서명 단계를 넣고, 어드미션 컨트롤러(예: Kyverno)에서 서명 검증을 강제해 미서명 이미지가 배포되지 않게 한다.

재현성 기록

어떤 old-base에서 어떤 new-base로 갔는지 어노테이션과 별도 로그에 남긴다. 문제가 생겼을 때 직전 베이스로 되돌리는 롤백이 리베이스로 다시 수 초 만에 가능해진다.