- Authors
- Name
- 들어가며
- 컨테이너 이미지 위협 모델
- Trivy 취약점 스캐닝
- Cosign/Sigstore 이미지 서명
- SBOM 생성과 관리
- CI/CD 파이프라인 통합
- Kubernetes Admission 정책
- 실패 사례와 복구
- SLSA 프레임워크와 빌드 출처 증명
- 운영 체크리스트
- 마치며
- 참고자료

들어가며
2024년 XZ Utils 백도어 사건(CVE-2024-3094)은 소프트웨어 공급망 공격이 얼마나 정교하고 위험한지를 전 세계에 보여주었습니다. 컨테이너 기반 배포가 표준이 된 현재, 이미지 하나에 수백 개의 의존성이 포함되어 있고, 그 중 하나라도 변조되면 프로덕션 환경 전체가 위험에 노출됩니다.
이 글에서는 컨테이너 이미지 보안의 전체 라이프사이클을 다룹니다. 취약점 스캐닝(Trivy), 이미지 서명(Cosign/Sigstore), 소프트웨어 구성 명세(SBOM), 그리고 SLSA 프레임워크를 활용한 공급망 무결성 검증까지, 프로덕션에서 바로 적용할 수 있는 실전 코드와 함께 설명합니다.
컨테이너 이미지 위협 모델
컨테이너 이미지를 둘러싼 공격 벡터는 크게 다섯 가지로 분류할 수 있습니다.
| 위협 유형 | 설명 | 대응 수단 |
|---|---|---|
| 알려진 취약점(CVE) | 베이스 이미지나 패키지에 포함된 공개 취약점 | Trivy, Grype 스캐닝 |
| 이미지 변조(Tampering) | 레지스트리에서 이미지가 교체되거나 수정됨 | Cosign 서명 + 검증 |
| 의존성 혼동(Dependency Confusion) | 악의적 패키지가 내부 패키지 이름으로 등록 | SBOM 기반 의존성 추적 |
| 빌드 환경 오염 | CI/CD 파이프라인 자체가 침해됨 | SLSA 빌드 출처 증명 |
| 권한 상승(Privilege Escalation) | 컨테이너 내부에서 호스트 탈출 | 최소 권한 원칙 + 런타임 보안 |
각 위협에 대해 하나의 도구만으로는 충분하지 않으며, 계층화된 방어(Defense in Depth) 전략이 필요합니다.
Trivy 취약점 스캐닝
Trivy 소개와 설치
Trivy는 Aqua Security가 개발한 오픈소스 보안 스캐너로, 컨테이너 이미지, 파일시스템, Git 저장소, Kubernetes 클러스터를 스캔할 수 있습니다. 2026년 현재 v0.68 이상 버전에서는 읽기 전용 데이터베이스 모드를 지원하여 여러 프로세스가 동시에 스캔을 수행할 수 있습니다.
# Trivy 설치 (macOS)
brew install trivy
# Trivy 설치 (Linux)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# 버전 확인
trivy version
이미지 스캔 실행
# 기본 이미지 스캔 - 모든 심각도의 취약점 표시
trivy image nginx:1.25
# HIGH, CRITICAL만 필터링하고 수정 가능한 취약점만 표시
trivy image --severity HIGH,CRITICAL --ignore-unfixed nginx:1.25
# JSON 형식으로 결과 출력 (CI/CD 파이프라인 연동용)
trivy image --format json --output results.json nginx:1.25
# 특정 CVE를 무시하도록 설정 (.trivyignore 파일 활용)
trivy image --ignorefile .trivyignore myapp:latest
# SBOM 기반 스캔 (이미 생성된 SBOM을 입력으로 사용)
trivy sbom ./sbom.cyclonedx.json
.trivyignore 파일 구성
프로덕션 환경에서는 오탐(false positive)이나 즉시 수정할 수 없는 취약점을 관리하기 위해 .trivyignore 파일을 활용합니다.
# .trivyignore - 허용된 예외 목록
# 만료일과 사유를 반드시 기록할 것
# CVE-2024-1234: 해당 기능 미사용, 2026-04-01까지 유예
CVE-2024-1234
# CVE-2024-5678: 업스트림 패치 대기 중
CVE-2024-5678
취약점 스캐너 비교
| 기능 | Trivy | Grype | Snyk | Clair |
|---|---|---|---|---|
| 라이선스 | Apache 2.0 | Apache 2.0 | 상용(무료 플랜 있음) | Apache 2.0 |
| 컨테이너 이미지 스캔 | O | O | O | O |
| 파일시스템 스캔 | O | O | O | X |
| IaC 스캔 | O | X | O | X |
| SBOM 생성 | O | X (Syft 연동) | O | X |
| 시크릿 탐지 | O | X | O | X |
| Kubernetes 스캔 | O | X | O | X |
| CI/CD 통합 용이성 | 높음 | 높음 | 매우 높음 | 보통 |
| 오프라인 지원 | O | O | X | O |
Trivy는 단일 도구로 가장 넓은 범위를 커버하며, 오프라인 환경에서도 동작하는 것이 강점입니다.
Cosign/Sigstore 이미지 서명
Sigstore 아키텍처
Sigstore는 소프트웨어 서명을 위한 오픈소스 프로젝트로, 세 가지 핵심 컴포넌트로 구성됩니다.
- Cosign: 컨테이너 이미지와 OCI 아티팩트 서명/검증 도구
- Fulcio: OIDC 기반 단기 인증서 발급 CA(Certificate Authority)
- Rekor: 서명 기록을 저장하는 변경 불가능한(Immutable) 투명성 로그
키리스(Keyless) 서명 방식에서는 개발자가 별도의 키를 관리할 필요 없이, GitHub/Google 등의 OIDC 자격 증명으로 서명할 수 있습니다.
Cosign을 이용한 이미지 서명
# Cosign 설치
brew install cosign
# 1. 키 페어 생성 (전통적 방식)
cosign generate-key-pair
# 2. 이미지 서명 (키 기반)
cosign sign --key cosign.key myregistry.io/myapp:v1.0.0
# 3. 키리스 서명 (Sigstore Fulcio 사용 - 권장)
# OIDC 인증 후 자동으로 단기 인증서 발급
cosign sign myregistry.io/myapp:v1.0.0
# 4. 서명 검증
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0
# 5. 키리스 서명 검증 (인증서 발급자와 ID 지정)
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/myorg/myrepo" \
myregistry.io/myapp:v1.0.0
서명에 메타데이터 첨부
Cosign은 서명 시 커스텀 어노테이션을 추가할 수 있어, 빌드 출처 추적에 유용합니다.
# 빌드 메타데이터를 서명에 포함
cosign sign \
-a "git.sha=$(git rev-parse HEAD)" \
-a "build.timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-a "build.pipeline=github-actions" \
myregistry.io/myapp:v1.0.0
# 서명과 어노테이션 함께 검증
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0 | jq .
SBOM 생성과 관리
SBOM이란
SBOM(Software Bill of Materials)은 소프트웨어에 포함된 모든 컴포넌트, 라이브러리, 의존성 목록을 구조화한 문서입니다. 미국 행정 명령 14028호(EO 14028) 이후 연방 정부 납품 소프트웨어에는 SBOM 제출이 의무화되었으며, 글로벌 규제 흐름도 SBOM을 표준으로 요구하고 있습니다.
SBOM 포맷 비교
| 항목 | SPDX | CycloneDX |
|---|---|---|
| 주관 기관 | Linux Foundation | OWASP |
| ISO 표준 | ISO/IEC 5962:2021 | ECMA-424 |
| 주요 용도 | 라이선스 컴플라이언스 | 보안 취약점 관리 |
| 지원 형식 | JSON, RDF, YAML, Tag-Value | JSON, XML, Protocol Buffers |
| VEX 지원 | O | O |
| 서비스 의존성 표현 | 제한적 | O |
| 도구 생태계 | 넓음 | 빠르게 성장 중 |
라이선스 컴플라이언스가 중요하면 SPDX, 보안 취약점 관리가 우선이면 CycloneDX를 선택하세요. 두 포맷 모두 생성하는 것이 가장 이상적입니다.
Syft를 이용한 SBOM 생성
# Syft 설치
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# CycloneDX 포맷으로 SBOM 생성 (컨테이너 이미지)
syft myregistry.io/myapp:v1.0.0 -o cyclonedx-json=sbom-cyclonedx.json
# SPDX 포맷으로 SBOM 생성
syft myregistry.io/myapp:v1.0.0 -o spdx-json=sbom-spdx.json
# 로컬 디렉토리 기반 SBOM 생성
syft dir:./my-project -o cyclonedx-json=sbom.json
# Trivy로도 SBOM 생성 가능
trivy image --format cyclonedx --output sbom-trivy.json myregistry.io/myapp:v1.0.0
# SBOM을 OCI 레지스트리에 첨부 (Cosign 활용)
cosign attach sbom --sbom sbom-cyclonedx.json myregistry.io/myapp:v1.0.0
SBOM 드리프트 탐지
SBOM은 빌드 시점의 스냅샷이므로, 런타임에 설치된 패키지와 다를 수 있습니다. 이를 SBOM 드리프트라고 합니다.
# 런타임 SBOM과 빌드 시점 SBOM 비교
# 1. 빌드 시점 SBOM (이미 생성됨)
# sbom-build.json
# 2. 런타임 컨테이너에서 현재 상태 추출
docker exec running-container syft / -o cyclonedx-json > sbom-runtime.json
# 3. 차이점 비교 (cyclonedx-diff 또는 직접 비교)
diff <(jq -r '.components[].name' sbom-build.json | sort) \
<(jq -r '.components[].name' sbom-runtime.json | sort)
CI/CD 파이프라인 통합
GitHub Actions 통합 파이프라인
다음은 빌드, 스캔, 서명, SBOM 생성을 하나의 파이프라인으로 통합한 예시입니다.
name: Container Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: my-org/my-app
jobs:
build-scan-sign:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Keyless 서명에 필요
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/my-org/my-app:${{ github.sha }}
# Step 1: 취약점 스캔
- name: Trivy Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/my-org/my-app:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: '1'
- name: Upload Trivy Results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
# Step 2: SBOM 생성
- name: Generate SBOM with Syft
uses: anchore/sbom-action@v0
with:
image: ghcr.io/my-org/my-app:${{ github.sha }}
format: cyclonedx-json
output-file: sbom.cyclonedx.json
# Step 3: Cosign으로 이미지 서명 (Keyless)
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign Container Image
run: |
cosign sign --yes \
-a "git.sha=${{ github.sha }}" \
-a "build.pipeline=github-actions" \
ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}
# Step 4: SBOM 첨부
- name: Attach SBOM to Image
run: |
cosign attach sbom \
--sbom sbom.cyclonedx.json \
ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}
# Step 5: SBOM에도 서명
- name: Sign SBOM Attestation
run: |
cosign attest --yes \
--predicate sbom.cyclonedx.json \
--type cyclonedx \
ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}
위 파이프라인은 다음 순서로 동작합니다.
- Docker 이미지를 빌드하고 GHCR에 푸시
- Trivy로 HIGH/CRITICAL 취약점을 스캔하고, 발견 시 파이프라인을 중단
- Syft로 SBOM을 생성
- Cosign 키리스 서명으로 이미지에 서명
- SBOM을 이미지에 첨부하고 서명
Kubernetes Admission 정책
Kyverno를 이용한 이미지 서명 검증
Kubernetes 클러스터에서 서명되지 않은 이미지의 배포를 차단하려면 Admission Controller를 활용합니다. Kyverno는 정책 기반의 Kubernetes 네이티브 도구로, 이미지 서명 검증을 선언적으로 정의할 수 있습니다.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- 'ghcr.io/my-org/*'
attestors:
- entries:
- keyless:
issuer: 'https://token.actions.githubusercontent.com'
subject: 'https://github.com/my-org/my-repo/.github/workflows/*'
rekor:
url: 'https://rekor.sigstore.dev'
attestations:
- type: 'https://cyclonedx.org/bom'
conditions:
- all:
- key: 'components'
operator: NotEquals
value: ''
OPA Gatekeeper를 이용한 정책
OPA Gatekeeper를 사용하는 환경에서는 ConstraintTemplate을 정의합니다.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequireimagedigest
spec:
crd:
spec:
names:
kind: K8sRequireImageDigest
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequireimagedigest
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not contains(container.image, "@sha256:")
msg := sprintf(
"Container '%v' must use image digest (sha256) instead of tag: %v",
[container.name, container.image]
)
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
not contains(container.image, "@sha256:")
msg := sprintf(
"Init container '%v' must use image digest (sha256) instead of tag: %v",
[container.name, container.image]
)
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireImageDigest
metadata:
name: require-image-digest
spec:
match:
kinds:
- apiGroups: ['']
kinds: ['Pod']
namespaces:
- production
실패 사례와 복구
사례 1: 서명되지 않은 이미지가 프로덕션에 배포됨
상황: Admission Controller가 설정되지 않은 네임스페이스에 서명 없는 이미지가 배포되었습니다.
원인: 네임스페이스별 정책 예외(exemption)가 너무 넓게 설정되어 있었습니다.
복구 절차:
- 해당 Pod를 즉시 격리(NetworkPolicy로 외부 통신 차단)
- 이미지를 Trivy로 긴급 스캔
- 동일 이미지를 Cosign으로 사후 서명하거나, 서명된 이미지로 교체
- Admission Controller 정책의 예외 범위를 축소
# 긴급 격리: NetworkPolicy 적용
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: emergency-isolate
namespace: production
spec:
podSelector:
matchLabels:
app: compromised-app
policyTypes:
- Ingress
- Egress
EOF
# 긴급 스캔
trivy image --severity CRITICAL compromised-image:tag
사례 2: CRITICAL CVE가 프로덕션에서 발견됨
상황: 이미 운영 중인 서비스의 베이스 이미지에서 CRITICAL 취약점이 발표되었습니다.
복구 절차:
- 영향 범위 분석: SBOM을 활용하여 해당 패키지를 사용하는 모든 이미지를 식별
- 긴급 패치 이미지 빌드 및 배포
- 롤아웃 진행 상황 모니터링
# SBOM에서 영향받는 이미지 검색
for sbom in sbom-*.json; do
if jq -e '.components[] | select(.name == "libexpat")' "$sbom" > /dev/null 2>&1; then
echo "AFFECTED: $sbom"
fi
done
# 패치된 베이스 이미지로 재빌드
docker build --no-cache --build-arg BASE_IMAGE=nginx:1.25.4-alpine -t myapp:patched .
# 롤링 업데이트
kubectl set image deployment/myapp myapp=myregistry.io/myapp:patched
kubectl rollout status deployment/myapp
사례 3: SBOM 드리프트 발생
상황: 런타임 컨테이너에 빌드 시점 SBOM에 없는 패키지가 설치되어 있었습니다.
원인: 컨테이너 내부에서 apt-get install을 실행하는 초기화 스크립트가 있었습니다.
대응:
- 불변(Immutable) 컨테이너 원칙을 강제하도록 readOnlyRootFilesystem 설정
- 런타임 SBOM 비교를 정기적으로 수행하는 CronJob 배포
- 드리프트 발생 시 자동 알림 구성
SLSA 프레임워크와 빌드 출처 증명
SLSA(Supply-chain Levels for Software Artifacts)는 소프트웨어 공급망의 무결성을 보증하기 위한 프레임워크입니다. 레벨 0부터 3까지 단계적으로 보안 수준을 높일 수 있습니다.
| 레벨 | 요구사항 | 설명 |
|---|---|---|
| SLSA 0 | 없음 | 보안 보증 없음 |
| SLSA 1 | 빌드 출처(Provenance) 존재 | 빌드 프로세스가 문서화됨 |
| SLSA 2 | 호스팅된 빌드 서비스 사용 | 빌드 서비스에서 서명된 출처 생성 |
| SLSA 3 | 강화된 빌드 환경 | 변조 방지가 보장된 빌드 환경 사용 |
GitHub Actions에서 SLSA Level 3 빌드 출처 증명을 구현하려면 slsa-framework/slsa-github-generator를 활용합니다.
# SLSA Provenance 생성 워크플로우 (별도 재사용 가능 워크플로우 호출)
name: SLSA Build Provenance
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Build Image
id: build
run: |
docker build -t ghcr.io/my-org/my-app:${{ github.ref_name }} .
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/my-org/my-app:${{ github.ref_name }} | cut -d@ -f2)
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
provenance:
needs: build
permissions:
actions: read
id-token: write
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ghcr.io/my-org/my-app
digest: ${{ needs.build.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
운영 체크리스트
프로덕션 환경에서 컨테이너 이미지 보안을 운영할 때 다음 체크리스트를 활용하세요.
빌드 단계
- 모든 이미지는 최소 베이스 이미지(distroless, alpine)를 사용하는가
- Dockerfile에 고정된 버전(태그 + digest)을 사용하는가
- 멀티스테이지 빌드로 빌드 도구가 최종 이미지에 포함되지 않는가
- 시크릿이 이미지 레이어에 포함되지 않는가
스캐닝 단계
- CI 파이프라인에서 Trivy 스캔이 필수로 실행되는가
- CRITICAL 취약점 발견 시 파이프라인이 중단되는가
- 스캔 결과가 중앙화된 대시보드에 수집되는가
- .trivyignore의 각 항목에 만료일과 사유가 기록되어 있는가
서명 단계
- 모든 프로덕션 이미지에 Cosign 서명이 적용되는가
- 키리스 서명을 사용하여 키 관리 부담을 제거했는가
- Rekor 투명성 로그에 서명이 기록되는가
SBOM 단계
- 모든 이미지에 SBOM이 생성되고 첨부되는가
- SBOM이 서명되어 변조 방지가 보장되는가
- SBOM 드리프트 탐지가 주기적으로 실행되는가
배포 단계
- Kubernetes Admission Controller가 서명 검증을 강제하는가
- 이미지 태그 대신 digest를 사용하는가
- 네임스페이스별 정책 예외가 최소화되어 있는가
마치며
컨테이너 이미지 보안은 단일 도구가 아닌 계층화된 전략으로 접근해야 합니다. Trivy로 알려진 취약점을 탐지하고, Cosign/Sigstore로 이미지 무결성을 보장하며, SBOM으로 구성 요소를 추적하고, SLSA로 빌드 출처를 증명하는 것이 현대적인 소프트웨어 공급망 보안의 표준입니다.
가장 중요한 것은 이 모든 과정을 자동화하는 것입니다. CI/CD 파이프라인에 보안 게이트를 통합하고, Kubernetes Admission Controller로 정책을 강제하며, SBOM 드리프트를 지속적으로 모니터링하세요. 보안은 한 번의 설정이 아니라 지속적인 프로세스입니다.