Skip to content
Published on

컨테이너 이미지 보안 파이프라인: Trivy·Sigstore·Cosign으로 구축하는 Supply Chain Security

Authors
  • Name
    Twitter
Container Image Security

들어가며

소프트웨어 공급망 공격(Supply Chain Attack)은 2019년부터 2024년까지 742% 증가했습니다. SolarWinds 사태, Log4Shell, Codecov 해킹 등 굵직한 사건들이 반복되면서 "빌드하고 배포하는 이미지를 얼마나 신뢰할 수 있는가"라는 질문이 모든 엔지니어링 조직의 핵심 과제가 되었습니다.

컨테이너 이미지는 현대 애플리케이션 배포의 기본 단위입니다. 그러나 이미지 내부에 어떤 패키지가 있는지, 알려진 취약점은 없는지, 이 이미지가 정말 우리 파이프라인에서 빌드된 것인지를 체계적으로 검증하지 않으면 프로덕션 환경은 언제든 위협에 노출될 수 있습니다.

이 글에서는 오픈소스 도구인 Trivy(취약점 스캐닝), Sigstore/Cosign(이미지 서명 및 검증), 그리고 SBOM(Software Bill of Materials) 생성을 결합하여 엔드투엔드 컨테이너 이미지 보안 파이프라인을 구축하는 방법을 다룹니다. CI/CD 통합부터 Kubernetes Admission Controller를 활용한 런타임 정책 적용까지, 실전에서 바로 적용 가능한 가이드를 제공합니다.

Supply Chain Security 아키텍처 개요

컨테이너 이미지 보안 파이프라인은 크게 네 단계로 구성됩니다.

┌─────────────────────────────────────────────────────────────────┐
Supply Chain Security Pipeline├─────────┬──────────────┬──────────────┬────────────────────────┤
BuildScanSignEnforce│         │              │              │                        │
DockerTrivyCosignKyverno /Build    (CVE Scan)    (KeylessOPA Gatekeeper│         │              │  Signing)     (Admission Controller)SBOMGrypeSigstore     │                        │
│ 생성     (대안 도구)   (Rekor Log)Policy Engine│         │              │              │                        │
Distro-Secret ScanFulcioImage Digest│ less    │               (인증서 발급)PinningBaseIaC Scan     │              │                        │
└─────────┴──────────────┴──────────────┴────────────────────────┘
     │            │              │                │
     ▼            ▼              ▼                ▼
  Registry    Report /       Signature        Allow / Deny
  Push        Gate CI/CD     in Registry       Pod Creation

1단계 Build: 최소 베이스 이미지 사용, 멀티 스테이지 빌드, SBOM 동시 생성 2단계 Scan: 이미지 내 OS 패키지 및 애플리케이션 의존성 취약점 스캐닝 3단계 Sign: 스캔 통과 이미지에 Cosign으로 디지털 서명 부착 4단계 Enforce: Kubernetes에서 서명 검증 및 정책 기반 배포 허용/차단

이 네 단계가 하나의 파이프라인으로 자동화되어야 인간의 실수를 배제하고 일관된 보안 수준을 유지할 수 있습니다.

Trivy 취약점 스캐닝 심화

Trivy 설치 및 기본 사용법

Trivy는 Aqua Security에서 개발한 오픈소스 보안 스캐너로, 컨테이너 이미지뿐 아니라 파일시스템, Git 저장소, Kubernetes 클러스터까지 스캔할 수 있습니다. 2026년 3월 기준 최신 안정 버전은 v0.68.x 시리즈입니다.

# macOS 설치
brew install trivy

# Linux 설치 (deb 패키지)
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
  gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] \
  https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | \
  sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy

# Docker로 실행 (설치 없이)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image python:3.12-slim

컨테이너 이미지 스캔 실전

# 기본 이미지 스캔
trivy image nginx:1.27

# 심각도 필터링: CRITICAL과 HIGH만 표시
trivy image --severity CRITICAL,HIGH myapp:v1.2.3

# 수정 가능한 취약점만 표시 (실제 조치 가능한 항목에 집중)
trivy image --ignore-unfixed --severity CRITICAL,HIGH myapp:v1.2.3

# JSON 포맷 출력 (CI/CD 파이프라인에서 파싱용)
trivy image --format json --output results.json myapp:v1.2.3

