Cosign 컨테이너 이미지 서명 완벽 가이드 2026: Sigstore Keyless와 CI/CD 통합

Cosign v2.5로 컨테이너 이미지 서명을 자동화하는 실전 가이드. Sigstore Keyless 서명 원리부터 GitHub Actions·GitLab CI 통합, SBOM/SLSA Provenance 첨부, Kubernetes admission 검증 강제, 흔한 트러블슈팅까지 프로덕션 파이프라인에 바로 적용할 코드와 함께 정리했습니다.

Cosign Keyless 서명 CI/CD 가이드 2026

업데이트: 2026년 9월 13일

Cosign은 Sigstore 생태계의 컨테이너 이미지 서명 도구로, 2026년 현재 OCI 이미지·SBOM·SLSA Provenance에 대한 사실상의 표준 서명 방식이다. 특히 Keyless 서명(OIDC 기반)을 사용하면 장기 보관 키 없이 CI 러너의 워크로드 아이덴티티만으로 서명이 가능하고, 서명 로그는 Rekor 투명성 로그에 자동으로 기록된다. 이 글에서는 Cosign v2.5 기준으로 설치, Keyless 서명, GitHub Actions·GitLab CI 통합, SBOM/Attestations 첨부, Kubernetes admission 검증까지 파이프라인 관점에서 실전 스니펫과 함께 정리한다.

  • Cosign v2.5는 OCI 1.1 Referrers API를 기본 지원하며, 서명·SBOM·Attestation을 이미지와 함께 저장한다.
  • Keyless 서명은 Fulcio가 발급하는 단기 인증서(10분 유효)와 OIDC 토큰만 사용하므로 키 관리 부담이 없다.
  • GitHub Actions는 id-token: write 권한만 있으면 별도 시크릿 없이 Keyless 서명이 가능하다.
  • Kubernetes에서는 Sigstore policy-controller 또는 Kyverno v1.13 이상으로 서명되지 않은 이미지의 배포를 차단할 수 있다.
  • SLSA v1.1 Provenance는 cosign attest로 이미지에 첨부하고 cosign verify-attestation으로 검증한다.
  • Rekor 투명성 로그 SLA(99.5%)를 신뢰할 수 없는 폐쇄망은 --tlog-upload=false와 자체 KMS 조합을 고려한다.

컨테이너 이미지 서명이 지금 필수가 된 이유

내가 지난 3년간 파이프라인을 손대면서 가장 크게 체감한 변화는, 취약점 스캔만으로는 공급망 공격을 막을 수 없다는 사실이 실무자 상식이 됐다는 점이다. SolarWinds·3CX·XZ Utils 백도어 사건을 지나며 규제도 빠르게 따라왔다. 미국 EO 14028의 후속 지침인 CISA Secure Software Development Attestation Form은 연방 정부에 소프트웨어를 납품하는 벤더에게 서명된 아티팩트와 SLSA 수준을 요구한다. EU CRA(Cyber Resilience Act) 역시 2026년 12월 시행을 앞두고 SBOM과 무결성 증명을 사실상 강제한다.

이 규제 흐름과 무관하게, 이미지 서명은 "이 이미지가 우리 CI에서 나왔다"를 수학적으로 증명하는 유일한 방법이다. 레지스트리 계정이 탈취되어도, 태그가 재작성되어도, 서명이 없거나 우리 CI OIDC 아이덴티티로 서명되지 않았다면 배포를 거부하면 된다. Trivy 같은 도구로 컨테이너 이미지 취약점을 스캔하는 것이 "무엇이 들어있는가"를 확인하는 일이라면, Cosign 서명은 "누가 만들었고 변조되지 않았는가"를 증명한다. 두 축은 상호 보완이지 대체가 아니다.

# 서명되지 않은 이미지는 pull 자체를 감사 로그에 남기고 실패시키기
$ cosign verify \
    --certificate-identity-regexp='https://github\.com/myorg/.*' \
    --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
    ghcr.io/myorg/api:v1.4.2 \
  || { echo "SIGNATURE_MISSING" | logger -t supply-chain -p auth.warning; exit 1; }

Sigstore 아키텍처: Fulcio · Rekor · Cosign의 역할

Sigstore는 2024년 CNCF 졸업 프로젝트로 승격됐고, 2026년 기준 세 개의 핵심 컴포넌트로 구성된다. 이 구조를 이해해야 Keyless 서명이 왜 "키가 없는데도" 안전한지 감이 잡힌다.

Fulcio: 단기 X.509 인증서 발급 CA

