Published on

Docker 이미지 SBOM+SLSA로 공급망 보안 CI/CD

Authors

서드파티 라이브러리와 베이스 이미지가 얽힌 컨테이너 환경에서 공급망 공격은 더 이상 "가능성"이 아니라 "전제"에 가깝습니다. 단순히 취약점 스캐너를 붙이는 것만으로는 부족합니다.

  • 무엇이 들어갔는지(구성요소) 추적 가능한 SBOM(Software Bill of Materials)
  • 누가/어떻게 만들었는지(출처와 무결성) 검증 가능한 SLSA(Supply-chain Levels for Software Artifacts)

이 두 축을 CI/CD에 녹이면, "빌드된 이미지"를 신뢰의 대상으로 승격시키고 배포 단계에서 정책 기반으로 차단할 수 있습니다. 이 글에서는 Docker 이미지 기준으로 SBOM 생성과 SLSA 스타일의 빌드 증명(프로비넌스)을 남기고, 레지스트리와 쿠버네티스 배포 파이프라인에서 검증하는 흐름을 정리합니다.

왜 SBOM과 SLSA를 같이 해야 하나

SBOM만으로 부족한 이유

SBOM은 "아티팩트 안에 무엇이 들어있는지"를 알려줍니다. 하지만 아래 질문에는 답하지 못합니다.

  • 이 SBOM이 실제로 해당 이미지에서 추출된 것이 맞나
  • 누가 만들었고, CI에서 어떤 소스 커밋으로 빌드됐나
  • 빌드 과정이 재현 가능하고 변조되지 않았나

즉, SBOM은 "내용물 목록"이고, 공급망 보안에서 또 하나의 축은 "출처 증명"입니다.

SLSA가 채워주는 부분

SLSA는 "소스에서 아티팩트까지"의 경로를 검증 가능하게 만드는 프레임워크입니다. 핵심은 다음입니다.

  • 빌드가 신뢰할 수 있는 환경에서 수행되었는가
  • 빌드에 사용된 입력(소스/의존성/빌드 스크립트)은 무엇인가
  • 결과물(이미지 다이제스트)은 무엇이며, 이 정보가 서명/검증 가능한 형태로 남았는가

실무적으로는 provenance(프로비넌스)라는 빌드 증명 문서를 생성하고, 이를 서명해 OCI 레지스트리(또는 별도 저장소)에 보관한 뒤, 배포 시점에 검증합니다.

전체 아키텍처: "생성"과 "검증"을 분리하라

권장 파이프라인을 단계로 나누면 다음과 같습니다.

  1. 빌드: Docker 이미지 생성(가능하면 BuildKit)
  2. SBOM 생성: 이미지 기준으로 SBOM 생성(SPDX 또는 CycloneDX)
  3. 프로비넌스 생성: SLSA 스타일 provenance 생성
  4. 서명: 이미지 다이제스트 및 SBOM/프로비넌스를 cosign으로 서명
  5. 배포 전 검증: CI 또는 클러스터 Admission 정책에서 서명/정책 검증

여기서 중요한 포인트는 태그가 아니라 다이제스트 기반으로 검증하는 것입니다. 태그는 가변이고, 다이제스트는 불변입니다.

실전 1: Docker 이미지 빌드 시 다이제스트를 기준으로 고정하기

먼저 이미지 빌드 후 다이제스트를 얻어 후속 작업(SBOM/서명/정책)에 "단일 진실"로 사용합니다.

# BuildKit 사용 권장
export DOCKER_BUILDKIT=1

IMAGE_REPO=ghcr.io/acme/api
IMAGE_TAG=${GITHUB_SHA}

docker build -t ${IMAGE_REPO}:${IMAGE_TAG} .
docker push ${IMAGE_REPO}:${IMAGE_TAG}

# 다이제스트 조회
DIGEST=$(docker buildx imagetools inspect ${IMAGE_REPO}:${IMAGE_TAG} --format '{{.Digest}}')
IMAGE_REF=${IMAGE_REPO}@${DIGEST}

echo "IMAGE_REF=${IMAGE_REF}" >> $GITHUB_ENV

IMAGE_REF 형태(repo@sha256:...)를 이후 모든 단계에서 사용하면, "나중에 태그가 바뀌어서 다른 이미지가 배포되는" 류의 사고를 줄일 수 있습니다.

실전 2: SBOM 생성(SPDX 또는 CycloneDX)과 OCI 첨부

SBOM 도구는 syft가 범용적이고, 생성 결과를 OCI 아티팩트로 레지스트리에 "첨부"할 수 있어 운영이 깔끔합니다.

Syft로 SBOM 생성

# SPDX JSON 생성 예시
syft ${IMAGE_REF} -o spdx-json=sbom.spdx.json

# CycloneDX JSON 생성 예시
syft ${IMAGE_REF} -o cyclonedx-json=sbom.cdx.json