# SARIF 포맷 출력 (GitHub Security Tab 연동)
trivy image --format sarif --output trivy-results.sarif myapp:v1.2.3

# 종료 코드를 활용한 CI 게이트 (CRITICAL 발견 시 빌드 실패)
trivy image --exit-code 1 --severity CRITICAL myapp:v1.2.3

--exit-code 1 옵션은 CI/CD 파이프라인에서 핵심적인 역할을 합니다. 지정한 심각도 이상의 취약점이 발견되면 0이 아닌 종료 코드를 반환하여 파이프라인을 자동으로 중단시킵니다.

탐지 우선순위 모드

Trivy는 --detection-priority 플래그를 통해 탐지 정확도를 조절할 수 있습니다.

모드설명사용 시나리오
precise (기본값)오탐(false positive) 최소화 우선프로덕션 이미지 최종 검증
comprehensive미탐(false negative) 최소화 우선보안 감사, 전수 조사
# 포괄적 탐지 모드로 스캔
trivy image --detection-priority comprehensive myapp:v1.2.3

프로덕션 게이트에서는 precise 모드를 사용하여 불필요한 빌드 실패를 방지하고, 주기적인 보안 감사에서는 comprehensive 모드로 잠재적 위협까지 파악하는 이중 전략이 효과적입니다.

Trivy 설정 파일을 활용한 정책 관리

프로젝트 루트에 trivy.yaml을 배치하면 스캔 정책을 코드로 관리할 수 있습니다.

# trivy.yaml
scan:
  security-checks:
    - vuln
    - secret
    - misconfig
  severity:
    - CRITICAL
    - HIGH
  ignore-unfixed: true

vulnerability:
  type:
    - os
    - library

image:
  removed-pkgs: true
  input: []
# 특정 CVE 예외 처리 (승인된 위험 수용)
# .trivyignore 파일에서 관리

예외 처리가 필요한 CVE는 .trivyignore 파일에서 관리합니다.

# .trivyignore
# 2026-06-30까지 유예: 패치 미제공, 영향 범위 제한적
CVE-2024-12345  # libfoo: 로컬 권한 상승, 네트워크 접근 불가
CVE-2025-67890  # openssl: DoS 가능성, WAF에서 차단 중

.trivyignore에 CVE를 추가할 때는 반드시 사유와 유효 기간을 주석으로 기록해야 합니다. 예외 처리는 위험 수용의 의사결정이므로 감사 추적이 가능해야 합니다.

컨테이너 이미지 취약점 스캐너 비교

Trivy vs Grype vs Snyk 상세 비교

올바른 도구 선택을 위해 세 가지 주요 스캐너를 비교합니다.

항목TrivyGrypeSnyk Container
라이선스Apache 2.0 (완전 오픈소스)Apache 2.0 (완전 오픈소스)상용 (무료 티어 제한적)
개발사Aqua SecurityAnchoreSnyk Ltd.
스캔 대상이미지, FS, Repo, IaC, K8s이미지, FS, SBOM이미지, 코드, IaC
SBOM 생성내장 (CycloneDX, SPDX)Syft 연동 필요내장
Secret 탐지내장미지원별도 제품 (Snyk Code)
IaC 스캔내장 (Terraform, K8s 등)미지원내장
DB 업데이트전체 DB 다운로드 (~40MB)증분 업데이트 (~5MB)클라우드 기반 (자동)
오프라인 지원가능 (에어갭 환경)가능불가 (SaaS 의존)
스캔 속도매우 빠름빠름보통
자동 수정 제안미지원미지원지원 (PR 자동 생성)
CI/CD 통합GitHub Actions, GitLab CI, JenkinsGitHub Actions, GitLab CI모든 주요 CI/CD
VS Code 확장지원미지원지원
Kubernetes 운영자Trivy Operator미지원Snyk Controller
프라이버시로컬 처리로컬 처리클라우드 전송 필요

도구 선택 가이드라인:

  • Trivy: 올인원 오픈소스 솔루션이 필요하거나 에어갭(Air-gapped) 환경에서 운영할 때 최적의 선택입니다. 취약점, 시크릿, IaC 설정 오류까지 단일 도구로 커버합니다.
  • Grype: 프라이버시가 최우선이고 SBOM 기반 워크플로를 이미 구축한 조직에 적합합니다. Syft와 함께 사용하면 SBOM 생성부터 스캔까지 매끄럽게 연결됩니다. DB 증분 업데이트 덕분에 네트워크 대역폭이 제한된 환경에서도 유리합니다.
  • Snyk: 개발자 경험을 중시하고 자동 수정 PR 기능이 필요한 팀에 적합합니다. 다만 민감한 데이터를 클라우드로 전송해야 하므로 규정 준수 요건을 확인해야 합니다.

