작년 말부터 컨테이너 이미지 서명을 본격적으로 강제하기 시작했다. 사내 정책상 프로덕션에 들어가는 모든 이미지는 서명되어 있어야 하고, 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.
혹시 두 도구를 한 환경에서 같이 쓰는 팀이 있다면, 어떻게 분리해서 운영하시는지 댓글로 알려주시면 좋겠다. 우리도 아직 검증 단계 캐싱 전략을 더 다듬는 중이다.
'IT > DevSecOps' 카테고리의 다른 글
| Tetragon vs Falco, 런타임 보안 뭘 쓸까 (0) | 2026.05.21 |
|---|---|
| OpenBao 내부 들여다보기 - 포크 이후 2년, 진짜로 Vault를 대체할 수 있나 (0) | 2026.05.20 |
| Falco Operator로 K8s 런타임 보안 이벤트 잡기 가이드 (0) | 2026.05.12 |
| External Secrets Operator vs SOPS, 1년 같이 써본 후기 (0) | 2026.05.12 |
| Cosign + Kyverno로 컨테이너 이미지 서명 검증, 클러스터에 강제하기 (0) | 2026.05.07 |