Skip to content
Published on

컨테이너 이미지 보안과 소프트웨어 공급망 보호: Trivy, Cosign, SBOM, Sigstore 실전 가이드

Authors
  • Name
    Twitter
Container Image Security

들어가며

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

취약점 스캐너 비교

기능TrivyGrypeSnykClair
라이선스Apache 2.0Apache 2.0상용(무료 플랜 있음)Apache 2.0
컨테이너 이미지 스캔OOOO
파일시스템 스캔OOOX
IaC 스캔OXOX
SBOM 생성OX (Syft 연동)OX
시크릿 탐지OXOX
Kubernetes 스캔OXOX
CI/CD 통합 용이성높음높음매우 높음보통
오프라인 지원OOXO

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 포맷 비교

항목SPDXCycloneDX
주관 기관Linux FoundationOWASP
ISO 표준ISO/IEC 5962:2021ECMA-424
주요 용도라이선스 컴플라이언스보안 취약점 관리
지원 형식JSON, RDF, YAML, Tag-ValueJSON, XML, Protocol Buffers
VEX 지원OO
서비스 의존성 표현제한적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 }}

위 파이프라인은 다음 순서로 동작합니다.

  1. Docker 이미지를 빌드하고 GHCR에 푸시
  2. Trivy로 HIGH/CRITICAL 취약점을 스캔하고, 발견 시 파이프라인을 중단
  3. Syft로 SBOM을 생성
  4. Cosign 키리스 서명으로 이미지에 서명
  5. 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)가 너무 넓게 설정되어 있었습니다.

복구 절차:

  1. 해당 Pod를 즉시 격리(NetworkPolicy로 외부 통신 차단)
  2. 이미지를 Trivy로 긴급 스캔
  3. 동일 이미지를 Cosign으로 사후 서명하거나, 서명된 이미지로 교체
  4. 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 취약점이 발표되었습니다.

복구 절차:

  1. 영향 범위 분석: SBOM을 활용하여 해당 패키지를 사용하는 모든 이미지를 식별
  2. 긴급 패치 이미지 빌드 및 배포
  3. 롤아웃 진행 상황 모니터링
# 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을 실행하는 초기화 스크립트가 있었습니다.

대응:

  1. 불변(Immutable) 컨테이너 원칙을 강제하도록 readOnlyRootFilesystem 설정
  2. 런타임 SBOM 비교를 정기적으로 수행하는 CronJob 배포
  3. 드리프트 발생 시 자동 알림 구성

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 드리프트를 지속적으로 모니터링하세요. 보안은 한 번의 설정이 아니라 지속적인 프로세스입니다.

참고자료