SBOM(Software Bill of Materials) 생성

SBOM의 필요성

SBOM은 소프트웨어에 포함된 모든 컴포넌트의 목록입니다. 미국 행정명령(EO 14028) 이후 SBOM 제출은 연방 정부 납품 소프트웨어의 필수 요건이 되었고, 민간 부문에서도 SBOM 요구가 확산되고 있습니다.

SBOM이 있으면 Log4Shell과 같은 제로데이 취약점이 공개되었을 때, 영향받는 이미지를 수 분 내에 식별하고 대응할 수 있습니다.

Trivy를 활용한 SBOM 생성

# CycloneDX 포맷으로 SBOM 생성
trivy image --format cyclonedx --output sbom.cdx.json myapp:v1.2.3

# SPDX 포맷으로 SBOM 생성
trivy image --format spdx-json --output sbom.spdx.json myapp:v1.2.3

# SBOM 기반 취약점 재스캔 (이미지 없이도 가능)
trivy sbom sbom.cdx.json

# Syft를 사용한 SBOM 생성 (Grype 연동 시)
syft myapp:v1.2.3 -o cyclonedx-json > sbom.cdx.json
grype sbom:sbom.cdx.json

SBOM을 한 번 생성해두면 이미지 원본 없이도 반복적으로 취약점을 재스캔할 수 있습니다. 새로운 CVE가 공개될 때마다 저장된 SBOM으로 영향 여부를 즉시 확인할 수 있어 운영 효율이 크게 향상됩니다.

SBOM 저장 전략

# OCI 레지스트리에 SBOM을 아티팩트로 첨부 (Cosign 활용)
cosign attach sbom --sbom sbom.cdx.json myregistry.io/myapp:v1.2.3

# SBOM을 이미지와 함께 레지스트리에 저장하면
# 이미지를 pull하는 쪽에서 SBOM도 함께 가져올 수 있음
cosign download sbom myregistry.io/myapp:v1.2.3

SBOM은 이미지와 동일한 레지스트리에 OCI 아티팩트로 저장하는 것이 베스트 프랙티스입니다. 별도 스토리지에 관리하면 이미지와 SBOM 사이의 동기화가 깨지기 쉽습니다.

Sigstore/Cosign 이미지 서명

Sigstore 생태계 이해

Sigstore는 소프트웨어 아티팩트의 서명, 검증, 투명성을 위한 오픈소스 표준입니다. 세 가지 핵심 컴포넌트로 구성됩니다.

  • Cosign: 컨테이너 이미지와 OCI 아티팩트에 서명하고 검증하는 CLI 도구
  • Fulcio: OpenID Connect(OIDC) 기반 단기 인증서를 발급하는 무료 인증 기관(CA). 인증서 유효 기간은 약 10분으로, 장기 키 관리 부담을 제거합니다.
  • Rekor: 서명 메타데이터를 기록하는 변조 불가능한 투명성 로그. 서명이 언제 생성되었는지 감사 추적을 제공합니다.

Cosign 설치

# macOS
brew install cosign

# Linux (Go Install)
go install github.com/sigstore/cosign/v2/cmd/cosign@latest

# GitHub Actions에서는 sigstore/cosign-installer 사용
# (아래 CI/CD 통합 섹션에서 상세 설명)

키 기반 서명 vs 키리스(Keyless) 서명

항목키 기반 서명키리스(Keyless) 서명
키 관리개인키 안전 보관 필요 (KMS 등)키 관리 불필요 (임시 키 자동 생성/폐기)
신뢰 근거개인키 보유 = 신뢰OIDC ID(GitHub, Google 등) = 신뢰
적합 환경에어갭, 엄격한 규정 준수CI/CD 자동화, 클라우드 네이티브
복잡도키 로테이션, 백업 필요OIDC 설정만 필요
투명성 로그선택 사항필수 (Rekor에 기록)

권장: CI/CD 환경에서는 키리스 서명을 사용하세요. 장기 키를 관리하지 않아도 되므로 키 유출 위험이 원천적으로 차단됩니다.

