왜 공급망 보안이 문제가 되는가
현대 애플리케이션은 직접 작성한 코드보다 외부 의존성의 비중이 압도적으로 크다. npm, PyPI, 컨테이너 베이스 이미지까지 포함하면 수백 개의 전이 의존성(transitive dependency)이 빌드에 딸려 들어온다. 문제는 이 의존성 중 하나에 취약점이 공개되거나 악성 패키지가 섞였을 때, "우리 서비스가 그 버전을 쓰고 있는가?"를 즉시 답하지 못한다는 점이다. Log4Shell 당시 많은 조직이 며칠을 이 질문에 소비했다.
SBOM(Software Bill of Materials)은 빌드 산출물에 포함된 모든 구성요소를 기계가 읽을 수 있는 형식으로 목록화한 것이다. SBOM이 CI에 통합되어 있으면 "어떤 버전이 어디에 있는가"를 쿼리로 답할 수 있고, 신규 CVE 공개 시 영향 범위를 분 단위로 파악한다.
SBOM 표준 포맷 선택
실무에서는 두 포맷이 사실상 표준이다. 도구 호환성만 확인하면 되고, 대부분의 스캐너는 둘 다 소비한다.
| 포맷 | 주관 | 강점 | 주 용도 |
|---|---|---|---|
| SPDX | Linux Foundation | 라이선스 표현이 정교, ISO 표준 | 라이선스 컴플라이언스 |
| CycloneDX | OWASP | 취약점·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에 한 번 엮어두면 이후 사고 대응의 비용 구조가 근본적으로 바뀐다.