Fulcio는 OIDC 토큰(예: GitHub Actions의 id-token, Google Cloud SA, Kubernetes SA JWT)을 받아 그 아이덴티티를 Subject Alternative Name에 새긴 X.509 인증서를 발급한다. 인증서의 유효기간은 10분. 즉, 서명 순간에만 유효한 ephemeral key pair가 생성되고, 서명이 끝나면 개인키는 메모리에서 폐기된다. 유출될 장기 자격증명 자체가 존재하지 않는 것이다.

Rekor: 서명 투명성 로그

Rekor는 Certificate Transparency와 유사한 append-only 머클 트리 로그다. 모든 서명 이벤트가 여기에 기록되며, 누구나 Sigstore Search에서 자신의 이미지에 대한 서명 이력을 감사할 수 있다. 공격자가 몰래 서명해서 어딘가에 배포하려 해도 그 사실이 공개 로그에 남는다는 것이 핵심 억제력이다.

Cosign: 클라이언트

Cosign은 OCI 레지스트리와 대화하며 서명·검증을 수행하는 CLI다. 서명 결과는 원본 이미지 옆에 sha256-<digest>.sig 태그(구식) 또는 OCI 1.1 Referrers API(신식)로 저장된다. Cosign v2.0부터 Referrers가 기본이고, v2.5부터는 --registry-referrers-mode=oci-1-1 플래그 없이도 지원 레지스트리는 자동 감지한다.

# CI에서 Sigstore 컴포넌트 헬스체크 (파이프라인 pre-flight 스니펫)
set -e
for endpoint in \
  https://fulcio.sigstore.dev/api/v2/configuration \
  https://rekor.sigstore.dev/api/v1/log \
  https://tuf-repo-cdn.sigstore.dev/targets/trusted_root.json; do
    curl -fsSL --max-time 5 -o /dev/null "$endpoint" \
      || { echo "sigstore endpoint down: $endpoint"; exit 42; }
done

Cosign v2.5 설치와 초기 검증

2026년 9월 현재 안정 버전은 v2.5.x이다. 배포판 패키지보다 GitHub Releases 바이너리를 고정 버전으로 받는 것을 권장한다. 배포판 저장소는 마이너 버전이 뒤처져 있어 최신 Cosign 릴리스 노트의 보안 픽스를 놓치기 쉽다.

# 재현 가능한 설치 (SHA256 고정)
COSIGN_VERSION="v2.5.3"
COSIGN_SHA256="a1b2c3...<릴리스 노트의 실제 해시로 교체>"
curl -sSL -o /usr/local/bin/cosign \
  "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64"
echo "${COSIGN_SHA256}  /usr/local/bin/cosign" | sha256sum -c -
chmod 0755 /usr/local/bin/cosign
cosign version --json | jq '{GitVersion, GoVersion, Platform}'

설치가 끝나면 신뢰 루트(Trusted Root)를 미리 초기화한다. Cosign은 TUF(The Update Framework)로 Fulcio·Rekor 공개키를 갱신하는데, 최초 실행 시 네트워크가 막혀 있으면 서명 검증이 알 수 없는 에러로 실패한다. 오프라인·에어갭 환경에서는 cosign initialize --mirror <내부 미러> --root <root.json>로 사설 TUF 미러를 지정한다.

# 컨테이너 이미지 빌드 파이프라인 pre-flight
cosign initialize \
  --mirror https://tuf-repo-cdn.sigstore.dev \
  --root https://tuf-repo-cdn.sigstore.dev/root.json

# 파일 무결성 서명(스크립트·바이너리 배포에도 재활용 가능)
cosign sign-blob --yes ./install.sh > install.sh.sig
cosign verify-blob \
  --certificate-identity="[email protected]" \
  --certificate-oidc-issuer="https://accounts.google.com" \
  --signature install.sh.sig ./install.sh

Keyless 서명 실전: OIDC 기반 무키 서명

Keyless 서명은 실무에서 압도적으로 권장되는 방식이다. 유출될 키가 없고, 감사 로그(Rekor)에 자동으로 기록되며, 검증 정책이 "이 OIDC 아이덴티티가 서명한 것만 통과"라는 인간이 이해 가능한 형태로 쓰인다. 로컬에서 처음 실험해 보려면 다음처럼 하면 된다. 브라우저가 열리며 GitHub/Google/Microsoft 로그인을 요청한다.

# 로컬에서 Keyless 서명 시험 (브라우저 OIDC 플로우)
IMAGE="ghcr.io/myorg/api@sha256:$(docker inspect ghcr.io/myorg/api:v1.4.2 \
  --format '{{index .RepoDigests 0}}' | cut -d@ -f2)"