키리스 서명 실습

# 키리스 서명 (OIDC 기반)
# 로컬에서 실행 시 브라우저가 열리며 OIDC 인증 진행
cosign sign myregistry.io/myapp@sha256:abc123def456...

# 중요: 태그가 아닌 다이제스트로 서명해야 함
# 태그는 변경 가능하므로 다이제스트를 사용해야 Tag Mutability 공격을 방지
IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' myapp:v1.2.3)
cosign sign "${IMAGE_DIGEST}"

# 서명 검증
cosign verify \
  --certificate-identity=ci-pipeline@myorg.iam.gserviceaccount.com \
  --certificate-oidc-issuer=https://accounts.google.com \
  myregistry.io/myapp@sha256:abc123def456...

# 특정 GitHub Actions 워크플로에서 빌드된 이미지만 신뢰
cosign verify \
  --certificate-identity-regexp="https://github.com/myorg/myrepo/.github/workflows/.*" \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  myregistry.io/myapp@sha256:abc123def456...

서명 시 반드시 다이제스트(SHA256)를 사용해야 합니다. 태그(:latest, :v1.2.3)는 덮어쓸 수 있으므로, 서명 후 다른 이미지로 교체하는 Tag Mutability 공격에 취약합니다.

키 기반 서명 (에어갭 환경용)

# 키페어 생성
cosign generate-key-pair

# 생성된 키로 서명
cosign sign --key cosign.key myregistry.io/myapp@sha256:abc123...

# 공개키로 검증
cosign verify --key cosign.pub myregistry.io/myapp@sha256:abc123...

# KMS 연동 서명 (AWS KMS 예시)
cosign generate-key-pair --kms awskms:///alias/cosign-key
cosign sign --key awskms:///alias/cosign-key myregistry.io/myapp@sha256:abc123...

CI/CD 파이프라인 통합

GitHub Actions 완전한 파이프라인

아래는 빌드, 스캔, 서명, SBOM 생성을 하나의 워크플로에 통합한 예시입니다.

# .github/workflows/container-security.yml
name: Container Security Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

permissions:
  contents: read
  packages: write
  id-token: write # Cosign 키리스 서명에 필요
  security-events: write # SARIF 업로드에 필요

jobs:
  build-scan-sign:
    runs-on: ubuntu-latest
    steps:
      # 1. 체크아웃
      - name: Checkout repository
        uses: actions/checkout@v4

      # 2. Cosign 설치
      - name: Install Cosign
        uses: sigstore/cosign-installer@v3.8.2

      # 3. Docker Buildx 설정
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 4. 레지스트리 로그인
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # 5. 이미지 빌드 및 푸시
      - name: Build and push image
        id: build-push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # 6. Trivy 취약점 스캔 (CI 게이트)
      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
          format: 'table'
          exit-code: '1'
          ignore-unfixed: true
          severity: 'CRITICAL,HIGH'

      # 7. Trivy SARIF 리포트 (GitHub Security Tab)
      - name: Generate Trivy SARIF report
        uses: aquasecurity/trivy-action@0.28.0
        if: always()
        with:
          image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH,MEDIUM'

      - name: Upload SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

      # 8. SBOM 생성
      - name: Generate SBOM
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
          format: 'cyclonedx'
          output: 'sbom.cdx.json'

      # 9. Cosign 키리스 서명 (스캔 통과 후에만 실행)
      - name: Sign container image
        run: |
          cosign sign --yes \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }}

      # 10. SBOM을 이미지에 첨부
      - name: Attach SBOM to image
        run: |
          cosign attach sbom \
            --sbom sbom.cdx.json \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }}

      # 11. 서명 검증 (파이프라인 내 검증)
      - name: Verify signature
        run: |
          cosign verify \
            --certificate-identity-regexp="https://github.com/${{ github.repository }}/.github/workflows/.*" \
            --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-push.outputs.digest }}

이 워크플로에서 주목해야 할 핵심 포인트는 다음과 같습니다.

  • id-token: write 퍼미션이 없으면 키리스 서명이 실패합니다. GitHub Actions의 OIDC 토큰 발급에 이 권한이 필수입니다.
  • Trivy 스캔 단계에서 exit-code: '1'로 설정하면 CRITICAL/HIGH 취약점 발견 시 워크플로가 중단되어 서명 단계에 도달하지 않습니다. 즉, 취약한 이미지에는 서명이 붙지 않습니다.
  • steps.build-push.outputs.digest를 사용하여 태그가 아닌 다이제스트로 서명합니다.

