Certified Kubernetes Security Specialist (CKS) #15 이미지 서명: cosign, SBOM
#14 Image scan: Trivy, Kubesec, KubeLinter에서 이미지 안에 어떤 취약점이 들어 있는지 스캔으로 들여다봤습니다. 그런데 스캔은 “이 이미지가 안전한가"를 보는 일이고, 그에 앞서 더 근본적인 질문이 있습니다. **“이 이미지가 정말 우리가 만든 그 이미지가 맞는가”**입니다. 레지스트리에서 끌어온 myapp:1.0이 우리 CI가 빌드한 것인지, 아니면 공격자가 같은 태그로 밀어넣은 다른 이미지인지를 태그만으로는 알 수 없습니다. 태그는 언제든 덮어쓸 수 있기 때문입니다.
이 출처 불확실성을 해소하는 것이 이미지 서명입니다. 빌드 주체가 이미지에 서명을 남기고, 배포 시점에 그 서명을 검증하면 출처와 무결성을 함께 보장합니다. 이번 글은 sigstore의 cosign으로 서명과 검증을 다루고, **SBOM(Software Bill of Materials)**으로 이미지가 무엇으로 이루어졌는지를 문서화하는 두 축을 정리하겠습니다.
왜 서명인가: 신뢰의 출발점 #
컨테이너 이미지는 레지스트리에 올라가는 순간부터 누구나 받아 갈 수 있는 공개 자산입니다. 문제는 태그가 식별자이지 신뢰의 증거가 아니라는 점입니다. registry.example.com/myapp:1.0이라는 이름은 단지 레지스트리 안의 한 매니페스트를 가리키는 포인터일 뿐이고, 쓰기 권한을 가진 누군가가 같은 태그를 다른 이미지로 덮어쓰면 이름은 그대로인 채 내용물만 바뀝니다. 이것이 공급망 공격의 전형적인 진입로입니다.
이미지에는 변하지 않는 식별자가 하나 있습니다. **다이제스트(digest)**입니다. sha256:으로 시작하는 이 해시는 이미지 내용 전체에서 계산되므로, 내용이 1바이트라도 다르면 다이제스트가 달라집니다. 그래서 무결성 측면에서는 태그 대신 다이제스트로 이미지를 고정하는 것이 첫걸음입니다.
# 태그 대신 다이제스트로 이미지를 고정한다
spec:
containers:
- name: app
image: registry.example.com/myapp@sha256:3a1b...e9f0그런데 다이제스트는 무결성을 보장할 뿐 출처를 증명하지는 못합니다. “이 이미지가 빌드 이후 바뀌지 않았다"는 알 수 있지만, “이 이미지를 신뢰할 만한 주체가 만들었다"는 별개의 질문입니다. 출처까지 증명하려면 빌드 주체의 서명이 필요합니다. 여기서 cosign이 등장합니다.
cosign: sigstore의 서명 도구 #
sigstore는 소프트웨어 공급망에 서명과 검증을 손쉽게 도입하기 위한 오픈소스 프로젝트이고, cosign은 그 안에서 컨테이너 이미지에 서명하고 검증하는 명령줄 도구입니다. cosign의 핵심은 서명을 별도 인프라 없이 이미지가 사는 레지스트리에 함께 저장한다는 점입니다. 서명은 원본 이미지 다이제스트에 연결된 별도의 아티팩트로 올라갑니다.
cosign이 지원하는 서명 방식은 크게 두 가지입니다.
| 방식 | 키 관리 | 특징 |
|---|---|---|
| 키 기반(key pair) | 개인키,공개키 직접 보관 | 단순. 키 유출,로테이션을 직접 책임 |
| keyless(OIDC) | 키 보관 없음 | OIDC 신원으로 단기 인증서 발급. CI에 적합 |
시험에서는 두 방식 가운데 키 기반 서명,검증이 손에 익혀야 할 기본이고, keyless는 개념으로 이해해 두면 됩니다.
키 쌍 생성 #
먼저 서명에 쓸 키 쌍을 만듭니다. 비밀번호를 입력하면 그 비밀번호로 보호된 개인키 파일과 공개키 파일이 생성됩니다.
# cosign.key(개인키), cosign.pub(공개키)가 생성된다
cosign generate-key-paircosign.key는 서명에 쓰는 개인키입니다. 절대 외부에 노출하면 안 됩니다.cosign.pub는 검증에 쓰는 공개키입니다. 검증 주체에 배포해도 안전합니다.
운영 환경에서는 개인키를 파일로 두기보다 KMS(AWS KMS, GCP KMS 등)나 Kubernetes Secret에 보관하는 방식을 씁니다. cosign은 cosign generate-key-pair k8s://<namespace>/<secret> 형태로 키를 바로 Secret에 넣는 것도 지원합니다.
이미지에 서명 #
생성한 개인키로 이미지에 서명합니다. 대상 이미지는 가능하면 태그가 아니라 다이제스트로 지정하는 것이 안전합니다. 태그로 서명하면 cosign이 그 시점의 다이제스트를 풀어 서명하지만, 명시적으로 다이제스트를 주면 의도가 분명해집니다.
# 개인키로 이미지에 서명한다
cosign sign --key cosign.key registry.example.com/myapp:1.0서명이 끝나면 레지스트리에는 원본 이미지 옆에 sha256-....sig라는 이름의 서명 아티팩트가 함께 저장됩니다. 서명 자체가 레지스트리에 사니, 별도의 서명 저장소를 운영할 필요가 없습니다.
서명 검증 #
배포 측에서는 공개키로 서명을 검증합니다. 검증이 성공하면 해당 이미지가 그 개인키의 보유자에 의해 서명되었고 이후 변경되지 않았음을 확인할 수 있습니다.
# 공개키로 이미지 서명을 검증한다
cosign verify --key cosign.pub registry.example.com/myapp:1.0검증에 성공하면 cosign은 서명의 페이로드를 표준 출력으로 보여 줍니다. 검증에 실패하면, 즉 서명이 없거나 다른 키로 서명되었거나 이미지가 변조되었으면 0이 아닌 종료 코드와 함께 에러를 냅니다. 이 종료 코드가 뒤에서 다룰 자동화의 출입문이 됩니다.
# 종료 코드로 검증 성공 여부를 판단한다
cosign verify --key cosign.pub registry.example.com/myapp:1.0 \
&& echo "서명 검증 통과" \
|| echo "서명 검증 실패: 배포 차단"keyless 서명(OIDC) 한 줄 #
키 기반 방식은 개인키를 안전하게 보관하고 주기적으로 로테이션해야 하는 부담이 있습니다. keyless 서명은 이 부담을 없앱니다. 서명 시점에 OIDC 신원(예: GitHub Actions의 워크플로 신원, 사용자의 구글,깃허브 계정)으로 인증하면 sigstore의 인증 기관(Fulcio)이 그 신원에 묶인 단기 인증서를 발급하고, 서명 기록은 투명성 로그(Rekor)에 남습니다. 개인키를 보관하지 않으므로 CI 파이프라인에서 특히 적합합니다.
# OIDC 신원으로 서명한다(브라우저 또는 CI 토큰으로 인증)
cosign sign registry.example.com/myapp:1.0# keyless 검증은 어떤 신원,발급자를 신뢰할지 명시한다
cosign verify \
--certificate-identity "https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
registry.example.com/myapp:1.0keyless 검증에서는 공개키 대신 누가(certificate-identity) 어디서(certificate-oidc-issuer) 서명했는지를 신뢰 조건으로 지정합니다. 이렇게 하면 “우리 main 브랜치의 빌드 워크플로가 서명한 이미지만 통과"라는 정책을 키 관리 없이 세울 수 있습니다.
SBOM: 이미지를 이루는 부품 목록 #
서명이 “이 이미지를 누가 만들었는가"를 증명한다면, **SBOM(Software Bill of Materials)**은 “이 이미지가 무엇으로 이루어졌는가"를 문서화합니다. SBOM은 이미지에 들어간 OS 패키지, 언어 라이브러리, 버전, 라이선스 등 구성 요소의 전체 목록입니다. 제조업의 부품 명세서(BOM)를 소프트웨어에 그대로 옮긴 개념입니다.
SBOM이 중요한 이유는 취약점 추적과 직결되기 때문입니다. 새로운 치명적 취약점(예: 과거의 Log4Shell)이 공개되면, 가장 먼저 답해야 할 질문은 “우리 이미지 가운데 그 라이브러리를 쓰는 것이 무엇인가"입니다. SBOM이 미리 만들어져 있으면 이 질문에 즉시 답할 수 있습니다. SBOM 없이는 모든 이미지를 다시 스캔하며 찾아야 합니다.
SBOM 형식: SPDX와 CycloneDX #
SBOM은 사람이 읽는 자유 문서가 아니라 표준 형식을 따릅니다. 도구가 생성하고 다른 도구가 읽어 들이려면 형식이 정해져 있어야 하기 때문입니다. 대표적인 두 표준이 있습니다.
| 형식 | 주관 | 특징 |
|---|---|---|
| SPDX | Linux Foundation | 라이선스 컴플라이언스 중심에서 출발. ISO 표준 |
| CycloneDX | OWASP | 보안,취약점 추적 중심. 경량 |
둘 다 널리 쓰이며, 도구 대부분이 두 형식을 모두 출력합니다. 시험에서는 “어떤 형식으로 생성하라"는 옵션을 정확히 지정하는 것이 포인트입니다.
syft로 SBOM 생성 #
syft는 이미지나 파일 시스템에서 구성 요소를 추출해 SBOM을 만드는 도구입니다. 출력 형식을 옵션으로 골라 지정합니다.
# 이미지에서 SBOM을 생성한다(기본 출력)
syft registry.example.com/myapp:1.0# SPDX(JSON) 형식으로 파일에 저장한다
syft registry.example.com/myapp:1.0 -o spdx-json=sbom.spdx.json# CycloneDX(JSON) 형식으로 파일에 저장한다
syft registry.example.com/myapp:1.0 -o cyclonedx-json=sbom.cdx.json생성한 SBOM은 그 자체로 끝이 아니라 다음 단계의 입력이 됩니다. 취약점 스캐너 grype는 이미지를 직접 스캔하는 대신 SBOM을 입력으로 받아 취약점을 매칭할 수 있습니다.
# SBOM을 입력으로 취약점을 매칭한다
grype sbom:sbom.spdx.json#14에서 다룬 Trivy도 SBOM 생성과 SBOM 기반 스캔을 모두 지원하며, 흐름은 같습니다. 구성 요소 목록을 한 번 만들어 두고, 새 취약점이 나올 때마다 그 목록에 대조합니다.
SBOM과 서명을 함께: 첨부와 증명 #
SBOM은 이미지와 함께 다녀야 의미가 있습니다. cosign은 SBOM을 이미지에 **첨부(attach)**하거나, 출처를 증명하는 **attestation(증명)**으로 서명해 붙이는 것을 지원합니다. attestation은 “이 이미지에 대한 이 SBOM이 진짜다"를 서명으로 보증하는 방식입니다.
# SBOM을 attestation으로 만들어 이미지에 서명,첨부한다
cosign attest --key cosign.key \
--predicate sbom.spdx.json \
--type spdxjson \
registry.example.com/myapp:1.0# 첨부된 SBOM attestation을 검증한다
cosign verify-attestation --key cosign.pub \
--type spdxjson \
registry.example.com/myapp:1.0이렇게 하면 이미지의 출처(서명)와 구성(SBOM)이 모두 검증 가능한 형태로 레지스트리에 함께 살게 됩니다. 공급망 보안의 목표가 바로 이 상태입니다.
admission으로 서명을 강제하기 #
지금까지의 명령은 사람이 손으로 검증하는 흐름이었습니다. 그러나 실제 클러스터에서 필요한 것은 미서명 이미지가 애초에 배포되지 못하게 막는 것입니다. 이를 위해서는 클러스터가 Pod를 받아들이는 길목, 즉 admission control에서 서명을 검증해야 합니다.
흐름은 이렇습니다. kubectl이 Pod 생성을 요청하면 API server가 admission webhook에 그 요청을 넘기고, webhook이 이미지 서명을 cosign 방식으로 검증한 뒤 통과 또는 거부를 결정합니다. 서명이 없거나 신뢰하지 않는 키로 서명된 이미지를 쓴 Pod는 생성 단계에서 막힙니다.
kubectl apply
│
▼
API server ──→ admission webhook(서명 검증)
│ 통과: Pod 생성 허용
└ 실패: Pod 생성 거부이 검증을 정책으로 표현하는 도구가 바로 다음 글의 주제인 OPA/Gatekeeper와 Kyverno입니다. 특히 Kyverno에는 verifyImages 규칙이 있어, “이 레지스트리의 이미지는 이 공개키로 서명되어 있어야 한다"는 정책을 선언적으로 작성할 수 있습니다. sigstore 진영의 policy-controller도 같은 역할을 하는 전용 admission controller입니다.
# Kyverno: 지정 레지스트리 이미지에 cosign 서명을 요구하는 정책의 골자
spec:
rules:
- name: verify-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "registry.example.com/*"
attestors:
- entries:
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
...cosign.pub 내용...
-----END PUBLIC KEY-----정책의 세부 문법과 OPA/Gatekeeper와의 비교는 #16 Admission control: OPA/Gatekeeper, Kyverno에서 자세히 다루겠습니다. 이번 글에서는 서명,SBOM이라는 재료를 만들었고, 그 재료를 admission이 강제한다는 연결고리를 기억하면 됩니다.
시험 포인트 #
CKS 시험의 Supply Chain Security도메인에서 이미지 서명과 SBOM은 다음을 손에 익혀 두는 것이 핵심입니다.
- 키 쌍 생성.
cosign generate-key-pair로cosign.key,cosign.pub를 만든다. 둘의 역할 구분을 헷갈리지 않는다. - 서명.
cosign sign --key cosign.key <이미지>. 가능하면 다이제스트로 대상을 고정한다. - 검증.
cosign verify --key cosign.pub <이미지>. 검증 실패 시 0이 아닌 종료 코드가 난다는 점을 안다. - keyless 개념. 개인키 없이 OIDC 신원으로 단기 인증서를 받아 서명,검증하며, 검증에서는
--certificate-identity와--certificate-oidc-issuer로 신뢰 조건을 지정한다. - SBOM 생성.
syft <이미지> -o spdx-json또는-o cyclonedx-json으로 형식을 명시한다. SPDX와 CycloneDX의 차이를 한 줄로 설명할 수 있어야 한다. - SBOM의 목적. 구성 요소 목록을 미리 만들어 새 취약점이 나올 때 영향 이미지를 즉시 찾는 추적 수단이다.
- 다이제스트 고정. 태그는 변조 가능하므로 무결성이 중요한 곳은
@sha256:다이제스트로 이미지를 지정한다.
시험에서는 “이 이미지에 주어진 키로 서명하라”, “이 공개키로 서명을 검증하고 결과를 파일에 남겨라”, “이 이미지의 SBOM을 SPDX 형식으로 생성하라” 같은 작업이 단골입니다. 명령 한 줄을 정확한 옵션과 함께 칠 수 있으면 빠르게 점수가 됩니다.
정리 #
이번 글에서 잡은 것:
- 태그는 신뢰의 증거가 아니다. 무결성은 다이제스트로 고정하고, 출처는 서명으로 증명한다.
- cosign. sigstore의 서명 도구.
generate-key-pair로 키를 만들고,sign으로 서명하고,verify로 검증한다. 서명은 이미지가 사는 레지스트리에 함께 저장된다. - keyless(OIDC). 개인키 보관 없이 신원 기반 단기 인증서로 서명한다. CI 파이프라인에 적합하다.
- SBOM. 이미지의 구성 요소 목록. syft로 SPDX,CycloneDX 형식으로 생성하며, 새 취약점이 나올 때 영향 범위를 즉시 추적하는 근거가 된다.
- admission 강제. 손 검증을 넘어, Kyverno,OPA/Gatekeeper,policy-controller로 미서명 이미지의 배포를 클러스터 입구에서 막는다.
서명과 SBOM은 공급망 보안의 재료입니다. 이 재료를 클러스터가 자동으로 강제하게 만드는 것이 마지막 한 걸음입니다.
다음: Admission control #
이번 글의 마지막에서 미뤄 둔 질문, “그래서 미서명 이미지를 어떻게 클러스터가 자동으로 거부하게 하는가"를 다음 글에서 정면으로 다룹니다.
#16 Admission control: OPA/Gatekeeper, Kyverno에서는 admission webhook이 동작하는 원리, OPA/Gatekeeper의 Rego 정책과 ConstraintTemplate,Constraint 구조, Kyverno의 선언적 정책과 verifyImages 규칙, 그리고 “특정 레지스트리,서명을 요구하라”, “privileged Pod를 거부하라” 같은 시험 단골 정책을 직접 작성해 보며 정리하겠습니다.