cosign sign --yes "$IMAGE"

cosign verify \
  --certificate-identity="[email protected]" \
  --certificate-oidc-issuer="https://accounts.google.com" \
  "$IMAGE" | jq '.[0].optional'

여기서 반드시 digest 참조(@sha256:...)를 사용해야 한다. 태그로 서명하면 태그가 재작성될 때 "이 시점에 어떤 다이제스트를 서명했는지"가 흐려진다. 나는 파이프라인 전체를 태그 대신 다이제스트로 강제하는 헬퍼를 만들어 두고, set -e와 함께 쓴다.

키 기반 서명과 KMS 통합은 언제 쓰는가

Keyless가 기본이라고 해서 키 기반 서명이 사라진 것은 아니다. 폐쇄망(에어갭)이거나, 규제상 HSM 기반 서명이 강제되거나, Sigstore Public Good SLA에 의존할 수 없는 환경에서는 여전히 키 기반이 필요하다. Cosign은 로컬 키 파일 외에 AWS KMS·GCP KMS·Azure Key Vault·HashiCorp Vault·PKCS#11 HSM을 네이티브로 지원한다.

# AWS KMS를 서명 키로 사용 (키는 KMS 밖으로 나오지 않음)
export AWS_REGION=ap-northeast-2
KEY_URI="awskms:///alias/prod-cosign-signer"

cosign sign --key "$KEY_URI" --tlog-upload=false \
  ghcr.io/myorg/api@sha256:abcd...

# 검증 시에는 공개키 참조
cosign verify --key "$KEY_URI" --insecure-ignore-tlog \
  ghcr.io/myorg/api@sha256:abcd...

실무 팁: 키 기반 서명이라도 가능하면 Rekor에 업로드하라(--tlog-upload=true, 기본값). 나중에 "이 이미지가 언제 서명됐는가"를 감사할 수 있는 유일한 증거가 된다. 사내 폐쇄망이면 rekor-server를 내부에 세워 사설 투명성 로그를 운영하는 것이 정석이다.

GitHub Actions에서 이미지 자동 서명

내가 가장 자주 셋업하는 파이프라인이다. GitHub Actions는 id-token: write 권한을 주면 OIDC 토큰을 자동 발급하므로, 어떤 시크릿도 저장할 필요가 없다. 아래는 빌드→푸시→서명→SBOM 첨부까지 한 번에 처리하는 최소 러닝 예시다.

# .github/workflows/build-sign.yml
name: build-and-sign
on:
  push:
    tags: ['v*.*.*']

permissions:
  contents: read
  packages: write
  id-token: write   # Keyless 서명에 필수

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
      - uses: sigstore/[email protected]
        with:
          cosign-release: 'v2.5.3'
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push (digest 캡처)
        id: build
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
          provenance: mode=max
          sbom: true

      - name: Cosign sign (Keyless)
        env:
          IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
        run: cosign sign --yes "$IMAGE"

      - name: Attach SBOM as attestation
        env:
          IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
        run: |
          syft "$IMAGE" -o cyclonedx-json > sbom.cdx.json
          cosign attest --yes --predicate sbom.cdx.json \
            --type cyclonedx "$IMAGE"

검증 측에서는 --certificate-identity-regexp로 우리 organization의 워크플로우만 통과시키는 것이 핵심이다. 특정 저장소·특정 브랜치까지 좁힐수록 침해 반경이 줄어든다.

GitLab CI/CD 파이프라인에 Cosign 통합

GitLab은 v16.0부터 CI/CD OIDC ID 토큰을 정식 지원한다. 사내 GitLab이라면 gitlab.example.com이 자체 발급자가 되며, Sigstore Public Fulcio가 이를 신뢰하지 않으므로 사설 Fulcio를 세우거나 rekor-cli + KMS 조합으로 가야 한다. Public GitLab.com이면 아래 예시가 그대로 동작한다.

# .gitlab-ci.yml
sign-image:
  stage: sign
  image: alpine:3.20
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  variables:
    IMAGE: "$CI_REGISTRY_IMAGE@$IMAGE_DIGEST"
  before_script:
    - apk add --no-cache curl
    - curl -sSL -o /usr/local/bin/cosign \
        https://github.com/sigstore/cosign/releases/download/v2.5.3/cosign-linux-amd64
    - chmod +x /usr/local/bin/cosign
  script:
    - cosign sign --yes "$IMAGE"
    - cosign verify \
        --certificate-identity-regexp="^https://gitlab.com/mygroup/.*" \
        --certificate-oidc-issuer="https://gitlab.com" \
        "$IMAGE"
  rules:
    - if: $CI_COMMIT_TAG