GitLab CI 파이프라인 예시

# .gitlab-ci.yml
stages:
  - build
  - scan
  - sign

variables:
  IMAGE_TAG: '${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}'

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
    - |
      DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $IMAGE_TAG)
      echo "IMAGE_DIGEST=${DIGEST}" >> build.env
  artifacts:
    reports:
      dotenv: build.env

scan:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: ['']
  script:
    - trivy image --exit-code 1 --severity CRITICAL,HIGH --ignore-unfixed $IMAGE_TAG
    - trivy image --format cyclonedx --output sbom.cdx.json $IMAGE_TAG
  artifacts:
    paths:
      - sbom.cdx.json
    when: always

sign:
  stage: sign
  image: bitnami/cosign:latest
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  script:
    - cosign sign --yes ${IMAGE_DIGEST}
  needs:
    - job: build
      artifacts: true
    - job: scan

보안 강화 Dockerfile 작성

취약점 스캐닝 결과를 최소화하려면 이미지 빌드 단계에서부터 보안을 고려해야 합니다.

# ---- 보안 강화 멀티 스테이지 Dockerfile ----

# Stage 1: 빌드 (빌드 의존성은 최종 이미지에 포함되지 않음)
FROM golang:1.23-alpine AS builder

# 보안: 루트가 아닌 사용자로 빌드
RUN adduser -D -u 10001 appuser

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .

# CGO 비활성화로 정적 바이너리 생성 (외부 C 라이브러리 의존성 제거)
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags='-w -s -extldflags "-static"' \
    -o /app/server ./cmd/server

# Stage 2: 최종 이미지 (Distroless 사용)
FROM gcr.io/distroless/static-debian12:nonroot

# 빌드 스테이지에서 바이너리만 복사
COPY --from=builder /app/server /server

# 비루트 사용자로 실행
USER 10001:10001

# 헬스체크용 포트만 노출
EXPOSE 8080

ENTRYPOINT ["/server"]

Distroless 베이스 이미지를 사용하는 이유: 셸, 패키지 관리자, 일반적인 유틸리티가 없으므로 공격 표면이 극소화됩니다. Alpine 이미지도 약 5MB로 작지만 셸과 패키지 관리자가 포함되어 있어 컨테이너 침입 후 lateral movement에 활용될 수 있습니다. Distroless는 이러한 도구 자체가 없으므로 설령 컨테이너가 침해되더라도 공격자가 활용할 수 있는 도구가 없습니다.

Kubernetes Admission Controller 연동

Kyverno를 활용한 이미지 서명 검증 정책

Kubernetes Admission Controller는 Pod 생성 요청을 가로채어 정책을 검증합니다. Kyverno를 사용하면 YAML 기반으로 직관적인 정책을 작성할 수 있습니다.

# kyverno-verify-image-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-container-image-signatures
  annotations:
    policies.kyverno.io/title: Verify Image Signatures
    policies.kyverno.io/description: >-
      서명되지 않은 컨테이너 이미지의 배포를 차단합니다.
      Cosign keyless 서명이 확인된 이미지만 허용합니다.
spec:
  validationFailureAction: Enforce # Audit(경고만) 또는 Enforce(차단)
  background: false
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
              namespaces:
                - production
                - staging
      verifyImages:
        - imageReferences:
            - 'ghcr.io/myorg/*'
            - 'myregistry.io/myorg/*'
          attestors:
            - entries:
                - keyless:
                    subject: 'https://github.com/myorg/myrepo/.github/workflows/*'
                    issuer: 'https://token.actions.githubusercontent.com'
                    rekor:
                      url: https://rekor.sigstore.dev
          mutateDigest: true # 태그를 다이제스트로 자동 변환
          verifyDigest: true # 다이제스트 일치 검증
          required: true
# Kyverno 설치 (Helm)
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace \
  --set replicaCount=3

# 정책 적용
kubectl apply -f kyverno-verify-image-policy.yaml

# 정책 테스트: 서명되지 않은 이미지로 Pod 생성 시도
kubectl run test --image=nginx:latest -n production
# 결과: Error from server: admission webhook denied the request:
# resource Pod/production/test was blocked due to the following policies:
# verify-container-image-signatures

