Split View: 컨테이너 이미지 보안과 소프트웨어 공급망 보호: Trivy, Cosign, SBOM, Sigstore 실전 가이드
컨테이너 이미지 보안과 소프트웨어 공급망 보호: Trivy, Cosign, SBOM, Sigstore 실전 가이드
- 들어가며
- 컨테이너 이미지 위협 모델
- 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 드리프트를 지속적으로 모니터링하세요. 보안은 한 번의 설정이 아니라 지속적인 프로세스입니다.
참고자료
Container Image Security and Software Supply Chain Protection: A Practical Guide to Trivy, Cosign, SBOM, and Sigstore
- Introduction
- Container Image Threat Model
- Trivy Vulnerability Scanning
- Cosign/Sigstore Image Signing
- SBOM Generation and Management
- CI/CD Pipeline Integration
- Kubernetes Admission Policies
- Failure Cases and Recovery
- SLSA Framework and Build Provenance
- Operations Checklist
- Conclusion
- References

Introduction
The XZ Utils backdoor incident in 2024 (CVE-2024-3094) demonstrated to the world just how sophisticated and dangerous software supply chain attacks can be. In the current era where container-based deployments are the standard, a single image contains hundreds of dependencies, and if even one is compromised, your entire production environment is at risk.
This article covers the complete container image security lifecycle. From vulnerability scanning (Trivy), image signing (Cosign/Sigstore), Software Bill of Materials (SBOM), to supply chain integrity verification using the SLSA framework -- we provide production-ready code that you can apply immediately.
Container Image Threat Model
Attack vectors surrounding container images can be broadly classified into five categories.
| Threat Type | Description | Countermeasure |
|---|---|---|
| Known Vulnerabilities (CVE) | Public vulnerabilities in base images or packages | Trivy, Grype scanning |
| Image Tampering | Images replaced or modified in the registry | Cosign signing + verification |
| Dependency Confusion | Malicious packages registered with internal package names | SBOM-based dependency tracking |
| Build Environment Compromise | The CI/CD pipeline itself is compromised | SLSA build provenance attestation |
| Privilege Escalation | Container escape to host | Least privilege principle + runtime security |
No single tool is sufficient against each threat. A Defense in Depth strategy is required.
Trivy Vulnerability Scanning
Introduction and Installation
Trivy is an open-source security scanner developed by Aqua Security that can scan container images, filesystems, Git repositories, and Kubernetes clusters. As of 2026, version 0.68 and above supports read-only database mode, allowing multiple processes to perform scans concurrently.
# Install Trivy (macOS)
brew install trivy
# Install Trivy (Linux)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Verify installation
trivy version
Running Image Scans
# Basic image scan - show all severity levels
trivy image nginx:1.25
# Filter only HIGH and CRITICAL, show only fixable vulnerabilities
trivy image --severity HIGH,CRITICAL --ignore-unfixed nginx:1.25
# Output results in JSON format (for CI/CD pipeline integration)
trivy image --format json --output results.json nginx:1.25
# Use a .trivyignore file to skip specific CVEs
trivy image --ignorefile .trivyignore myapp:latest
# SBOM-based scan (use an existing SBOM as input)
trivy sbom ./sbom.cyclonedx.json
Configuring .trivyignore
In production environments, use .trivyignore to manage false positives or vulnerabilities that cannot be immediately fixed.
# .trivyignore - Approved exception list
# Always document expiration date and reason
# CVE-2024-1234: Feature not used, grace period until 2026-04-01
CVE-2024-1234
# CVE-2024-5678: Waiting for upstream patch
CVE-2024-5678
Vulnerability Scanner Comparison
| Feature | Trivy | Grype | Snyk | Clair |
|---|---|---|---|---|
| License | Apache 2.0 | Apache 2.0 | Commercial (free tier) | Apache 2.0 |
| Container Image Scan | Yes | Yes | Yes | Yes |
| Filesystem Scan | Yes | Yes | Yes | No |
| IaC Scan | Yes | No | Yes | No |
| SBOM Generation | Yes | No (Syft integration) | Yes | No |
| Secret Detection | Yes | No | Yes | No |
| Kubernetes Scan | Yes | No | Yes | No |
| CI/CD Integration | High | High | Very High | Medium |
| Offline Support | Yes | Yes | No | Yes |
Trivy covers the broadest scope with a single tool, and its ability to work in offline environments is a key strength.
Cosign/Sigstore Image Signing
Sigstore Architecture
Sigstore is an open-source project for software signing, composed of three core components.
- Cosign: Signing and verification tool for container images and OCI artifacts
- Fulcio: OIDC-based short-lived certificate authority (CA)
- Rekor: Immutable transparency log for recording signatures
In keyless signing mode, developers do not need to manage separate keys -- they can sign using OIDC credentials from GitHub, Google, and other identity providers.
Signing Images with Cosign
# Install Cosign
brew install cosign
# 1. Generate key pair (traditional approach)
cosign generate-key-pair
# 2. Sign image (key-based)
cosign sign --key cosign.key myregistry.io/myapp:v1.0.0
# 3. Keyless signing (using Sigstore Fulcio - recommended)
# Automatically issues short-lived certificate after OIDC authentication
cosign sign myregistry.io/myapp:v1.0.0
# 4. Verify signature
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0
# 5. Keyless signature verification (specify certificate issuer and identity)
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/myorg/myrepo" \
myregistry.io/myapp:v1.0.0
Attaching Metadata to Signatures
Cosign can add custom annotations during signing, which is useful for build provenance tracking.
# Include build metadata in signature
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
# Verify signature with annotations
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0 | jq .
SBOM Generation and Management
What is an SBOM
SBOM (Software Bill of Materials) is a structured document listing all components, libraries, and dependencies included in a piece of software. Following US Executive Order 14028 (EO 14028), SBOM submission has become mandatory for software delivered to federal agencies, and global regulatory trends are converging on SBOM as a standard requirement.
SBOM Format Comparison
| Aspect | SPDX | CycloneDX |
|---|---|---|
| Governing Body | Linux Foundation | OWASP |
| ISO Standard | ISO/IEC 5962:2021 | ECMA-424 |
| Primary Use Case | License compliance | Security vulnerability management |
| Supported Formats | JSON, RDF, YAML, Tag-Value | JSON, XML, Protocol Buffers |
| VEX Support | Yes | Yes |
| Service Dependency Mapping | Limited | Yes |
| Tool Ecosystem | Broad | Rapidly growing |
Choose SPDX when license compliance is the priority, and CycloneDX when security vulnerability management comes first. Generating both formats is the most ideal approach.
Generating SBOMs with Syft
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Generate SBOM in CycloneDX format (container image)
syft myregistry.io/myapp:v1.0.0 -o cyclonedx-json=sbom-cyclonedx.json
# Generate SBOM in SPDX format
syft myregistry.io/myapp:v1.0.0 -o spdx-json=sbom-spdx.json
# Generate SBOM from local directory
syft dir:./my-project -o cyclonedx-json=sbom.json
# Trivy can also generate SBOMs
trivy image --format cyclonedx --output sbom-trivy.json myregistry.io/myapp:v1.0.0
# Attach SBOM to OCI registry (using Cosign)
cosign attach sbom --sbom sbom-cyclonedx.json myregistry.io/myapp:v1.0.0
SBOM Drift Detection
An SBOM is a snapshot at build time, so it may differ from packages installed at runtime. This is called SBOM drift.
# Compare runtime SBOM with build-time SBOM
# 1. Build-time SBOM (already generated)
# sbom-build.json
# 2. Extract current state from running container
docker exec running-container syft / -o cyclonedx-json > sbom-runtime.json
# 3. Compare differences
diff <(jq -r '.components[].name' sbom-build.json | sort) \
<(jq -r '.components[].name' sbom-runtime.json | sort)
CI/CD Pipeline Integration
GitHub Actions Integrated Pipeline
The following is an example that integrates build, scan, sign, and SBOM generation into a single pipeline.
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 # Required for keyless signing
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: Vulnerability Scan
- 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: Generate 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: Sign Image with 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: Attach 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: Sign SBOM Attestation
- name: Sign SBOM Attestation
run: |
cosign attest --yes \
--predicate sbom.cyclonedx.json \
--type cyclonedx \
ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}
This pipeline operates in the following order:
- Build Docker image and push to GHCR
- Scan for HIGH/CRITICAL vulnerabilities with Trivy, halting the pipeline on findings
- Generate SBOM with Syft
- Sign the image with Cosign keyless signing
- Attach and sign the SBOM
Kubernetes Admission Policies
Image Signature Verification with Kyverno
To block deployment of unsigned images in a Kubernetes cluster, use an Admission Controller. Kyverno is a policy-based Kubernetes-native tool that allows you to declaratively define image signature verification.
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: ''
Policy with OPA Gatekeeper
For environments using OPA Gatekeeper, define a 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
Failure Cases and Recovery
Case 1: Unsigned Image Deployed to Production
Situation: An unsigned image was deployed to a namespace where the Admission Controller was not configured.
Root Cause: Namespace-level policy exemptions were set too broadly.
Recovery Steps:
- Immediately isolate the affected Pod (block external communication via NetworkPolicy)
- Perform an emergency scan of the image with Trivy
- Either retroactively sign the same image with Cosign, or replace with a signed image
- Narrow the scope of Admission Controller policy exemptions
# Emergency isolation: apply 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
# Emergency scan
trivy image --severity CRITICAL compromised-image:tag
Case 2: CRITICAL CVE Discovered in Production
Situation: A CRITICAL vulnerability was disclosed in the base image of a running service.
Recovery Steps:
- Impact analysis: Use SBOMs to identify all images using the affected package
- Build and deploy an emergency patched image
- Monitor rollout progress
# Search SBOMs for affected images
for sbom in sbom-*.json; do
if jq -e '.components[] | select(.name == "libexpat")' "$sbom" > /dev/null 2>&1; then
echo "AFFECTED: $sbom"
fi
done
# Rebuild with patched base image
docker build --no-cache --build-arg BASE_IMAGE=nginx:1.25.4-alpine -t myapp:patched .
# Rolling update
kubectl set image deployment/myapp myapp=myregistry.io/myapp:patched
kubectl rollout status deployment/myapp
Case 3: SBOM Drift Detected
Situation: Packages not present in the build-time SBOM were found installed in the runtime container.
Root Cause: An initialization script was running apt-get install inside the container.
Response:
- Enforce immutable container principles by setting readOnlyRootFilesystem
- Deploy a CronJob to periodically compare runtime SBOMs
- Configure automatic alerts on drift detection
SLSA Framework and Build Provenance
SLSA (Supply-chain Levels for Software Artifacts) is a framework for ensuring the integrity of the software supply chain. Security levels can be incrementally raised from Level 0 to Level 3.
| Level | Requirement | Description |
|---|---|---|
| SLSA 0 | None | No security guarantees |
| SLSA 1 | Provenance exists | Build process is documented |
| SLSA 2 | Hosted build service | Build service generates signed provenance |
| SLSA 3 | Hardened build environment | Tamper-resistant build environment |
To implement SLSA Level 3 build provenance attestation in GitHub Actions, use the slsa-framework/slsa-github-generator.
# SLSA Provenance generation workflow
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 }}
Operations Checklist
Use the following checklist when operating container image security in production environments.
Build Phase
- Are all images using minimal base images (distroless, alpine)?
- Do Dockerfiles use pinned versions (tag + digest)?
- Are multi-stage builds ensuring build tools are excluded from the final image?
- Are secrets excluded from image layers?
Scanning Phase
- Is Trivy scanning mandatory in the CI pipeline?
- Does the pipeline halt on CRITICAL vulnerability findings?
- Are scan results collected in a centralized dashboard?
- Does each .trivyignore entry have a documented expiration date and reason?
Signing Phase
- Do all production images have Cosign signatures applied?
- Is keyless signing used to eliminate key management burden?
- Are signatures recorded in the Rekor transparency log?
SBOM Phase
- Is an SBOM generated and attached to every image?
- Is the SBOM signed to ensure tamper resistance?
- Is SBOM drift detection running periodically?
Deployment Phase
- Does the Kubernetes Admission Controller enforce signature verification?
- Are image digests used instead of tags?
- Are namespace-level policy exemptions minimized?
Conclusion
Container image security must be approached with a layered strategy, not a single tool. Detecting known vulnerabilities with Trivy, ensuring image integrity with Cosign/Sigstore, tracking components with SBOM, and attesting build provenance with SLSA is the modern standard for software supply chain security.
The most important thing is to automate all of these processes. Integrate security gates into your CI/CD pipeline, enforce policies with Kubernetes Admission Controllers, and continuously monitor for SBOM drift. Security is not a one-time configuration -- it is a continuous process.
References
- Trivy Official Docs - Container Image Scanning
- Sigstore Official Docs - Signing Containers with Cosign
- SLSA Framework Official Site
- OWASP CycloneDX - SBOM Standard
- Anchore Syft - SBOM Generation Tool
- Chainguard Academy - Introduction to Sigstore
- Aqua Security Blog - Trivy and Cosign in GitHub Actions
- Sonatype - SPDX vs CycloneDX Comparison