파이프라인이 $IMAGE_DIGEST를 이전 build 단계에서 아티팩트로 넘겨받는 구조를 권장한다. 태그를 다시 resolving하면 레이스 컨디션이 생길 수 있다. 이건 실제 프로덕션에서 한 번 물려본 뒤로 무조건 지키는 규칙이다.

SBOM과 SLSA Provenance 첨부하기

Cosign은 서명뿐 아니라 attestation—즉 이미지에 대한 서명된 주장을 첨부할 수 있다. 대표적으로 SBOM(CycloneDX·SPDX)과 SLSA Provenance가 있다. 2026년 기준 SLSA v1.1 Provenance는 어떤 CI가 어떤 소스 커밋으로 어떤 빌더에서 이미지를 만들었는지를 in-toto 스펙으로 기술한다.

# SBOM 생성 → attestation으로 첨부
syft ghcr.io/myorg/api@sha256:abcd... -o cyclonedx-json > sbom.cdx.json
cosign attest --yes --type cyclonedx \
  --predicate sbom.cdx.json \
  ghcr.io/myorg/api@sha256:abcd...

# 검증 (특정 CI 아이덴티티의 SBOM만 신뢰)
cosign verify-attestation \
  --type cyclonedx \
  --certificate-identity-regexp='https://github\.com/myorg/.*/\.github/workflows/build-sign\.yml@.*' \
  --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
  ghcr.io/myorg/api@sha256:abcd... \
  | jq -r '.payload' | base64 -d | jq '.predicate.components | length'

Trivy로 이미지 취약점을 스캔한 결과를 attestation으로 붙여 두면, 배포 단계에서 "취약점 리포트가 서명되어 있고 크리티컬이 0인 경우에만 허용"이라는 정책도 쓸 수 있다. 스캐너의 오탐(false positive)까지 감안한다면, 나는 취약점 attestation은 통과 게이트가 아니라 감사 아티팩트로만 쓰는 편이 실무에서 덜 시끄러웠다.

Kubernetes에서 서명 검증 강제하기

서명은 배포 시점에 검증되지 않으면 아무 의미가 없다. Kubernetes에서는 두 가지 선택지가 있다.

Sigstore policy-controller

Sigstore 프로젝트가 유지하는 admission controller로, ClusterImagePolicy CRD로 정책을 정의한다. Cosign 서명 검증에 가장 최적화되어 있다.

# ClusterImagePolicy: 우리 GitHub org의 이미지만 통과
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: allow-myorg-signed
spec:
  images:
    - glob: "ghcr.io/myorg/**"
  authorities:
    - keyless:
        url: https://fulcio.sigstore.dev
        identities:
          - issuer: https://token.actions.githubusercontent.com
            subjectRegExp: '^https://github\.com/myorg/[^/]+/\.github/workflows/.*'
        ctlog:
          url: https://rekor.sigstore.dev

Kyverno v1.13

이미 Kyverno를 정책 엔진으로 쓰는 조직이라면 Kyverno의 verifyImages 규칙이 편하다. 2026년 릴리스인 v1.13은 SLSA Provenance 검증과 attestation-based 정책을 GA로 지원한다.

apiVersion: kyverno.io/v2
kind: ClusterPolicy
metadata:
  name: require-cosign-keyless
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-ghcr-signed
      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

런타임에도 이상 프로세스를 계속 감시하고 싶다면, eBPF 기반 Falco와 Tetragon 런타임 보안 조합을 함께 배치하는 것을 권장한다. 서명 검증은 진입 지점을, eBPF는 런타임의 뒷문을 지킨다.

서명 검증 실패 트러블슈팅과 운영 팁

실제 운영에서 마주친 흔한 실패 패턴을 정리했다. 대부분은 정책이 아니라 시간·네트워크·아이덴티티 정규식 문제였다.

1) "no matching signatures" — 대부분 identity 정규식이 틀림

Reusable workflow를 쓰면 SAN은 https://github.com/myorg/reusable/.github/workflows/build.yml@refs/tags/v2처럼 재사용된 워크플로우의 경로가 된다. 호출한 저장소가 아니다. 파이프라인을 마이그레이션한 뒤 검증이 깨지는 이유의 90%가 여기다.

2) 시간 왜곡