OPA Gatekeeper를 활용한 대안 구성

# ConstraintTemplate 정의
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedregistries
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRegistries
      validation:
        openAPIV3Schema:
          type: object
          properties:
            registries:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedregistries
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not startswith(container.image, input.parameters.registries[_])
          msg := sprintf(
            "컨테이너 이미지 '%v'는 허용된 레지스트리에서 가져온 것이 아닙니다. 허용 목록: %v",
            [container.image, input.parameters.registries]
          )
        }
---
# Constraint 적용
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
  name: allowed-registries
spec:
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Pod']
    namespaces: ['production', 'staging']
  parameters:
    registries:
      - 'ghcr.io/myorg/'
      - 'myregistry.io/myorg/'

Kyverno vs OPA Gatekeeper 선택 기준: Kyverno는 이미지 서명 검증이 네이티브로 내장되어 있어 Cosign 연동이 간편합니다. OPA Gatekeeper는 Rego 언어로 더 복잡한 정책을 표현할 수 있지만, 이미지 서명 검증에는 External Data Provider를 별도로 구성해야 합니다. Supply Chain Security가 주 목적이라면 Kyverno가 설정이 간단하고 유지보수 부담이 적습니다.

운영 시 주의사항 및 장애 대응

흔한 장애 시나리오와 복구 절차

장애 1: Trivy DB 다운로드 실패

CI/CD에서 Trivy가 취약점 데이터베이스를 다운로드하지 못하면 스캔 자체가 불가능합니다.

# 증상
FATAL  failed to download vulnerability DB

# 원인: GitHub API Rate Limit, 네트워크 제한
# 복구 절차:

# 1. DB를 수동 다운로드하여 캐싱
trivy image --download-db-only
# DB 위치: ~/.cache/trivy/db/

# 2. CI에서 DB 캐시 활용 (GitHub Actions 예시)
# - name: Cache Trivy DB
#   uses: actions/cache@v4
#   with:
#     path: ~/.cache/trivy
#     key: trivy-db-${{ hashFiles('.trivy-db-timestamp') }}

# 3. 에어갭 환경에서 OCI 아티팩트로 DB 미러링
oras pull ghcr.io/aquasecurity/trivy-db:2 -o ./trivy-db/
trivy image --cache-dir ./trivy-db/ --skip-db-update myapp:v1.2.3

장애 2: Cosign 키리스 서명 실패 (OIDC 토큰 문제)

# 증상
Error: getting identity token: oauth2/google: ...

# GitHub Actions에서 발생하는 경우:
# 원인 1: permissions에 id-token: write 누락
# 원인 2: fork된 PR에서는 OIDC 토큰 발급 불가 (보안 정책)
# 원인 3: Fulcio/Rekor 서비스 장애

# 복구 절차:
# 1. permissions 확인
permissions:
  id-token: write    # 이 줄이 반드시 필요

# 2. Sigstore 서비스 상태 확인
curl -s https://status.sigstore.dev/ | jq .

# 3. 폴백: 키 기반 서명으로 전환
cosign sign --key env://COSIGN_PRIVATE_KEY \
  myregistry.io/myapp@sha256:abc123...

장애 3: Admission Controller가 정상 이미지를 차단

# 증상: 서명된 이미지인데 배포가 거부됨
# Error: failed to verify image signature

# 디버깅 절차:
# 1. 로컬에서 서명 검증 먼저 확인
cosign verify \
  --certificate-identity-regexp="..." \
  --certificate-oidc-issuer="..." \
  myregistry.io/myapp@sha256:abc123...

# 2. Kyverno 로그 확인
kubectl logs -n kyverno -l app.kubernetes.io/component=admission-controller

# 3. 정책의 issuer/subject 패턴이 실제 인증서와 일치하는지 확인
cosign verify myregistry.io/myapp@sha256:abc123... 2>&1 | \
  grep -E "(Issuer|Subject)"

# 4. 긴급 복구: 정책을 Audit 모드로 전환 (차단 -> 경고)
kubectl patch clusterpolicy verify-container-image-signatures \
  --type='json' \
  -p='[{"op": "replace", "path": "/spec/validationFailureAction", "value": "Audit"}]'

장애 4: 레지스트리 서명 저장소 접근 불가

# 증상: 서명은 성공했지만 검증 시 서명을 찾을 수 없음
# Error: no matching signatures found

