Files
service-catalog/doc/sbom-pipeline.md
T
wbsong111 5e1422e0e4 CVE/SBOM 게이트 문서화: CLAUDE.md 3번째 서브시스템 + MEMORY.md 신설
CLAUDE.md 에 CVE/SBOM 게이트 서브시스템 절 추가(기존 7개 설계 원칙·구현
현황은 변경 없음). doc/sbom-pipeline.md 에 게이트 판정 절 + 커버리지
자가진단 미이식 제약 명시. MEMORY.md 신설 — warn-only 상태, 45+ 차트
미검증, 자체 빌드 프레임워크 미사용, SBOM_PIPELINE_IMAGE 재빌드 보류 등
후속 결정 사항 기록.
2026-08-03 10:06:48 +09:00

199 lines
12 KiB
Markdown

# Helm 카탈로그 SBOM 생성 파이프라인
`manifests/helm/` 정적 카탈로그의 **모든 컨테이너 이미지에 대한 SBOM(CycloneDX)과 취약점 리포트**를
결정론적으로 생성하는 파이프라인. helm/trivy/python3 가 설치된 리눅스 컨테이너 안에서 실행하며,
GitHub Actions([.github/workflows/sbom.yml](../.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 가 원격에서 받아 분석 후 폐기하므로 호스트/이미지 스토어에 남지 않는다.
## 스크립트 (`scripts/pipeline/`)
바이너리(`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`(다중).
### 로컬/컨테이너 실행
```bash
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 scripts/pipeline/extract-helm-images.sh manifests/helm sbom-out
bash scripts/pipeline/generate-sbom.sh sbom-out/images_final.tsv sbom-out # 인증: DOCKER_CONFIG/TRIVY_USERNAME
bash scripts/pipeline/scan-sbom.sh sbom-out
'
# 빠른 검증: generate 단계에 -e LIMIT=3
```
## 실행 이미지 (`scripts/pipeline/Dockerfile`)
파이프라인은 **도구가 설치된 컨테이너 안에서** 돈다. 그 이미지는 [scripts/pipeline/Dockerfile](../scripts/pipeline/Dockerfile)로 빌드한다.
| 항목 | 값 |
|------|------|
| 베이스 | `debian:stable-slim` (**glibc**) |
| 포함 도구 | `helm`(v3) · `trivy` · `python3` · `bash` · `git` |
| 아키텍처 | **linux/amd64** (GitHub 러너와 일치) |
| 현재 이미지 | `docker.io/wbsong111/sbom-pipeline:latest` (public) |
> 재빌드 후 `paasup` 네임스페이스로 이전 예정 — 미완료(`MEMORY.md` 참고). Dockerfile 내용은
> 안 바뀌었으므로 위 이미지는 이 마이그레이션으로 당장 깨지지 않는다.
> **glibc(debian) 필수**: GitHub Actions 의 `container:` 안에서 `actions/checkout`·`upload-artifact`
> (node 기반)가 동작하려면 glibc 이거나 node 가 있어야 한다. `aquasec/trivy` 같은 **alpine(musl)
> 이미지는 node 실행 실패** → debian 사용. **amd64 필수**(러너 아키텍처).
### 빌드 & 푸시
```bash
docker buildx build --platform linux/amd64 \
-t docker.io/paasup/sbom-pipeline:latest \
-f scripts/pipeline/Dockerfile --push scripts/pipeline
```
> 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 은 변경 차트만, 스케줄은 전체.
- **게이트**: 스캔 뒤 `scripts/pipeline/cve-gate.py` 로 판정(현재 `--warn-only`).
### 실행
```bash
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
```
## CVE 게이트 (`scripts/pipeline/cve-gate.py`)
스캔이 끝나면 `cve-gate.py``trivy-reports/*.json` 을 판정한다: 고유 CVE 단위 집계,
`max(벤더 등급, NVD 등급)` 실효 등급, `doc/cve-exceptions.json` 승인 예외 처리. 상세 판정
로직·근거는 스크립트 자체의 모듈 docstring을 우선 참고한다.
현재 `sbom.yml``--warn-only` 로 호출한다 — 게이트가 실패해도 워크플로/PR 을 막지
않는다. 45+ 개 카탈로그 차트가 아직 이 게이트로 트리아지된 적이 없어, 강제 전환 전에
먼저 전체 스캔 1회로 현황을 파악해야 한다(`MEMORY.md`).
> **커버리지 자가진단 미이식 — 알려진 제약.** security-catalog 의 `scan-sbom.sh` 는 SBOM
> 사본에 센티널 패키지를 주입해 재스캔하는 `CoverageProbe` 자가진단으로 "0건"과
> "측정되지 않음"을 구분한다. dip-catalog 의 `scan-sbom.sh` 는 이 로직이 없다 — 게이트는
> 항상 "프로브 이전 리포트" 경로로 판정한다(findings 총계 0건이면 보수적으로 실패,
> os-pkgs만 0건이면 경고만). 이식 여부는 미결(`MEMORY.md`).
## GitHub 설정 (워크플로 활성화에 필요)
`Settings → Secrets and variables → Actions`
| 종류 | 이름 | 용도 |
|------|------|------|
| **Variable** | `SBOM_PIPELINE_IMAGE` | 실행 이미지 태그 (예: `docker.io/wbsong111/sbom-pipeline:latest`) |
| Secret | `DOCKERHUB_USER` / `DOCKERHUB_TOKEN` | **docker.io + docker.getcollate.io rate limit 회피**. getcollate(openmetadata)는 Docker Hub 프록시라 익명 pull 시 rate limit(TOOMANYREQUESTS)에 걸림 → Docker Hub 자격증명으로 인증. 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** 권장.
## 결과 확인
1. **Job Summary** (권장) — Actions run 페이지 → Summary. `trivy-summary.md` 표가 렌더링된다.
2. **아티팩트** `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으로 만드는 것이 목표가 아니다. **고칠 수 있고, 악용 가능하고, 노출된** 것부터 대응한다.
1. **우선순위**: fix 가능(`--ignore-unfixed`) → KEV/EPSS(실제 악용) → 노출도(인터넷 vs 내부) → 심각도.
2. **대응 레버(모두 Git PR)**: 차트/이미지 **버전 업**(최고 레버리지) · **베이스 이미지 교체**(특히 미유지 `bitnamilegacy/*`) ·
**소유 이미지(`paasup/*`, `wbsong111/*`) 재빌드** · 좋은 버전 선정 후 **digest 고정**.
3. **게이트·억제**: `scripts/pipeline/cve-gate.py` 가 고유 CVE·실효 등급 기준으로 판정한다(현재
warn-only — 위 "CVE 게이트" 절 참고). 수용 CVE 는 `doc/cve-exceptions.json`(근거·만료일 명시).
4. 새 CVE 는 계속 나오므로 스케줄 스캔으로 **추세 추적**.
향후 개선(요약을 행동 가능하게): 이미지별 **fixable(패치 존재) 집계** 컬럼, `.trivyignore` 지원,
소유/legacy/3rd-party **분류 컬럼**.
## 운영 노트 — `:latest`/무태그 이미지 digest 고정
태그가 고정돼 있어도 내용물이 바뀌는 `:latest`/무태그 이미지는 재현성 저해 + 증분 스캔의 사각지대다.
정기 전체 스캔 대상에 항상 포함하고, 가능하면 **구체 버전 또는 digest(`@sha256:...`)로 고정**한다.
적용된 고정(2026-07-08 기준):
- `mlflow@1.9.0` postgresql → `bitnamilegacy/postgresql@sha256:42a8200d...` (legacy 에 v18 버전 태그 부재, latest 내용 digest 고정)
- `vllm@0.0.11` vllm-openai → `vllm/vllm-openai:v0.24.0`
digest 고정은 재현성을 보장하지만 자동 보안 패치는 멈춘다. → 정기 스캔으로 새 CVE 를 감지하고 필요 시 digest 를 갱신한다.
`bitnamilegacy/*` 는 미유지 이미지이므로 중기적으로 legacy 탈피(대체 이미지)를 검토한다.