SBOM을 이미지에 첨부(Attestation/OCI artifact)

cosign을 쓰면 SBOM을 레지스트리에 올려 "이미지 다이제스트"에 매달 수 있습니다.

# keyless 서명/첨부를 위해 OIDC 기반 환경(GitHub Actions 등) 가정

# SBOM 첨부 (SPDX)
cosign attest \
  --predicate sbom.spdx.json \
  --type spdxjson \
  ${IMAGE_REF}

이렇게 올리면, 레지스트리에 "이미지"와 "SBOM attestation"이 함께 존재하게 됩니다.

실전 3: SLSA 프로비넌스 생성과 서명

SLSA를 "정석"으로 올리려면 SLSA generator를 쓰는 방식이 좋습니다. GitHub Actions 환경이라면 slsa-framework/slsa-github-generator 계열을 고려할 수 있고, 범용적으로는 cosign attest로 provenance 문서를 만들어 첨부하는 접근도 가능합니다.

여기서는 이해를 돕기 위해 provenance를 "만들고 첨부"하는 흐름을 단순화해 설명합니다.

최소 형태의 provenance 예시(개념)

아래는 개념적으로 어떤 필드가 들어가는지 보여주는 예시입니다. 실제 운영에서는 표준 스키마(SLSA provenance v0.2 등)를 맞추는 것이 좋습니다.

{
  "buildType": "https://example.com/buildkit",
  "builder": {
    "id": "https://github.com/actions/runner"
  },
  "invocation": {
    "configSource": {
      "uri": "https://github.com/acme/api",
      "digest": {
        "sha1": "GIT_COMMIT_SHA"
      }
    }
  },
  "metadata": {
    "buildStartedOn": "2026-02-26T00:00:00Z",
    "buildFinishedOn": "2026-02-26T00:05:00Z"
  },
  "subject": [
    {
      "name": "ghcr.io/acme/api",
      "digest": {
        "sha256": "IMAGE_DIGEST"
      }
    }
  ]
}

provenance 첨부

cosign attest \
  --predicate provenance.json \
  --type slsaprovenance \
  ${IMAGE_REF}

운영 관점에서 핵심은 "이 provenance가 특정 이미지 다이제스트에 귀속"된다는 점입니다.

실전 4: Keyless 서명으로 운영 복잡도 줄이기

공급망 보안은 "도구"보다 "운영"에서 실패합니다. 키 관리가 복잡하면 파이프라인이 무너집니다. GitHub Actions 같은 OIDC 지원 CI라면 cosign의 keyless 방식을 우선 검토할 가치가 큽니다.

  • CI가 OIDC 토큰 발급
  • fulcio가 단기 인증서 발급
  • rekor 투명성 로그에 기록

즉, 별도 개인키를 CI에 넣지 않고도 "누가 서명했는지"를 검증 가능한 형태로 남길 수 있습니다.

실전 5: 배포 전 검증(정책) 없으면 반쪽짜리다

SBOM과 provenance를 만들고 서명해도, 배포 단계에서 이를 강제하지 않으면 "있어도 안 쓰는" 데이터가 됩니다.

쿠버네티스라면 Admission Controller 계열(예: Kyverno, OPA Gatekeeper, Sigstore policy-controller)을 통해 다음을 강제할 수 있습니다.

  • 서명된 이미지만 허용
  • 특정 CI 주체(issuer, subject)로 서명된 것만 허용
  • SBOM/프로비넌스 attestation이 존재해야 허용
  • 취약점 정책(예: CRITICAL 존재 시 차단)과 결합

아래는 cosign verify로 CI에서 사전 검증하는 예시입니다.

# 서명 검증 (keyless 기준으로 인증서 주체/발급자 제한)
cosign verify \
  --certificate-identity "https://github.com/acme/api/.github/workflows/release.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ${IMAGE_REF}

attestation 검증도 마찬가지로 수행할 수 있습니다.

# SBOM attestation 조회/검증(타입 기준)
cosign verify-attestation \
  --type spdxjson \
  ${IMAGE_REF}

# provenance attestation 조회/검증
cosign verify-attestation \
  --type slsaprovenance \
  ${IMAGE_REF}

CI/CD 예시: GitHub Actions로 묶기

아래 워크플로는 "빌드-푸시-다이제스트 고정-SBOM 생성-프로비넌스 첨부"까지의 뼈대를 보여줍니다. 실제로는 조직 정책에 맞게 권한 최소화, 캐시, 멀티아키 등을 추가하세요.

name: release
on:
  push:
    branches: ["main"]

permissions:
  contents: read
  packages: write
  id-token: write

