도커 고급 강좌 #4 SBOM과 서명 — 공급망 보안의 입구
#3의 보안 체크리스트가 한 컨테이너 안의 문제였다면, 이번 글은 한 단계 위 — **공급망(supply chain)**의 문제입니다. 이 이미지에 무엇이 들어 있는가, 그리고 누가 만든 것인가.
도커 고급 강좌 시리즈에서 이번 글의 위치:
- #1 BuildKit과 buildx
- #2 멀티 아키텍처 이미지
- #3 이미지 보안 — non-root, distroless, scan(Trivy)
- #4 SBOM과 서명(cosign) ← 이번 글
- #5 리소스 제한과 cgroups
- #6 프로덕션 운영 — restart 정책, healthcheck, graceful shutdown
왜 공급망 보안이 갑자기 중요해졌나 #
근래 몇 년 사이 큰 사고들이 연달아 일어났습니다.
- 2020 SolarWinds — 정상적으로 서명된 빌드 산출물에 백도어가 들어 있었음
- 2021 codecov bash uploader — 정상 도구의 다운로드 스크립트가 손상돼 비밀 유출
- 2024 xz utils 백도어 — 널리 쓰이는 압축 도구에 신뢰받는 메인테이너가 백도어 삽입
공통점은 — 취약점이 코드 안이 아니라, 코드를 만들고 배포하는 과정에 끼어 들었다는 것 입니다. CVE 스캔만으로는 잡히지 않는 위협입니다. 이걸 다루는 도구가 SBOM과 서명 두 축입니다.
| 질문 | 도구 |
|---|---|
| 이 이미지 안에 정확히 무엇이 들어 있나? | SBOM (Software Bill of Materials) |
| 이 이미지를 정말 그 사람/조직이 만들었나? | 서명 (cosign / sigstore) |
| 이 이미지가 어떤 절차를 거쳐 만들어졌나? | attestation / SLSA |
SBOM이란 #
SBOM은 한 소프트웨어가 무엇으로 이뤄졌는지를 기계가 읽을 수 있는 형식으로 적은 명세서입니다. 비유하면 식품의 영양성분표와 비슷합니다.
표준 형식이 두 개 있습니다.
| 형식 | 만든 곳 | 특징 |
|---|---|---|
| SPDX | Linux Foundation | 가장 오래된 표준, 라이선스 정보에 강함 |
| CycloneDX | OWASP | 보안 시각, 의존성 그래프 표현이 풍부 |
도구마다 두 형식 모두 출력 가능합니다. 처음 쓸 땐 한쪽을 골라 일관되게 쓰면 됩니다.
Syft로 SBOM 생성 #
syft (Anchore)가 표준에 가깝습니다.
brew install syft
# 또는
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/binsyft myapp:1.0 -o cyclonedx-json > sbom.cdx.json
syft myapp:1.0 -o spdx-json > sbom.spdx.json출력 예 (CycloneDX의 일부):
{
"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으로 첨부할 수 있습니다.
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로 확인할 수 있습니다.
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
--format '{{json .SBOM}}'이게 가장 깔끔한 길입니다 — 빌드 한 번에 SBOM까지 함께 만들고, 같은 매니페스트에 묶어 보관합니다.
SBOM을 어디에 쓰는가 #
SBOM 자체는 그냥 데이터입니다. 가치는 그걸 들고 쿼리할 도구 들에서 나옵니다.
취약점 스캔 — Grype #
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.pubcosign 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)에 기록하는 것입니다.
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.0certificate-identity-regexp— 누가 서명할 수 있는가 (URL 패턴)certificate-oidc-issuer— 어느 OIDC 발급자가 발급한 인증서를 신뢰할지
GitHub Actions 위에서 도는 빌드는 OIDC 토큰을 자동으로 받을 수 있어, 환경변수 / 비밀 관리 없이 서명이 됩니다. 운영에선 거의 항상 키리스로 가는 편이 안전합니다.
GitHub Actions에서 — 한곳 모음 #
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}이 워크플로우 하나로:
- 이미지 빌드 + 멀티 아키 (#2)
- SBOM + provenance attestation 첨부
- 키리스 서명 (Rekor 기록)
거의 모든 공급망 보안 흐름이 한 워크플로우에서 끝납니다.
Attestation — SBOM과 서명을 묶기 #
지금까지 나온 조각들을 한 그림으로:
image
│
├── manifest (linux/amd64)
├── manifest (linux/arm64)
├── attestation: SBOM (CycloneDX)
├── attestation: provenance (SLSA build info)
└── signature (cosign)이 모든 것이 같은 OCI 레지스트리에 같은 이미지의 메타데이터로 저장됩니다. imagetools inspect로 한 번에 들여다볼 수 있습니다.
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
--format '{{json .}}' | jq '.Manifest'검증 정책은 도구 / 환경마다 다르지만, 운영 환경에서 자주 보는 패턴은:
- 이 이미지에 SBOM이 첨부돼 있는가 — 없으면 거부
- 이 이미지가 우리 조직의 OIDC 신원으로 서명됐는가 — 아니면 거부
- 이 이미지의 SBOM에 critical CVE가 없는가 — 있으면 거부
Admission control — 클러스터 단에서 강제 #
쿠버네티스 같은 환경에서는 admission controller가 위 정책을 강제합니다.
| 도구 | 용도 |
|---|---|
| Kyverno | YAML 정책으로 이미지 검증 — verifyImages 키 |
| OPA Gatekeeper | Rego 언어 기반 정책 |
| Sigstore Policy Controller | cosign 검증 전용 |
도커만 쓰는 단일 호스트 환경에선 위 정도까지 갈 일이 적지만, 운영이 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 같은 또 다른 격리 수단입니다.