Fulcio 인증서 유효기간은 10분이다. 러너의 NTP가 5분 이상 어긋나면 검증이 실패한다. 컨테이너 이미지 alpine은 chrony가 없으므로 러너 호스트 시계에 의존한다. Kubernetes 노드는 timedatectl status로 정기 점검할 항목에 넣어라.

3) TUF root 업데이트 실패

error initializing TUF client가 나오면 ~/.sigstore/root를 지우고 cosign initialize를 다시 실행한다. 사내 미러를 쓰는 조직은 root.json 로테이션 시점(약 6개월)을 캘린더에 등록해 두는 것이 좋다.

4) 레지스트리별 Referrers 지원

ECR·GCR·GHCR·Harbor 2.10+는 OCI 1.1 Referrers를 지원한다. Docker Hub는 여전히 태그 기반 서명 저장(sha256-...sig 태그)만 지원하므로 검증 시 --experimental-oci11=false를 명시적으로 붙여야 한다.

# CI 최종 게이트 스니펫: 이미지 서명·SBOM 첨부·Provenance까지 한 번에 검증
#!/usr/bin/env bash
set -euo pipefail
IMAGE="${1:?usage: verify.sh <image@sha256:...>}"
ORG_REGEX='^https://github\.com/myorg/[^/]+/\.github/workflows/.*'
ISSUER='https://token.actions.githubusercontent.com'

cosign verify \
  --certificate-identity-regexp="$ORG_REGEX" \
  --certificate-oidc-issuer="$ISSUER" \
  "$IMAGE" >/dev/null

cosign verify-attestation --type cyclonedx \
  --certificate-identity-regexp="$ORG_REGEX" \
  --certificate-oidc-issuer="$ISSUER" \
  "$IMAGE" >/dev/null

cosign verify-attestation --type slsaprovenance1 \
  --certificate-identity-regexp="$ORG_REGEX" \
  --certificate-oidc-issuer="$ISSUER" \
  "$IMAGE" >/dev/null

echo "OK: $IMAGE has signature + SBOM + SLSA provenance"

자주 묻는 질문

Cosign은 무료로 사용할 수 있나요?

Cosign 자체는 Apache 2.0 라이선스의 오픈소스이며, Sigstore Public Good 인프라(Fulcio·Rekor)도 무료로 제공됩니다. 다만 SLA 없이 best-effort로 운영되므로, 프로덕션 배포 파이프라인이 검증 단계에서 Sigstore에 강하게 의존한다면 사설 인스턴스 운영이나 캐싱 전략을 함께 고려해야 합니다.

Keyless 서명과 키 기반 서명 중 어떤 것을 선택해야 하나요?

대부분의 조직은 Keyless가 정답입니다. 관리할 키가 없고 감사 로그가 자동으로 남기 때문입니다. 에어갭 환경, HSM 기반 서명이 규제상 필수인 금융권, 또는 Sigstore Public Good 인프라를 신뢰할 수 없는 경우에만 KMS/HSM 기반 키 서명을 선택하십시오.

Cosign으로 서명한 이미지를 Docker Hub에서도 사용할 수 있나요?

사용할 수 있습니다. 다만 Docker Hub는 아직 OCI 1.1 Referrers API를 완전히 지원하지 않아 서명이 sha256-<digest>.sig 형태의 별도 태그로 저장됩니다. 검증 명령은 동일하지만, 서명 태그가 사용자에게 노출되므로 태그 목록이 지저분해 보인다는 단점이 있습니다.

Sigstore Public Good 서비스가 다운되면 우리 배포가 멈추나요?

서명 단계는 Fulcio/Rekor가 필요하므로 CI가 실패합니다. 검증 단계는 Rekor 인증서 체인이 이미 캐시돼 있으면 오프라인으로도 가능합니다. 프로덕션 파이프라인은 SIGSTORE_ROOT_FILE을 사설 미러에 고정하고, 매우 중요한 배포는 KMS 기반 백업 서명 경로를 병행하는 것이 안전합니다.

Cosign 서명 검증이 갑자기 실패할 때 무엇을 먼저 확인해야 하나요?

순서대로 확인하십시오: (1) 시스템 시간이 정확한지 timedatectl로 확인, (2) TUF root 만료 여부를 ~/.sigstore/root/timestamp.json에서 확인, (3) --certificate-identity-regexp가 최근 워크플로우 경로 변경을 반영하는지 확인, (4) 이미지가 태그가 아닌 다이제스트로 참조되고 있는지 확인. 이 네 가지가 실전 원인의 95%입니다.

Raj Patel
저자 소개 Raj Patel

DevSecOps engineer who's gradually turning every CI pipeline he sees into a security-checking machine.