도커 고급 강좌 #4 SBOM과 서명 — 공급망 보안의 입구

7 분 소요

#3의 보안 체크리스트가 한 컨테이너 안의 문제였다면, 이번 글은 한 단계 위 — **공급망(supply chain)**의 문제입니다. 이 이미지에 무엇이 들어 있는가, 그리고 누가 만든 것인가.

도커 고급 강좌 시리즈에서 이번 글의 위치:

왜 공급망 보안이 갑자기 중요해졌나 #

근래 몇 년 사이 큰 사고들이 연달아 일어났습니다.

  • 2020 SolarWinds — 정상적으로 서명된 빌드 산출물에 백도어가 들어 있었음
  • 2021 codecov bash uploader — 정상 도구의 다운로드 스크립트가 손상돼 비밀 유출
  • 2024 xz utils 백도어 — 널리 쓰이는 압축 도구에 신뢰받는 메인테이너가 백도어 삽입

공통점은 — 취약점이 코드 안이 아니라, 코드를 만들고 배포하는 과정에 끼어 들었다는 것 입니다. CVE 스캔만으로는 잡히지 않는 위협입니다. 이걸 다루는 도구가 SBOM서명 두 축입니다.

질문도구
이 이미지 안에 정확히 무엇이 들어 있나?SBOM (Software Bill of Materials)
이 이미지를 정말 그 사람/조직이 만들었나?서명 (cosign / sigstore)
이 이미지가 어떤 절차를 거쳐 만들어졌나?attestation / SLSA

SBOM이란 #

SBOM은 한 소프트웨어가 무엇으로 이뤄졌는지를 기계가 읽을 수 있는 형식으로 적은 명세서입니다. 비유하면 식품의 영양성분표와 비슷합니다.

표준 형식이 두 개 있습니다.

형식만든 곳특징
SPDXLinux Foundation가장 오래된 표준, 라이선스 정보에 강함
CycloneDXOWASP보안 시각, 의존성 그래프 표현이 풍부

도구마다 두 형식 모두 출력 가능합니다. 처음 쓸 땐 한쪽을 골라 일관되게 쓰면 됩니다.

Syft로 SBOM 생성 #

syft (Anchore)가 표준에 가깝습니다.

설치
brew install syft
# 또는
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
이미지의 SBOM 만들기
syft myapp:1.0 -o cyclonedx-json > sbom.cdx.json
syft myapp:1.0 -o spdx-json > sbom.spdx.json

출력 예 (CycloneDX의 일부):

sbom.cdx.json (요약)
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "components": [
    {
      "name": "openssl",
      "version": "3.0.13-1ubuntu0.1",
      "type": "library",
      "purl": "pkg:deb/ubuntu/openssl@3.0.13-1ubuntu0.1?arch=amd64"
    },
    {
      "name": "flask",
      "version": "3.0.3",
      "type": "library",
      "purl": "pkg:pypi/flask@3.0.3"
    }
  ]
}

각 컴포넌트의 **purl (Package URL)**이 정확한 식별자입니다. pkg:pypi/flask@3.0.3은 PyPI의 flask 3.0.3을 가리키는 표준 URL입니다.

docker buildx가 직접 만들기 #

빌드 시점에 SBOM을 attestation으로 첨부할 수 있습니다.

빌드 시 SBOM 첨부
docker buildx build \
  --sbom=true \
  --provenance=mode=max \
  -t ghcr.io/me/myapp:1.0 \
  --push .
  • --sbom=true — SBOM을 만들어 이미지에 attestation으로 붙임
  • --provenance=mode=max — 빌드 출처 정보(어떤 명령으로 어떤 입력에서 빌드됐는지) 까지 포함

이렇게 푸시한 이미지는 manifest list에 attestation 매니페스트가 함께 들어갑니다. imagetools inspect로 확인할 수 있습니다.

attestation 들여다보기
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
  --format '{{json .SBOM}}'

이게 가장 깔끔한 길입니다 — 빌드 한 번에 SBOM까지 함께 만들고, 같은 매니페스트에 묶어 보관합니다.