jobs:
  build-sign-attest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        run: |
          IMAGE_REPO=ghcr.io/${{ github.repository }}
          IMAGE_TAG=${{ github.sha }}
          docker buildx build --push -t ${IMAGE_REPO}:${IMAGE_TAG} .

          DIGEST=$(docker buildx imagetools inspect ${IMAGE_REPO}:${IMAGE_TAG} --format '{{.Digest}}')
          echo "IMAGE_REF=${IMAGE_REPO}@${DIGEST}" >> $GITHUB_ENV

      - name: Install syft
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

      - name: Install cosign
        run: |
          curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 -o /usr/local/bin/cosign
          chmod +x /usr/local/bin/cosign

      - name: Generate SBOM
        run: |
          syft ${{ env.IMAGE_REF }} -o spdx-json=sbom.spdx.json

      - name: Attach SBOM attestation
        run: |
          cosign attest --predicate sbom.spdx.json --type spdxjson ${{ env.IMAGE_REF }}

      - name: Create provenance (simple)
        run: |
          cat > provenance.json << 'EOF'
          {
            "buildType": "https://example.com/buildx",
            "builder": { "id": "https://github.com/actions/runner" },
            "invocation": {
              "configSource": {
                "uri": "https://github.com/${{ github.repository }}",
                "digest": { "sha1": "${{ github.sha }}" }
              }
            },
            "subject": [
              {
                "name": "ghcr.io/${{ github.repository }}",
                "digest": { "sha256": "${{ env.IMAGE_REF }}" }
              }
            ]
          }
          EOF

      - name: Attach provenance attestation
        run: |
          cosign attest --predicate provenance.json --type slsaprovenance ${{ env.IMAGE_REF }}

주의할 점이 하나 있습니다. 위 예시는 provenance의 subject.digestIMAGE_REF 전체가 들어가므로, 실제로는 sha256 값만 정확히 넣도록 가공해야 합니다. 즉, repo@sha256:...에서 sha256:...만 추출하세요.

운영 팁: SBOM과 SLSA를 "정책"으로 바꾸는 방법

1) "취약점 0"이 아니라 "정책 준수"로 목표를 바꿔라

현실적으로 취약점이 0인 이미지는 드뭅니다. 대신 다음처럼 정책을 설계합니다.

  • CRITICAL은 차단, HIGH는 예외 승인 필요
  • SBOM 미존재는 무조건 차단
  • provenance가 특정 워크플로/브랜치에서 나온 것만 허용

이렇게 하면 보안과 개발 생산성 사이의 균형점이 생깁니다.

2) 쿠버네티스 권한 모델과 같이 보라

공급망 보안은 "누가 이미지를 만들었나"뿐 아니라 "클러스터가 무엇을 할 수 있나"와도 연결됩니다. 예를 들어 EKS에서 서비스어카운트 권한(IRSA)이 과도하거나 누락되면, 배포 파이프라인이 우회되거나 운영 장애로 이어질 수 있습니다. 클러스터 권한 문제를 다룰 때는 EKS IRSA 권한 누락 AccessDenied 해결 가이드도 같이 점검하는 것이 좋습니다.

3) 노드 스케일링/스케줄링 변화가 검증 흐름을 깨지 않게

오토스케일링(Karpenter 등)으로 노드가 자주 교체되면, 이미지 풀 정책이나 Admission 구성의 일관성이 더 중요해집니다. 노드 확장 환경에서 배포 안정성까지 같이 챙기려면 EKS Karpenter로 노드 자동확장·비용 30% 절감에서 다룬 운영 포인트들도 참고할 수 있습니다.

자주 터지는 실패 포인트 체크리스트

  • 태그 기반으로 검증한다: 반드시 다이제스트로 고정
  • SBOM은 생성했지만 레지스트리에 첨부하지 않는다: 배포 단계에서 조회 불가
  • 서명은 했지만 인증서 identity 제한이 없다: "아무나 서명"이 통과될 수 있음
  • provenance 스키마가 제각각이다: 표준 스키마로 정리하고 파서/정책을 단순화
  • 멀티아키 이미지에서 다이제스트 혼동: manifest list와 platform digest를 구분

결론: "만드는 것"보다 "거부할 수 있는 것"이 핵심

Docker 이미지 공급망 보안에서 SBOM과 SLSA는 각각 "구성요소 가시성"과 "출처 검증"을 담당합니다. 하지만 둘 다 **배포 시점의 강제(정책)**로 연결되지 않으면 효과가 제한적입니다.

정리하면, 실무에서 가장 재현성 높은 접근은 다음 순서입니다.

  • 이미지 다이제스트 고정
  • SBOM 생성 후 OCI에 첨부
  • provenance 생성 후 첨부
  • keyless 서명으로 운영 복잡도 감소
  • Admission/CI에서 서명과 attestation 존재를 강제

이 흐름을 한 번 구축해두면, 이후에는 취약점 스캐닝, 라이선스 정책, 베이스 이미지 교체 자동화 같은 확장도 자연스럽게 붙일 수 있습니다.