왜 공급망 보안이 문제가 되는가

현대 애플리케이션은 직접 작성한 코드보다 외부 의존성의 비중이 압도적으로 크다. npm, PyPI, 컨테이너 베이스 이미지까지 포함하면 수백 개의 전이 의존성(transitive dependency)이 빌드에 딸려 들어온다. 문제는 이 의존성 중 하나에 취약점이 공개되거나 악성 패키지가 섞였을 때, "우리 서비스가 그 버전을 쓰고 있는가?"를 즉시 답하지 못한다는 점이다. Log4Shell 당시 많은 조직이 며칠을 이 질문에 소비했다.

SBOM(Software Bill of Materials)은 빌드 산출물에 포함된 모든 구성요소를 기계가 읽을 수 있는 형식으로 목록화한 것이다. SBOM이 CI에 통합되어 있으면 "어떤 버전이 어디에 있는가"를 쿼리로 답할 수 있고, 신규 CVE 공개 시 영향 범위를 분 단위로 파악한다.

SBOM 표준 포맷 선택

실무에서는 두 포맷이 사실상 표준이다. 도구 호환성만 확인하면 되고, 대부분의 스캐너는 둘 다 소비한다.

포맷주관강점주 용도
SPDXLinux Foundation라이선스 표현이 정교, ISO 표준라이선스 컴플라이언스
CycloneDXOWASP취약점·VEX 연계가 강함보안 취약점 추적

보안이 주목적이면 CycloneDX, 법무/라이선스 감사가 주목적이면 SPDX를 권한다. 애매하면 CycloneDX 하나로 시작해도 무방하다.

Syft로 SBOM 생성하기

소스 디렉터리, 컨테이너 이미지, 파일시스템 모두에서 SBOM을 뽑을 수 있다. CI에서는 최종 산출물인 컨테이너 이미지 기준으로 생성하는 것이 실배포와 가장 일치한다.

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

IMAGE="registry.example.com/api:${GIT_SHA}"

# 컨테이너 이미지 기준으로 CycloneDX SBOM 생성
syft "${IMAGE}" \
  -o cyclonedx-json="sbom-${GIT_SHA}.cdx.json" \
  -o spdx-json="sbom-${GIT_SHA}.spdx.json"

# SBOM 자체를 취약점 스캔 (이미지 재분석 불필요)
grype "sbom:sbom-${GIT_SHA}.cdx.json" \
  --fail-on high \
  -o table

핵심은 이미지를 두 번 스캔하지 않는다는 것이다. 한 번 SBOM을 만들고, 그 SBOM을 grype에 넘겨 스캔한다. 빌드 시점의 구성요소가 고정되므로 결과가 재현 가능하다.

GitHub Actions에 통합

빌드 → SBOM 생성 → 스캔 → 아티팩트 보존을 하나의 잡으로 묶는다. fail-on은 처음부터 high로 조이면 기존 부채 때문에 파이프라인이 계속 깨지므로, critical부터 시작해 점진적으로 낮추는 것을 권한다.

name: build-and-sbom
on: [push]

jobs:
  sbom:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t api:${{ github.sha }} .

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: api:${{ github.sha }}
          format: cyclonedx-json
          output-file: sbom.cdx.json

      - name: Scan SBOM
        uses: anchore/scan-action@v6
        with:
          sbom: sbom.cdx.json
          fail-build: true
          severity-cutoff: critical

      - name: Store SBOM
        uses: actions/upload-artifact@v4
        with:
          name: sbom-${{ github.sha }}
          path: sbom.cdx.json
          retention-days: 90

SBOM 저장과 사후 조회

SBOM은 생성하고 버리면 의미가 없다. 배포된 각 버전의 SBOM을 커밋 SHA와 함께 보존해야, 신규 CVE가 뜬 뒤에도 과거 배포를 소급 조회할 수 있다. 아티팩트 저장소나 별도 DB에 적재해두면 다음과 같은 질의가 가능하다.

-- 특정 패키지의 취약 버전을 쓰는 배포본 조회
SELECT deployment_id, image_tag, component_version
FROM sbom_components
WHERE component_name = 'log4j-core'
  AND component_version LIKE '2.1%'
  AND component_version NOT IN ('2.17.1', '2.17.2');

이 한 줄이 사고 대응 시간을 며칠에서 몇 분으로 줄인다. 서명(cosign)으로 SBOM에 서명해두면 산출물의 무결성까지 검증할 수 있다.

실무 주의점

  • 게이트는 점진적으로: 처음부터 high 이상 전부 차단하면 팀이 스캔을 우회한다. critical부터 막고 SLA를 두어 단계적으로 조인다.
  • 오탐과 VEX: 실제로 도달 불가능한 코드 경로의 취약점까지 빌드를 깨뜨리면 신뢰를 잃는다. VEX(Vulnerability Exploitability eXchange) 문서로 "영향 없음"을 명시해 노이즈를 줄인다.
  • 소스가 아닌 산출물 기준: lockfile 스캔만으로는 베이스 이미지의 OS 패키지를 놓친다. 최종 이미지 기준 SBOM이 배포 실체와 일치한다.
  • DB는 시점 스냅샷: 취약점 DB는 매일 갱신되므로, 같은 SBOM도 재스캔하면 결과가 달라진다. 스캔 시각을 함께 기록한다.

SBOM은 도입 자체보다 "지속적으로 최신 상태를 유지하고 조회 가능하게 보존하는 것"이 본질이다. CI에 한 번 엮어두면 이후 사고 대응의 비용 구조가 근본적으로 바뀐다.