SBOM을 어디에 쓰는가 #

SBOM 자체는 그냥 데이터입니다. 가치는 그걸 들고 쿼리할 도구 들에서 나옵니다.

취약점 스캔 — Grype #

SBOM으로 스캔
grype sbom:./sbom.cdx.json

이미지 다운로드 없이, SBOM 한 파일만 있으면 취약점 스캔이 돕니다. CI의 캐시 / 운영 환경의 사후 추적에 매우 빠른 흐름입니다.

라이선스 컴플라이언스 #

라이선스 추출
syft myapp:1.0 -o spdx-tag-value | grep PackageLicenseDeclared | sort -u

이미지에 어떤 오픈소스 라이선스가 들어 있는지 — 컴플라이언스 보고에 자주 쓰는 정보입니다.

사고 대응 #

새로운 CVE가 발표됐을 때 (예: log4shell, xz), 이미 운영 중인 이미지 어디에 그 라이브러리가 들어 있나를 SBOM으로 쿼리합니다. CVE가 떨어지고 한 시간 안에 영향 범위를 보고하는 건 SBOM 없이는 어렵습니다.

서명 — Cosign #

이미지가 진짜 그 사람/조직이 만든 것인지 검증하는 도구로, Sigstore 프로젝트의 Cosign이 표준이 됐습니다.

설치
brew install cosign

키 기반 서명 (옛 방식) #

키 생성
cosign generate-key-pair
# cosign.key, cosign.pub
서명
cosign sign --key cosign.key ghcr.io/me/myapp:1.0

이미지에 서명이 붙어 레지스트리에 함께 저장됩니다.

검증
cosign verify --key cosign.pub ghcr.io/me/myapp:1.0

키를 관리해야 한다는 부담이 있습니다. 키가 유출되면 신뢰가 깨집니다. 이걸 풀어주는 게 다음 절의 키리스(keyless) 서명입니다.

키리스 서명 — OIDC + Fulcio + Rekor #

Sigstore의 핵심 아이디어는 장기 키 없이 OIDC 신원으로 한 번 서명하고, 그 사실을 공개 투명성 로그(Rekor)에 기록하는 것입니다.

키리스 서명 (CI에서)
cosign sign ghcr.io/me/myapp:1.0
# 브라우저가 열리며 OIDC 로그인 (또는 CI 환경에서 자동)
# Fulcio가 짧은 수명의 인증서를 발급
# 그걸로 서명
# Rekor (공개 투명성 로그)에 기록
키리스 검증
cosign verify \
  --certificate-identity-regexp 'https://github.com/me/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/me/myapp:1.0
  • certificate-identity-regexp — 누가 서명할 수 있는가 (URL 패턴)
  • certificate-oidc-issuer — 어느 OIDC 발급자가 발급한 인증서를 신뢰할지

GitHub Actions 위에서 도는 빌드는 OIDC 토큰을 자동으로 받을 수 있어, 환경변수 / 비밀 관리 없이 서명이 됩니다. 운영에선 거의 항상 키리스로 가는 편이 안전합니다.

GitHub Actions에서 — 한곳 모음 #

.github/workflows/build-sign.yml
name: build and sign
on: { push: { branches: [main] } }

permissions:
  contents: read
  packages: write
  id-token: write    # 키리스 서명에 필요

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

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

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

      - id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          sbom: true               # SBOM attestation
          provenance: mode=max     # provenance attestation

      - uses: sigstore/cosign-installer@v3

      - name: Sign image
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          cosign sign --yes ghcr.io/${{ github.repository }}@${DIGEST}

