Cosign vs Notation, 우리 팀은 어떻게 골랐나
작년 말부터 컨테이너 이미지 서명을 본격적으로 강제하기 시작했다. 사내 정책상 프로덕션에 들어가는 모든 이미지는 서명되어 있어야 하고, admission controller에서 검증에 실패하면 배포가 막힌다. 그때 첫 번째로 부딪힌 질문이 "그래서 Cosign 쓸 거야, Notation 쓸 거야?" 였다.
둘 다 OCI 아티팩트로 서명을 저장하고, 둘 다 표준화된 사양을 따른다. 그런데 막상 PoC 들어가니까 결이 꽤 달랐다. 5개월 정도 양쪽 다 운영해 본 입장에서 정리해본다.
키 관리 모델이 가장 큰 분기점
Cosign의 강점은 누가 뭐래도 keyless 서명이다. GitHub Actions의 OIDC 토큰을 Fulcio가 받아서 짧은 수명(보통 10분)의 X.509 인증서를 발급하고, 그걸로 서명한 뒤 Rekor 투명성 로그에 기록한다. 키 파일이 어디에도 저장되지 않는다.
# .github/workflows/build.yml
permissions:
id-token: write # OIDC 토큰 받기
contents: read
packages: write
jobs:
build:
steps:
- uses: sigstore/cosign-installer@v3
- name: Sign image
run: |
cosign sign --yes \
ghcr.io/myorg/app@${{ steps.build.outputs.digest }}
--key 플래그가 없으면 자동으로 keyless 모드로 동작한다. 처음 PoC 돌릴 때 정말 이렇게 키 하나 없이 되는 게 맞나 싶었는데, 검증 쪽 보면 신뢰 모델이 명확해진다. 검증할 때는 --certificate-identity와 --certificate-oidc-issuer로 "이 이미지는 우리 조직의 main 브랜치 워크플로에서만 서명될 수 있다"를 강제한다.
cosign verify \
--certificate-identity-regexp "https://github.com/myorg/.+/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/myorg/app:v1.2.3
Notation은 정반대다. X.509 인증서 + 키를 직접 관리해야 한다. 대신 AWS Signer, Azure Key Vault, HashiCorp Vault 같은 KMS/HSM 플러그인이 잘 만들어져 있다. 우리 조직에 이미 KMS 운영 노하우가 있다면 Notation 쪽이 자연스럽게 흘러간다.
# Notation으로 AWS Signer 프로필 등록
notation key add --plugin com.amazonaws.signer.notation.plugin \
--id "arn:aws:signer:ap-northeast-2:111122223333:/signing-profiles/my_profile" \
awssigner
notation sign --signature-format cose \
--key awssigner \
ghcr.io/myorg/app@sha256:abcd...
trustpolicy.json으로 어떤 인증서를 신뢰할지 명시적으로 정의해야 하는 것도 Notation답다. 좀 번거롭지만, 감사 요구사항이 빡센 환경에서는 오히려 이게 더 설명하기 좋다. "이 인증서를 발급한 CA는 우리 회사 PKI에 있고, 만료일은 언제, 폐기 메커니즘은 무엇입니다"가 한 줄로 정리된다.
검증 단계의 운영 부담
이게 내가 가장 늦게 깨달은 부분인데, 서명보다 검증 쪽이 훨씬 까다롭다. 빌드 파이프라인에서 서명 한 번 하는 건 어렵지 않다. 문제는 admission controller에서 매 Pod 생성 때마다 검증을 도는 거다.
Cosign keyless 검증은 Rekor에 매번 접근한다. Rekor가 만약 일시적으로 응답이 늦거나 다운되면? 우리는 작년에 한 번 호되게 당했다. Sigstore 퍼블릭 인스턴스가 30분 정도 불안정했는데, 그 시간에 신규 배포가 전부 막혔다. 다행히 기존 Pod는 영향 없었지만, 새로 띄우는 게 안 되니까 HPA 스케일아웃이 안 먹혔다.
그 사건 이후로는 다음 셋 중 하나는 반드시 해야 한다는 결론을 내렸다.
--tlog-upload=false+ 번들 형식으로 Rekor 증명을 이미지에 같이 박아두기 (오프라인 검증)- 사내 private Sigstore 인스턴스 운영 (Fulcio, Rekor, CT log 직접 운영)
- 서명만 keyless, 검증은 캐시된 인증서 체인 사용
Notation은 처음부터 오프라인 검증이 기본이다. KMS 호출은 서명할 때만 하고, 검증은 trustpolicy + 인증서 체인만 있으면 된다. 운영 관점에서는 이게 훨씬 마음이 편하다. 다만 인증서 폐기(CRL/OCSP) 처리를 어떻게 할지는 따로 고민해야 한다.
// trustpolicy.json - Notation 검증 정책
{
"version": "1.0",
"trustPolicies": [
{
"name": "prod-policy",
"registryScopes": ["ghcr.io/myorg/*"],
"signatureVerification": {
"level": "strict",
"override": {
"expiry": "log",
"revocation": "enforced"
}
},
"trustStores": ["ca:myorg-pki"],
"trustedIdentities": [
"x509.subject:CN=Build Signing,O=MyOrg,C=KR"
]
}
]
}
생태계와 도구 통합
Cosign은 SBOM과 attestation 쪽으로 크게 뻗어 있다. cosign attest로 in-toto SLSA provenance를 붙이고, Syft로 만든 SBOM도 같이 서명해서 OCI에 같이 저장한다. 그리고 이걸 Kyverno나 Connaisseur가 모두 검증할 수 있다. 한 번 셋업하면 "이 이미지는 어디서 빌드됐고, 어떤 의존성을 갖고 있고, 누가 서명했나"가 다 검증된다.
# SBOM 첨부 후 서명
syft ghcr.io/myorg/app:v1.2.3 -o spdx-json > sbom.json
cosign attest --predicate sbom.json --type spdxjson \
ghcr.io/myorg/app@${DIGEST}
# 검증 시점에 SLSA provenance 확인
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "..." \
ghcr.io/myorg/app:v1.2.3
Notation은 이 부분이 아직 약하다. OCI 1.1의 referrers API를 활용해서 attestation을 붙일 수 있지만, 도구 체인이 통합돼서 굴러가는 느낌은 아니다. 작년 KubeCon에서 Notation v1.2가 나오면서 referrers 기반 verification이 개선됐다곤 하는데, 우리 팀이 평가해봤을 땐 아직 Cosign 쪽이 매끄러웠다.
Admission controller도 분기점이다. Kyverno는 둘 다 지원하지만 Cosign 쪽 문서와 예제가 압도적으로 많다. Notation은 ratify라는 별도 프로젝트로 따로 가는 분위기다. Docker가 Content Trust를 deprecate한 이후로 Notation v2로 정착했는데, Azure 진영(ACR + Azure Key Vault + ratify)에서는 굉장히 매끄럽게 묶인다. 그 외 환경에서는 Cosign 생태계가 더 두텁다.
결국 우리는 이렇게 갈랐다
5개월 굴린 결론은 이거다. 우리 팀은 개발자가 직접 빌드하는 애플리케이션 이미지는 Cosign keyless, 외부에서 받아와서 재배포하는 base 이미지나 vendor 이미지는 Notation + KMS 로 분리했다.
이유는 명확하다. 사내 애플리케이션은 GitHub Actions에서 빌드되니까 OIDC 토큰이 강력한 ID 증명이 된다. 키 로테이션 신경 쓸 일 없고, 검증 시점에 "이 이미지는 main 브랜치에서만 나왔다"가 보장된다. 반대로 외부 base 이미지(예: 사내 표준 Python 베이스)는 한 번 서명하면 6개월~1년 쓰니까, OIDC 모델이 적합하지 않다. 이건 보안팀이 관리하는 KMS 키로 서명하고, 인증서 만료/폐기를 명시적으로 관리하는 게 맞다.
물론 이게 정답은 아니다. 두 가지 도구를 운영한다는 건 admission controller에 두 가지 정책을 박아야 한다는 뜻이고, 디버깅 포인트도 두 배가 된다. 한 가지로 통일하고 싶으면 둘 중 하나로 가는 게 맞고, 그땐 조직 성격을 봐야 한다. 클라우드 네이티브 + GitHub 중심이면 Cosign, 엔터프라이즈 PKI가 이미 잘 굴러가고 있으면 Notation.
혹시 두 도구를 한 환경에서 같이 쓰는 팀이 있다면, 어떻게 분리해서 운영하시는지 댓글로 알려주시면 좋겠다. 우리도 아직 검증 단계 캐싱 전략을 더 다듬는 중이다.