- scan-sbom: 요약에 단계별 소요(SBOM 생성 vs 취약점 스캔) 표시, 오해 주던 "총 소요"· 항상 0인 per-SBOM Sec·min/max 라인 제거. 스캔한 SEVERITY 만 동적 컬럼. - generate-sbom: 생성 소요시간을 .sbom-gen-seconds 로 기록(요약 단계별 시간용). - doc/sbom-pipeline.md: 실행 이미지(Dockerfile)·빌드/푸시·GitHub 설정·결과 확인/대응 보강. - .gitignore: .sbom-gen-seconds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
Helm 카탈로그 SBOM 생성 파이프라인
manifests/helm/ 정적 카탈로그의 모든 컨테이너 이미지에 대한 SBOM(CycloneDX)과 취약점 리포트를
결정론적으로 생성하는 파이프라인. helm/trivy/python3 가 설치된 리눅스 컨테이너 안에서 실행하며,
GitHub Actions(.github/workflows/sbom.yml)로 자동화한다.
왜 필요한가
GitHub Dependency-Graph SBOM(예: paasup_dip-catalog_199087.json)은 pypi 등 소스 매니페스트만 담고
Helm 차트가 참조하는 컨테이너 이미지는 하나도 포함하지 못한다(Helm 미지원 + 이미지 미스캔). 이 파이프라인이
그 공백을 메운다 — helm template 렌더로 실제 배포 이미지를 뽑고, 각 이미지의 SBOM 과 취약점을 생성한다.
아키텍처 (SBOM-first)
이미지를 한 번만 pull 해서 SBOM 을 만들고, 취약점은 그 SBOM 에서 오프라인으로 뽑는다.
extract-helm-images.sh generate-sbom.sh scan-sbom.sh
manifests/helm/** ──helm template──▶ images_final.tsv ──trivy image──▶ sbom/*.cdx.json ──trivy sbom──▶ trivy-summary.md
(오프라인·결정론) (이미지 1회 pull, 인증 필요) (CycloneDX) (오프라인·인증 불필요) + trivy-reports/
- 레지스트리 pull·인증은 SBOM 생성 단계 1곳으로 집중. 취약점 스캔은 네트워크·인증 없이 빠르게 반복 가능 (CVE DB 갱신 시 재pull 없이 재스캔). → 파이프라인 비용의 대부분은 SBOM 생성(이미지 pull)이고 스캔은 초 단위.
- 스캔 대상 이미지는 trivy 가 원격에서 받아 분석 후 폐기하므로 호스트/이미지 스토어에 남지 않는다.
스크립트 (doc/scripts/)
바이너리(helm/trivy/python3)를 컨테이너 내부에서 직접 호출한다(도커 소켓·docker CLI 불필요).
| 스크립트 | 입력 | 출력 |
|---|---|---|
extract-helm-images.sh |
manifests/helm/ |
images_final.tsv(chart⇥version⇥image), render_status.tsv |
generate-sbom.sh |
images_final.tsv |
sbom/<img>.cdx.json, sbom-index.tsv, sbom-gen.log |
scan-sbom.sh |
sbom/ + sbom-index.tsv |
trivy-summary.{md,tsv}, trivy-reports/, trivy-run.log |
주요 환경변수: PARALLEL(동시 처리, 기본 3), LIMIT(대상 상한, 0=전체 — 테스트용),
SEVERITY(기본 HIGH,CRITICAL), TRIVY_CACHE_DIR(캐시 디렉토리), MERGE=1(generate 시
catalog.cdx.json 병합, cyclonedx CLI 필요). 사설 레지스트리 인증은 trivy 네이티브:
TRIVY_USERNAME/TRIVY_PASSWORD(단일) 또는 DOCKER_CONFIG(다중).
로컬/컨테이너 실행
docker run --rm -v "$PWD:/repo" -w /repo \
-e TRIVY_CACHE_DIR=/repo/sbom-out/cache -e DOCKER_CONFIG=/repo/sbom-out/.docker \
docker.io/wbsong111/sbom-pipeline:latest bash -c '
bash doc/scripts/extract-helm-images.sh manifests/helm sbom-out
bash doc/scripts/generate-sbom.sh sbom-out/images_final.tsv sbom-out # 인증: DOCKER_CONFIG/TRIVY_USERNAME
bash doc/scripts/scan-sbom.sh sbom-out
'
# 빠른 검증: generate 단계에 -e LIMIT=3
실행 이미지 (doc/scripts/Dockerfile)
파이프라인은 도구가 설치된 컨테이너 안에서 돈다. 그 이미지는 doc/scripts/Dockerfile로 빌드한다.
| 항목 | 값 |
|---|---|
| 베이스 | debian:stable-slim (glibc) |
| 포함 도구 | helm(v3) · trivy · python3 · bash · git |
| 아키텍처 | linux/amd64 (GitHub 러너와 일치) |
| 현재 이미지 | docker.io/wbsong111/sbom-pipeline:latest (public) |
glibc(debian) 필수: GitHub Actions 의
container:안에서actions/checkout·upload-artifact(node 기반)가 동작하려면 glibc 이거나 node 가 있어야 한다.aquasec/trivy같은 alpine(musl) 이미지는 node 실행 실패 → debian 사용. amd64 필수(러너 아키텍처).
빌드 & 푸시
docker buildx build --platform linux/amd64 \
-t docker.io/<org>/sbom-pipeline:latest \
-f doc/scripts/Dockerfile --push doc/scripts
credsStore(Docker Desktop) 환경에서
docker-container빌더로--push시 인증 실패하면, 단일 아키텍처를 로컬 적재(--load) 후docker push하거나 인라인 토큰 config 를 쓴다.
CI (.github/workflows/sbom.yml)
- 트리거
workflow_dispatch— 수동. 입력limit(기본 0=전체, 테스트 시 예3).pull_request(manifests/helm/**) — 변경된 차트의 이미지만 증분 스캔(빠른 게이트).schedule(주 1회) — 전체 스캔.
- 컨테이너:
vars.SBOM_PIPELINE_IMAGE이미지에서 실행. - 인증 구성 스텝: 시크릿으로
~/.docker/config.json(인라인 토큰)을 만들어DOCKER_CONFIG로 trivy 에 전달. - 아티팩트: 스테이징 디렉토리(
report/)로 모아 상대경로로 업로드(절대경로면 v4 가 전체 경로트리를 보존하므로). - 스캔 주기 원칙: 인벤토리·SBOM 은 차트 변경 시에만 바뀌지만 취약점은 새 CVE 로 계속 변하므로 스케줄 전체 스캔이 필수. PR 은 변경 차트만, 스케줄은 전체.
실행
gh workflow run helm-catalog-sbom --repo <org>/dip-catalog -f limit=3 # 빠른 검증
gh workflow run helm-catalog-sbom --repo <org>/dip-catalog # 전체(limit=0)
gh run watch --repo <org>/dip-catalog
GitHub 설정 (워크플로 활성화에 필요)
Settings → Secrets and variables → Actions
| 종류 | 이름 | 용도 |
|---|---|---|
| Variable | SBOM_PIPELINE_IMAGE |
실행 이미지 태그 (예: docker.io/wbsong111/sbom-pipeline:latest) |
| Secret | DOCKERHUB_USER / DOCKERHUB_TOKEN |
docker.io rate limit 회피(이미지는 public). pull 만 하므로 read-only PAT 권장 |
| Secret | NGC_API_KEY |
nvcr.io 인증(NVIDIA nemo/nim — 없으면 pull 불가) |
| Secret | (필요 시) paasup 사설 레지스트리 자격증명 | paasup/* 이미지가 사설일 때 |
추가 고려:
SBOM_PIPELINE_IMAGE가 사설이면 워크플로container:에credentials:추가 필요.- 저장소 Actions 활성화(org 정책). 서드파티 액션은 GitHub 공식(
checkout·upload-artifact)만 사용. DOCKERHUB_TOKEN이 Docker Desktop 세션 토큰이면 만료 가능 → 장기적으로 스코프 지정 PAT 권장.
결과 확인
- Job Summary (권장) — Actions run 페이지 → Summary.
trivy-summary.md표가 렌더링된다. - 아티팩트
sbom-and-vuln-report— 아래 구조.gh run download <run-id> -n sbom-and-vuln-report -D ./out.
sbom-and-vuln-report/ ← 아티팩트 루트
├── trivy-summary.md ← 취약점 요약(표)
├── trivy-summary.tsv
├── sbom-index.tsv ← 차트↔이미지↔SBOM 인덱스
├── sbom-gen.log / trivy-run.log
└── sbom/ ← 이미지별 상세 CycloneDX SBOM
├── <img>.cdx.json
└── ...
요약(trivy-summary.md) 내용
- 단계별 소요시간:
SBOM 생성 소요(이미지 pull, 주 비용) /취약점 스캔 소요(오프라인). 오해를 주던 단일 "총 소요"는 제거. - 동적 심각도 컬럼: 실제 스캔한
SEVERITY만 표에 표시(기본CRITICAL·HIGH). MEDIUM/LOW 는 스캔 안 하면 컬럼 자체가 없다. - 차트·이미지별 집계 + 심각도별 총계. TSV 는
chart⇥image⇥<심각도들>⇥status(CRITICAL 이 첫 카운트 열).
첫 전체 실행 결과(참고, 2026-07-08)
- SBOM 생성 159/178(19 실패 — 대부분 사설/미인증 레지스트리 또는 대용량 timeout), 스캔 145 전부 성공.
- 취약점 합계 CRITICAL 1,511 · HIGH 15,724, 총 소요 약 20분(생성 19분 / 스캔 23초).
렌더 함정 (extract 단계, 스크립트에 내장)
| 플래그 | 이유 |
|---|---|
--set global.security.allowInsecureImages=true |
bitnamilegacy/* 오버라이드가 Bitnami common 의 insecureImages 가드에 걸려 렌더 실패하는 것 방지 |
--kube-version 1.31.0 |
일부 차트의 kubeVersion 상한 제약 회피 (예: rancher < 1.32) |
trivy 병렬 실행 시 공유 캐시 bolt 잠금 충돌을 피하려 워커는 --cache-backend memory --skip-db-update,
메인은 시작 시 trivy image --download-db-only 로 DB 를 1회 워밍한다.
결과 대응 (트리아지)
원시 CVE 수를 0으로 만드는 것이 목표가 아니다. 고칠 수 있고, 악용 가능하고, 노출된 것부터 대응한다.
- 우선순위: fix 가능(
--ignore-unfixed) → KEV/EPSS(실제 악용) → 노출도(인터넷 vs 내부) → 심각도. - 대응 레버(모두 Git PR): 차트/이미지 버전 업(최고 레버리지) · 베이스 이미지 교체(특히 미유지
bitnamilegacy/*) · 소유 이미지(paasup/*,wbsong111/*) 재빌드 · 좋은 버전 선정 후 digest 고정. - 게이트·억제: PR 에 "새로 유입되는 fix-available CRITICAL" 차단 게이트(워크플로에 주석으로 준비됨),
수용 CVE 는
.trivyignore(사유·만료일 명시). - 새 CVE 는 계속 나오므로 스케줄 스캔으로 추세 추적.
향후 개선(요약을 행동 가능하게): 이미지별 fixable(패치 존재) 집계 컬럼, .trivyignore 지원,
소유/legacy/3rd-party 분류 컬럼.
운영 노트 — :latest/무태그 이미지 digest 고정
태그가 고정돼 있어도 내용물이 바뀌는 :latest/무태그 이미지는 재현성 저해 + 증분 스캔의 사각지대다.
정기 전체 스캔 대상에 항상 포함하고, 가능하면 구체 버전 또는 digest(@sha256:...)로 고정한다.
적용된 고정(2026-07-08 기준):
mlflow@1.9.0postgresql →bitnamilegacy/postgresql@sha256:42a8200d...(legacy 에 v18 버전 태그 부재, latest 내용 digest 고정)vllm@0.0.11vllm-openai →vllm/vllm-openai:v0.24.0
digest 고정은 재현성을 보장하지만 자동 보안 패치는 멈춘다. → 정기 스캔으로 새 CVE 를 감지하고 필요 시 digest 를 갱신한다.
bitnamilegacy/* 는 미유지 이미지이므로 중기적으로 legacy 탈피(대체 이미지)를 검토한다.