이 워크플로우 하나로:

  1. 이미지 빌드 + 멀티 아키 (#2)
  2. SBOM + provenance attestation 첨부
  3. 키리스 서명 (Rekor 기록)

거의 모든 공급망 보안 흐름이 한 워크플로우에서 끝납니다.

Attestation — SBOM과 서명을 묶기 #

지금까지 나온 조각들을 한 그림으로:

공급망 메타데이터
   image
     ├── manifest (linux/amd64)
     ├── manifest (linux/arm64)
     ├── attestation: SBOM (CycloneDX)
     ├── attestation: provenance (SLSA build info)
     └── signature (cosign)

이 모든 것이 같은 OCI 레지스트리에 같은 이미지의 메타데이터로 저장됩니다. imagetools inspect로 한 번에 들여다볼 수 있습니다.

attestations 보기
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
  --format '{{json .}}' | jq '.Manifest'

검증 정책은 도구 / 환경마다 다르지만, 운영 환경에서 자주 보는 패턴은:

  1. 이 이미지에 SBOM이 첨부돼 있는가 — 없으면 거부
  2. 이 이미지가 우리 조직의 OIDC 신원으로 서명됐는가 — 아니면 거부
  3. 이 이미지의 SBOM에 critical CVE가 없는가 — 있으면 거부

Admission control — 클러스터 단에서 강제 #

쿠버네티스 같은 환경에서는 admission controller가 위 정책을 강제합니다.

도구용도
KyvernoYAML 정책으로 이미지 검증 — verifyImages
OPA GatekeeperRego 언어 기반 정책
Sigstore Policy Controllercosign 검증 전용

도커만 쓰는 단일 호스트 환경에선 위 정도까지 갈 일이 적지만, 운영이 K8s로 가면 자연스럽게 만나는 영역입니다.

SLSA — 공급망 성숙도 모델 #

SLSA (Supply-chain Levels for Software Artifacts)는 공급망 보안의 단계를 정의한 프레임워크입니다.

Level무엇이 필요한가
L1빌드 절차가 문서화됨
L2빌드가 hosted 서비스에서 — provenance가 자동 생성
L3빌드가 격리된 환경, provenance 위변조 방지
L4 (이전 표준)재현 가능한 빌드, two-party 검증

GitHub Actions의 호스티드 러너 + --provenance=mode=max + cosign 키리스 서명 정도로도 L3의 큰 부분을 만족시킵니다. 깊이 들어가지 않더라도 점진적으로 위 단계로 올라갈 수 있습니다.

자주 만나는 함정 / 실수 #

  • 운영 이미지에 SBOM만 만들고 검증 안 함 — 만든 게 의미가 없음. CI / admission 단에서 검증을 게이트로
  • 키 기반 서명에서 키 분실 / 유출 — 키리스로 마이그레이션
  • 여러 빌드가 같은 태그를 덮어씀latest 같은 태그는 attestation의 추적을 깨뜨린다. digest (@sha256:...)로 검증
  • Rekor가 다운된 시점에 빌드 — 키리스 서명이 안 됨. 서명을 retry 로직 안에
  • SBOM의 컴포넌트 출처가 정확하지 않음 — 일부 정적 바이너리는 syft가 못 잡음. 빌드 시점 SBOM (buildx --sbom)이 더 정확

정리 #

이번 글에서 잡은 그림:

  • 공급망 위협은 코드 안이 아니라 코드를 만드는 과정에서 들어옴 — CVE 스캔으로 부족
  • SBOM = 이미지에 무엇이 들어 있는지의 기계 읽기 가능 명세 (CycloneDX / SPDX)
  • Syft로 SBOM 생성, **buildx --sbom=true**로 빌드 시점에 attestation 첨부
  • Cosign 키리스 서명 = OIDC 신원 + 짧은 수명 인증서(Fulcio) + 투명성 로그(Rekor) — 장기 키 관리 없음
  • GHA 한 워크플로우로 빌드 → 멀티 아키 → SBOM → provenance → 키리스 서명까지 끝
  • 운영 환경에선 검증을 게이트로 — 디지털 서명 / SBOM이 단순 첨부에 그치면 효과 없음
  • SLSA Level은 점진적 — 한 단계씩 올리는 길

다음 글(#5 리소스 제한과 cgroups)에서는 컨테이너의 자원 제한으로 시각을 바꿉니다. cgroups v2의 기본, mem_limit / cpus의 동작, OOMKilled의 디버깅, 그리고 ulimit / pids 같은 또 다른 격리 수단입니다.

X