컨테이너 이미지 보안
지원 버전: Trivy 0.56+, Kubernetes 1.31, 1.32, 1.33 마지막 업데이트: 2026년 2월 22일
컨테이너 이미지 보안은 Kubernetes 보안의 첫 번째 방어선입니다. 이 문서에서는 이미지 스캐닝, 서명, 검증, 그리고 공급망 보안에 대해 다룹니다.
목차
- 이미지 스캐닝 개요
- Trivy
- Amazon ECR 이미지 스캐닝
- Cosign/Sigstore를 사용한 이미지 서명
- Admission Control에서 이미지 검증
- 공급망 보안
- 기본 이미지 선택
- 이미지 레지스트리 모범 사례
- CI/CD 파이프라인 통합
이미지 스캐닝 개요
Shift-Left 보안
이미지 보안은 개발 프로세스 초기 단계에서 시작해야 합니다.
┌─────────────────────────────────────────────────────────────────────────┐
│ Shift-Left 이미지 보안 │
│ │
│ 개발 단계 빌드 단계 런타임 단계 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ IDE │ │ CI/CD │ │ K8s │ │
│ │ 스캐닝 │──────────────▶│ 스캐닝 │──────────────▶│ 스캐닝 │ │
│ │ │ │ │ │ │ │
│ │ • 로컬 │ │ • 빌드 │ │ • 런타임│ │
│ │ 스캔 │ │ 게이트│ │ 모니터│ │
│ │ • 의존성│ │ • 레지 │ │ • 정책 │ │
│ │ 검사 │ │ 스트리│ │ 적용 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ◀─────────────────── 비용 및 수정 용이성 ─────────────────────▶ │
│ 낮음 높음 │
└─────────────────────────────────────────────────────────────────────────┘스캐닝 대상
| 대상 | 설명 | 도구 |
|---|---|---|
| OS 패키지 | 운영체제 패키지 취약점 | Trivy, Grype, Clair |
| 언어별 의존성 | npm, pip, go modules 등 | Trivy, Snyk |
| 설정 오류 | Dockerfile, K8s 매니페스트 | Trivy, Checkov |
| 시크릿 | 하드코딩된 비밀 정보 | Trivy, Trufflehog |
| 라이선스 | 오픈소스 라이선스 | Trivy, FOSSA |
Trivy
Trivy 개요
Trivy는 컨테이너, 파일시스템, Git 저장소 등을 스캔하는 종합 보안 스캐너입니다.
Trivy 설치
bash
# macOS
brew install trivy
# Linux (Debian/Ubuntu)
sudo apt-get install wget apt-transport-https gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
# Docker
docker pull aquasec/trivy:latest이미지 스캐닝
bash
# 기본 이미지 스캔
trivy image nginx:latest
# 심각도 필터링
trivy image --severity HIGH,CRITICAL nginx:latest
# JSON 출력
trivy image --format json --output results.json nginx:latest
# SARIF 형식 (GitHub Security에 통합)
trivy image --format sarif --output results.sarif nginx:latest
# 특정 취약점 무시
trivy image --ignore-unfixed nginx:latest
# 시크릿 스캔 포함
trivy image --scanners vuln,secret nginx:latest파일시스템 스캔
bash
# 프로젝트 디렉토리 스캔
trivy fs --scanners vuln,secret,config .
# Dockerfile 스캔
trivy config Dockerfile
# Kubernetes 매니페스트 스캔
trivy config ./k8s/
# Helm 차트 스캔
trivy config ./charts/my-app/Trivy 설정 파일
yaml
# trivy.yaml
severity:
- HIGH
- CRITICAL
scan:
scanners:
- vuln
- secret
- config
vulnerability:
ignore-unfixed: true
type:
- os
- library
secret:
config: /path/to/secret-config.yaml
misconfiguration:
helm:
values:
- values.yaml
ignore:
- CVE-2023-12345 # 특정 CVE 무시
- secret-rule-id # 특정 시크릿 규칙 무시Trivy Operator (Kubernetes 통합)
bash
# Trivy Operator 설치
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update
helm install trivy-operator aqua/trivy-operator \
--namespace trivy-system \
--create-namespace \
--set trivy.ignoreUnfixed=trueyaml
# VulnerabilityReport 확인
apiVersion: aquasecurity.github.io/v1alpha1
kind: VulnerabilityReport
metadata:
name: deployment-nginx-nginx
namespace: default
spec:
# Trivy Operator가 자동 생성
report:
scanner:
name: Trivy
version: 0.56.0
summary:
criticalCount: 2
highCount: 5
mediumCount: 10
lowCount: 15
vulnerabilities:
- vulnerabilityID: CVE-2024-12345
severity: CRITICAL
title: "Buffer overflow in libssl"
installedVersion: "1.1.1"
fixedVersion: "1.1.2"Amazon ECR 이미지 스캐닝
기본 스캐닝 vs 향상된 스캐닝
| 기능 | 기본 스캐닝 | 향상된 스캐닝 (Inspector) |
|---|---|---|
| 스캔 엔진 | Clair | Amazon Inspector |
| 스캔 빈도 | 푸시 시 | 지속적 스캔 |
| 취약점 DB | CVE | CVE + Amazon 위협 인텔리전스 |
| 비용 | 무료 | Inspector 요금 |
| 언어 패키지 | 제한적 | 광범위 지원 |
| AWS 통합 | 기본 | Security Hub, EventBridge |
향상된 스캐닝 활성화
bash
# 레지스트리 스캐닝 설정
aws ecr put-registry-scanning-configuration \
--scan-type ENHANCED \
--rules '[
{
"repositoryFilters": [
{"filter": "production/*", "filterType": "WILDCARD"}
],
"scanFrequency": "CONTINUOUS_SCAN"
},
{
"repositoryFilters": [
{"filter": "development/*", "filterType": "WILDCARD"}
],
"scanFrequency": "SCAN_ON_PUSH"
}
]'스캔 결과 조회
bash
# 스캔 결과 확인
aws ecr describe-image-scan-findings \
--repository-name my-app \
--image-id imageTag=latest \
--query 'imageScanFindings.findings[?severity==`CRITICAL`]'
# 취약점 요약
aws ecr describe-image-scan-findings \
--repository-name my-app \
--image-id imageTag=latest \
--query 'imageScanFindings.findingSeverityCounts'EventBridge를 통한 알림
yaml
# CloudFormation/SAM 템플릿
Resources:
ECRScanRule:
Type: AWS::Events::Rule
Properties:
Name: ecr-scan-findings
EventPattern:
source:
- aws.ecr
detail-type:
- ECR Image Scan
detail:
scan-status:
- COMPLETE
finding-severity-counts:
CRITICAL:
- exists: true
Targets:
- Id: SNSTarget
Arn: !Ref AlertTopic
InputTransformer:
InputPathsMap:
repo: "$.detail.repository-name"
tag: "$.detail.image-tags[0]"
critical: "$.detail.finding-severity-counts.CRITICAL"
InputTemplate: |
"ECR 스캔 경고: 저장소 <repo>:<tag>에서 CRITICAL 취약점 <critical>개 발견"Cosign/Sigstore를 사용한 이미지 서명
Cosign 개요
Cosign은 Sigstore 프로젝트의 일부로, 컨테이너 이미지에 서명하고 검증하는 도구입니다.
┌─────────────────────────────────────────────────────────────────────────┐
│ Cosign 이미지 서명 워크플로우 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Build │────▶│ Sign │────▶│ Push to Registry │ │
│ │ Image │ │ (Cosign) │ │ (Image + Signature) │ │
│ └─────────────┘ └─────────────┘ └───────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Container Registry │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Image │ │ Signature │ │ │
│ │ │ sha256:abc... │ │ sha256:xyz... │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Kubernetes Cluster │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Admission │────▶│ Verify │────▶│ Deploy │ │ │
│ │ │ Controller │ │ Signature │ │ Pod │ │ │
│ │ │ (Kyverno) │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘Cosign 설치
bash
# macOS
brew install cosign
# Linux
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# 버전 확인
cosign version키 기반 서명
bash
# 키 쌍 생성
cosign generate-key-pair
# 이미지 서명
cosign sign --key cosign.key myregistry.io/myapp:v1.0.0
# 서명 검증
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0키리스 서명 (OIDC 기반)
bash
# GitHub Actions에서 키리스 서명
# GITHUB_TOKEN이 자동으로 OIDC 토큰 제공
cosign sign myregistry.io/myapp:v1.0.0
# 키리스 서명 검증
cosign verify \
--certificate-identity=https://github.com/myorg/myrepo/.github/workflows/build.yaml@refs/heads/main \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
myregistry.io/myapp:v1.0.0GitHub Actions 통합
yaml
name: Build, Sign and Push
on:
push:
branches: [main]
jobs:
build-sign-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # 키리스 서명에 필요
steps:
- uses: actions/checkout@v4
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
id: build
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Sign Image (Keyless)
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes ghcr.io/${{ github.repository }}@${DIGEST}
- name: Verify Signature
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}/*" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
ghcr.io/${{ github.repository }}:${{ github.sha }}Admission Control에서 이미지 검증
Kyverno imageVerify
yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/myorg/*/.github/workflows/*@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
---
# 키 기반 검증
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-key
spec:
validationFailureAction: Enforce
rules:
- name: verify-key-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.io/*"
attestors:
- count: 1
entries:
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----Connaisseur
yaml
# Connaisseur 설치
helm repo add connaisseur https://sse-secure-systems.github.io/connaisseur/charts
helm install connaisseur connaisseur/connaisseur \
-n connaisseur \
--create-namespaceyaml
# Connaisseur 설정
# values.yaml
validators:
- name: cosign
type: cosign
trustRoots:
- name: production
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
policy:
- pattern: "ghcr.io/myorg/*"
validator: cosign
with:
trustRoot: production
- pattern: "*"
validator: deny # 기타 이미지 거부공급망 보안
SBOM (Software Bill of Materials) 생성
bash
# Syft로 SBOM 생성
syft myregistry.io/myapp:latest -o spdx-json > sbom.spdx.json
syft myregistry.io/myapp:latest -o cyclonedx-json > sbom.cyclonedx.json
# Trivy로 SBOM 생성
trivy image --format spdx-json --output sbom.json myregistry.io/myapp:latest
# SBOM을 이미지에 첨부 (Cosign)
cosign attach sbom --sbom sbom.spdx.json myregistry.io/myapp:latestSBOM 기반 취약점 검사
bash
# SBOM에서 취약점 스캔
trivy sbom sbom.spdx.json
# Grype로 SBOM 스캔
grype sbom:sbom.spdx.jsonSLSA (Supply chain Levels for Software Artifacts)
yaml
# GitHub Actions에서 SLSA Provenance 생성
name: SLSA Build
on:
push:
branches: [main]
jobs:
build:
outputs:
digest: ${{ steps.build.outputs.digest }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and Push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
provenance:
needs: build
permissions:
actions: read
id-token: write
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.build.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}기본 이미지 선택
이미지 유형 비교
| 이미지 유형 | 크기 | 취약점 | 디버깅 | 사용 사례 |
|---|---|---|---|---|
| distroless | 매우 작음 | 매우 적음 | 어려움 | 프로덕션 |
| Alpine | 작음 (~5MB) | 적음 | 가능 | 경량 앱 |
| Chainguard | 작음 | 매우 적음 | 제한적 | 보안 중심 |
| Ubuntu/Debian | 큼 | 많음 | 쉬움 | 개발/레거시 |
| Scratch | 최소 | 없음 | 불가 | 정적 바이너리 |
Distroless 이미지 사용
dockerfile
# 멀티 스테이지 빌드로 distroless 사용
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server
# Distroless 런타임 이미지
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]Chainguard 이미지 사용
dockerfile
# Chainguard Python 이미지
FROM cgr.dev/chainguard/python:latest
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER nonroot
CMD ["python", "app.py"]Alpine 보안 강화
dockerfile
FROM alpine:3.19
# 최소 패키지만 설치
RUN apk add --no-cache \
python3 \
py3-pip \
&& rm -rf /var/cache/apk/*
# 비루트 사용자 생성
RUN addgroup -g 1000 app && \
adduser -u 1000 -G app -s /bin/sh -D app
WORKDIR /app
COPY --chown=app:app . .
USER app
CMD ["python3", "app.py"]이미지 레지스트리 모범 사례
프라이빗 레지스트리 사용
yaml
# ImagePullSecrets 설정
apiVersion: v1
kind: Secret
metadata:
name: registry-credentials
namespace: production
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64-encoded-docker-config>
---
# ServiceAccount에 기본 ImagePullSecret 설정
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: production
imagePullSecrets:
- name: registry-credentials이미지 풀 정책
yaml
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
containers:
- name: app
image: myregistry.io/myapp@sha256:abc123... # 다이제스트 사용
imagePullPolicy: Always # 항상 최신 검증불변 태그 정책 (Kyverno)
yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
spec:
validationFailureAction: Enforce
rules:
- name: require-digest
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must use digest instead of tags"
pattern:
spec:
containers:
- image: "*@sha256:*"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: Enforce
rules:
- name: disallow-latest
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Using 'latest' tag is not allowed"
pattern:
spec:
containers:
- image: "!*:latest"CI/CD 파이프라인 통합
완전한 이미지 보안 파이프라인
yaml
# .github/workflows/secure-build.yaml
name: Secure Image Build
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 1. 코드 스캔
code-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner (fs mode)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
exit-code: '1'
# 2. 빌드 및 스캔
build-scan:
needs: code-scan
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image for scanning
uses: docker/build-push-action@v5
with:
context: .
load: true
tags: ${{ env.IMAGE_NAME }}:scan
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.IMAGE_NAME }}:scan'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Check for critical vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.IMAGE_NAME }}:scan'
exit-code: '1'
severity: 'CRITICAL'
# 3. 푸시 및 서명
push-sign:
needs: build-scan
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Sign image with Cosign
run: |
cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }}
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- name: Attach SBOM to image
run: |
cosign attach sbom --sbom sbom.spdx.json ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }}
# 4. SLSA Provenance 생성
provenance:
needs: push-sign
permissions:
actions: read
id-token: write
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.push-sign.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}요약
컨테이너 이미지 보안의 핵심:
- 스캐닝: Trivy, ECR 향상된 스캔으로 취약점 탐지
- 서명: Cosign/Sigstore로 이미지 무결성 보장
- 검증: Kyverno로 서명된 이미지만 배포 허용
- 공급망: SBOM, SLSA로 투명성 확보
- 기본 이미지: distroless, Chainguard 사용
- CI/CD 통합: 자동화된 보안 파이프라인 구축
권장 사항
- 모든 이미지에 취약점 스캔 적용
- 프로덕션 이미지는 반드시 서명
latest태그 대신 다이제스트 사용- distroless 또는 최소 기본 이미지 사용
- SBOM 생성 및 관리 자동화