왜 아티팩트 서명이 필요한가
컨테이너 이미지나 배포 아티팩트는 레지스트리에서 태그로 참조된다. 문제는 태그가 언제든 다른 다이제스트로 덮어써질 수 있고, 중간에 누군가 악성 레이어를 밀어넣어도 소비하는 쪽에서 알아채기 어렵다는 점이다. "우리 CI가 빌드한 그 이미지가 맞다"를 증명할 방법이 없으면 공급망 공격의 표적이 된다.
서명은 이 문제를 "출처(provenance)와 무결성"의 문제로 바꾼다. 빌드 파이프라인이 아티팩트에 서명하고, 배포 단계에서 서명을 검증하면 신뢰할 수 있는 주체가 만든 아티팩트만 클러스터에 들어온다. cosign은 이 과정을 컨테이너 레지스트리 위에서 표준적으로 처리하는 도구다.
키 기반 서명 vs Keyless
cosign은 두 가지 방식을 제공한다. 장기 키를 관리하기 부담스럽다면 OIDC 신원과 짧은 수명 인증서를 쓰는 keyless 방식이 운영상 유리하다.
| 구분 | 키 기반 | Keyless (OIDC) |
|---|---|---|
| 키 관리 | KMS/파일로 직접 관리, 유출·회전 부담 | 단명 인증서, 관리 대상 없음 |
| 신뢰 근거 | 공개키 보유 여부 | OIDC 신원 + 투명성 로그(Rekor) |
| 적합한 상황 | 폐쇄망, 외부 의존 최소화 | GitHub Actions 등 CI 표준 환경 |
기본 서명과 검증
KMS(예: AWS KMS)에 키를 두고 서명하는 흐름은 다음과 같다. 반드시 태그가 아니라 다이제스트로 서명·검증해야 태그 교체 공격을 막을 수 있다.
#!/usr/bin/env bash
set -euo pipefail
IMAGE="registry.example.com/app"
DIGEST=$(crane digest "${IMAGE}:latest")
REF="${IMAGE}@${DIGEST}"
# KMS 키로 서명 (서명은 레지스트리에 함께 저장됨)
cosign sign --yes \
--key awskms:///alias/artifact-signing \
"${REF}"
# 검증: 공개키로 무결성/출처 확인
cosign verify \
--key awskms:///alias/artifact-signing \
"${REF}"
CI에서 Keyless 서명 자동화
GitHub Actions에서는 OIDC 토큰을 발급받아 Fulcio가 단명 인증서를 발급하고, 서명 사실은 Rekor 투명성 로그에 기록된다. 별도 시크릿 키를 저장하지 않아도 된다.
name: build-and-sign
on:
push:
tags: ["v*"]
permissions:
contents: read
packages: write
id-token: write # OIDC 발급에 필수
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Build & push
id: push
run: |
IMAGE=ghcr.io/${{ github.repository }}
docker build -t "$IMAGE:${{ github.ref_name }}" .
docker push "$IMAGE:${{ github.ref_name }}"
DIGEST=$(cosign triangulate --type digest "$IMAGE:${{ github.ref_name }}" || \
crane digest "$IMAGE:${{ github.ref_name }}")
echo "ref=$IMAGE@$DIGEST" >> "$GITHUB_OUTPUT"
- name: Sign (keyless)
env:
COSIGN_EXPERIMENTAL: "1"
run: cosign sign --yes "${{ steps.push.outputs.ref }}"
검증 시 신원 고정
keyless 검증에서는 "누가 서명했는가"를 반드시 고정해야 한다. 신원과 발급자를 명시하지 않으면 아무 유효 서명이나 통과하므로 의미가 없다.
cosign verify \
--certificate-identity-regexp \
"^https://github.com/myorg/app/.github/workflows/.+@refs/tags/v" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"ghcr.io/myorg/app@sha256:..."
배포 단계에서 정책으로 강제
서명만 하고 검증을 강제하지 않으면 효과가 없다. 쿠버네티스에서는 Kyverno나 sigstore policy-controller로 어드미션 단계에서 미서명 이미지를 차단한다. 아래는 Kyverno 예시로, 지정한 CI 신원의 서명이 없는 이미지의 파드 생성을 거부한다.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
subjectRegExp: "^https://github.com/myorg/.+"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: "https://rekor.sigstore.dev"
SBOM·프로버넌스 어테스테이션
서명은 "무결성"만 보장한다. "이 이미지가 어떤 소스와 빌드 과정에서 나왔는가"까지 검증하려면 어테스테이션을 붙인다. SBOM을 생성해 이미지에 서명된 형태로 첨부하면, 취약점 대응 시 어떤 컴포넌트가 포함됐는지 추적할 수 있다.
# SBOM 생성 후 어테스테이션으로 첨부
syft "$REF" -o cyclonedx-json > sbom.json
cosign attest --yes --predicate sbom.json \
--type cyclonedx "$REF"
# 검증
cosign verify-attestation --type cyclonedx \
--certificate-identity-regexp "^https://github.com/myorg/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$REF"
운영 시 주의점
- 태그가 아니라 다이제스트로 검증하라. 태그는 가변이므로 서명과 실제 실행 이미지가 어긋날 수 있다.
- 신원 고정은 필수다.
--certificate-identity와--certificate-oidc-issuer없이 하는 keyless 검증은 사실상 무방비다. 정규식은 브랜치/태그 조건까지 좁혀라. - 폐쇄망은 Rekor·Fulcio를 자체 호스팅하거나 키 기반으로 전환한다. 공용 sigstore 인프라 접근이 막히면 검증이 실패한다.
- 키 회전 계획을 세워라. KMS 키를 쓰면 회전 시 과거 서명 검증을 위해 이전 공개키를 함께 신뢰 목록에 유지해야 한다.
- 검증 실패를 관측 가능하게 만들어라. 어드미션 거부 이벤트를 로그·알림으로 연결하지 않으면 배포가 조용히 막힌다.