# 원인: 서명이 별도 저장소에 저장되었거나 레지스트리가 OCI 1.1 미지원
# 복구 절차:

# 1. 서명이 저장된 위치 확인
crane ls myregistry.io/myapp | grep sha256-

# 2. 수동으로 서명 저장소 지정
cosign verify \
  --certificate-identity-regexp="..." \
  --certificate-oidc-issuer="..." \
  --registry-referrers-mode=oci-1-1 \
  myregistry.io/myapp@sha256:abc123...

운영 체크리스트

빌드 단계 체크리스트:

  • 멀티 스테이지 빌드를 사용하여 빌드 도구가 최종 이미지에 포함되지 않도록 한다
  • Distroless 또는 최소 베이스 이미지(Alpine, scratch)를 사용한다
  • 이미지에 셸, 패키지 관리자 등 불필요한 도구를 포함하지 않는다
  • 비루트(non-root) 사용자로 프로세스를 실행한다
  • .dockerignore로 시크릿, 소스 코드 등이 이미지에 포함되지 않도록 한다
  • 베이스 이미지 태그를 고정(pinning)하고 다이제스트를 사용한다

스캔 단계 체크리스트:

  • CI 파이프라인에서 Trivy 스캔이 실패하면 빌드가 중단되는가
  • CRITICAL, HIGH 취약점에 대한 게이트가 설정되어 있는가
  • .trivyignore 예외 항목에 사유와 만료일이 기록되어 있는가
  • SBOM이 모든 빌드에서 자동 생성되는가
  • SBOM이 이미지와 함께 레지스트리에 저장되는가
  • 주기적 재스캔(새 CVE 대응)이 예약되어 있는가

서명 단계 체크리스트:

  • 키리스 서명을 사용하고 OIDC 설정이 올바른가
  • 다이제스트(SHA256) 기반으로 서명하는가 (태그 아님)
  • 서명이 레지스트리에 정상적으로 저장되는가
  • Fulcio/Rekor 서비스 장애 시 폴백 전략이 있는가

배포 단계 체크리스트:

  • Admission Controller(Kyverno/Gatekeeper)가 프로덕션 네임스페이스에 적용되어 있는가
  • 서명 검증 정책이 Enforce 모드인가 (Audit가 아닌)
  • Admission Controller 장애 시 failurePolicy가 적절히 설정되어 있는가 (Fail 권장)
  • Deployment manifest에서 이미지 태그 대신 다이제스트를 사용하는가

모니터링 체크리스트:

  • Trivy DB 업데이트 성공/실패 알림이 설정되어 있는가
  • 서명 검증 실패 이벤트에 대한 알림이 있는가
  • Admission Controller 가용성 모니터링이 설정되어 있는가
  • SBOM 기반 주기적 재스캔 결과를 대시보드로 확인할 수 있는가

프로모션 단계별 서명 검증 전략

실무에서는 Dev, Staging, Production 환경 간 이미지를 프로모션할 때 각 단계에서 서명을 검증하고, 상위 환경에서는 추가 서명을 부착하는 다계층 서명 전략을 사용합니다.

Dev 빌드 완료 → [Trivy Scan][CI 서명 부착]
              Staging 배포 ← [CI 서명 검증]QA 테스트 통과
              Production 배포 ← [CI 서명 + QA 서명 검증]
# 1. CI 파이프라인에서 빌드 후 서명 (위의 GitHub Actions 참조)

# 2. QA 팀이 테스트 통과 후 추가 서명
cosign sign --key env://QA_COSIGN_KEY \
  -a "qa-approved=true" \
  -a "qa-tester=jane.doe" \
  -a "test-suite=integration-v3" \
  myregistry.io/myapp@sha256:abc123...

# 3. Production Kyverno 정책: CI 서명 + QA 서명 모두 필수
# verifyImages에 attestors를 AND 조건으로 구성

이 전략을 적용하면 프로덕션에 배포되는 모든 이미지가 (1) CI 파이프라인에서 빌드되었고, (2) 취약점 스캔을 통과했으며, (3) QA 테스트를 통과했음이 암호학적으로 보장됩니다.

고급 주제: Attestation과 SLSA

Cosign Attestation

Attestation은 이미지에 대한 메타데이터(빌드 출처, 스캔 결과 등)를 서명과 함께 부착하는 메커니즘입니다. 단순 서명이 "이 이미지를 누가 서명했는가"를 증명한다면, Attestation은 "이 이미지가 어떤 조건에서 빌드되었는가"까지 증명합니다.

# 취약점 스캔 결과를 Attestation으로 부착
trivy image --format cosign-vuln --output vuln-att.json myapp:v1.2.3
cosign attest --predicate vuln-att.json \
  --type vuln \
  myregistry.io/myapp@sha256:abc123...

# SBOM을 Attestation으로 부착
cosign attest --predicate sbom.cdx.json \
  --type cyclonedx \
  myregistry.io/myapp@sha256:abc123...

# Attestation 검증
cosign verify-attestation \
  --certificate-identity-regexp="..." \
  --certificate-oidc-issuer="..." \
  --type vuln \
  myregistry.io/myapp@sha256:abc123...

SLSA(Supply-chain Levels for Software Artifacts)

SLSA는 소프트웨어 공급망 보안 성숙도를 4단계로 정의하는 프레임워크입니다. Sigstore 생태계는 SLSA Level 3 달성을 위한 핵심 인프라를 제공합니다.

SLSA Level요구사항이 파이프라인의 커버리지
Level 1빌드 프로세스 문서화GitHub Actions 워크플로 YAML
Level 2빌드 서비스 사용, 출처 생성GitHub Actions + Cosign 서명
Level 3빌드 환경 격리, 변조 방지Keyless 서명 + Rekor 투명성 로그
Level 42인 이상 리뷰, 재현 가능 빌드추가 구성 필요

성능 최적화 팁

Trivy 스캔 성능 개선

# 1. DB 캐싱으로 반복 다운로드 방지
# CI에서 캐시 디렉토리를 persist
trivy image --cache-dir /shared-cache/trivy myapp:v1.2.3

# 2. 불필요한 스캔 타입 비활성화
# 취약점만 필요할 때 secret, misconfig 스캔 제외
trivy image --scanners vuln myapp:v1.2.3

# 3. 병렬 스캐닝 (Trivy v0.68+ 동시 DB 접근 지원)
# 여러 이미지를 동시에 스캔할 때 DB lock 이슈 해소
parallel -j4 trivy image --cache-dir /shared-cache/trivy {} ::: \
  myapp-api:v1.2.3 myapp-web:v1.2.3 myapp-worker:v1.2.3 myapp-cron:v1.2.3

이미지 크기와 취약점의 상관관계

베이스 이미지 선택이 취약점 수에 직접적인 영향을 미칩니다.

베이스 이미지크기 (압축)일반적 CVE 수권장 용도
ubuntu:24.04~29MB30~80개범용 (비권장)
debian:bookworm-slim~25MB20~50개범용
alpine:3.21~3.5MB0~5개경량 서비스
gcr.io/distroless/static~2MB0~2개Go 정적 바이너리
scratch0MB0개정적 바이너리 전용

자주 묻는 질문 (FAQ)

Q: 키리스 서명의 인증서 유효기간이 10분인데, 나중에 검증할 수 있나요? A: 가능합니다. Rekor 투명성 로그에 서명 시점이 기록되어 있으므로, 인증서가 만료된 후에도 "서명 당시에는 인증서가 유효했다"는 것을 검증할 수 있습니다. 이것이 Rekor의 핵심 가치입니다.

Q: 기존 이미지에 소급해서 서명을 붙일 수 있나요? A: 기술적으로는 가능하지만 권장하지 않습니다. 서명의 의미는 "이 이미지가 신뢰할 수 있는 파이프라인에서 빌드되었다"는 보증인데, 사후 서명은 이 보증의 의미를 약화시킵니다.

Q: 멀티 아키텍처(multi-arch) 이미지도 서명할 수 있나요? A: Cosign은 매니페스트 인덱스(manifest list) 서명을 지원합니다. cosign sign은 자동으로 매니페스트 인덱스 레벨에서 서명하므로 포함된 모든 아키텍처 이미지가 함께 검증됩니다.

Q: Private Registry(Harbor, Nexus 등)에서도 키리스 서명이 동작하나요? A: 서명 자체는 동작하지만, 서명 저장 시 레지스트리가 OCI 1.1 Referrers API 또는 Tag-based Discovery를 지원해야 합니다. Harbor 2.9+, Nexus 3.x 최신 버전은 지원합니다.

참고자료