이미지 서명을 안 하는 팀은 이제 거의 없을 거다. 빌드 파이프라인에 cosign 한 줄 박는 건 어렵지도 않으니까. 문제는 검증 쪽이다. 누가 서명 안 된 이미지를 클러스터에 푸시해도 그냥 굴러간다. 결국 admission 단에서 막아야 하는데, 이걸 가장 깔끔하게 해주는 게 Kyverno의 ImageValidatingPolicy다.
올해 초 Kyverno에서 기존 ClusterPolicy의 verifyImages 규칙을 ImageValidatingPolicy(IVP) 라는 별도 타입으로 분리하면서 정책 작성이 좀 더 명시적으로 바뀌었다. 우리 팀에서도 4월 초에 ClusterPolicy 기반 검증을 IVP로 옮겼는데, 옮기면서 정리한 내용을 가이드 형태로 풀어본다.
사전 준비물
- Kubernetes 1.28+ (가능하면 1.31+, IVP 안정성 이슈가 좀 줄었다)
- Kyverno 1.13.x 이상
- cosign 2.4+ (bundle format 기본 활성)
- 이미지 레지스트리 — ECR, GHCR, Harbor 어느 쪽이든 OCI 1.1 지원하면 OK
cosign 2.x부터는 서명을 별도 .sig 태그가 아닌 OCI 1.1 referrers API로 첨부하는 방식이 기본이다. 레지스트리가 referrers를 지원 안 하면 fallback 모드로 동작하긴 하는데, 검증 시 --experimental 플래그를 안 줘도 되는 건 큰 차이다.
1단계: 빌드 파이프라인에서 keyless 서명
키 관리 안 하려고 keyless 쓰는 거라, GitHub Actions 기준으로 보면 이렇게 끝난다.
permissions:
id-token: write
packages: write
contents: read
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
id: build
with:
push: true
tags: ghcr.io/myorg/api:${{ github.sha }}
- name: Sign with cosign
env:
COSIGN_EXPERIMENTAL: "1"
run: |
cosign sign --yes \
ghcr.io/myorg/api@${{ steps.build.outputs.digest }}
id-token: write가 핵심이다. 이게 빠지면 keyless OIDC 토큰을 못 받아서 그대로 멈춘다. 처음 도입할 때 권한 범위 문제로 한 번 헤맸다.
서명 후 Rekor 투명성 로그에 기록이 남는데, cosign verify로 식별자(주체+이슈어)를 매칭하면 검증 끝이다. 그래서 검증 정책에서는 누가, 어디서 서명했는지를 명시해야 한다. 이게 keyless의 핵심 발상.
2단계: Kyverno 설치 시 verify-images 옵션 켜기
Helm 차트로 설치할 때 image verification은 admission webhook 별도 설정이 들어간다. 1.13부터는 IVP 컨트롤러가 기본 활성이긴 한데, 초기 설치 시엔 확인하는 게 좋다.
helm upgrade --install kyverno kyverno/kyverno \
-n kyverno --create-namespace \
--set features.imageVerificationCache.enabled=true \
--set features.imageVerificationCache.maxSize=1024 \
--set features.imageVerificationCache.ttlDurationInSeconds=3600
imageVerificationCache는 검증 결과를 캐시한다. 이걸 안 켜놓으면 모든 Pod 생성마다 Rekor 조회가 발생해서 admission latency가 올라간다. 우리 팀에선 캐시 켜고 P95가 800ms → 90ms로 떨어졌다.
3단계: ImageValidatingPolicy 작성
본론이다. ghcr.io/myorg/* 이미지에 대해 GitHub Actions에서 만든 keyless 서명을 강제하는 정책이다.
apiVersion: policies.kyverno.io/v1alpha1
kind: ImageValidatingPolicy
metadata:
name: verify-ghcr-keyless
spec:
evaluation:
background:
enabled: false
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
matchImageReferences:
- glob: "ghcr.io/myorg/*"
attestors:
- name: gha-keyless
cosign:
keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
subjectRegExp: "^https://github.com/myorg/.+/.github/workflows/.+@refs/heads/(main|release/.+)$"
validations:
- expression: >
images.containers.all(image,
verifyImageSignatures(image, [attestors.gha-keyless]) >= 1)
message: "이미지 {{ image }} 서명이 검증되지 않음 (signer/issuer 불일치 또는 미서명)"
subjectRegExp가 보안의 마지막 보루다. main 또는 release/* 브랜치 워크플로에서만 서명을 인정한다. 만약 누가 자기 PR 브랜치에서 서명을 만들어 넣으려 해도 정규식에서 걸린다. 이거 넣을 때 한 번 더 깐깐하게 보는 게 좋다 — 너무 느슨하면 의미가 없고, 너무 빡세면 hotfix 브랜치 같은 게 막힌다.
background.enabled: false는 기존 Pod에 대해 백그라운드 검사를 안 하겠다는 뜻이다. 이미 떠 있는 Pod까지 막으면 마이그레이션 시점에 클러스터가 마비된다. 정책 도입 초기에 이걸로 한 번 사고 칠 뻔했다.
4단계: 단계적 롤아웃
처음부터 failurePolicy: Fail로 가면 안 된다. 다음 순서로 점진 도입을 권장한다.
먼저 validationActions: [Audit] 모드로 한 주 정도 돌린다. Kyverno PolicyReport에 "어떤 이미지가 검증 실패하고 있는지" 다 찍힌다. 의외로 third-party 이미지가 클러스터에 많이 깔려 있다는 걸 발견하게 된다 (cert-manager, external-dns, metric-server… 이런 거). 이 단계에서 matchImageReferences의 glob을 정확히 좁혀야 한다. 우리 팀은 처음에 *로 잡아놓고 audit 돌렸다가 PolicyReport가 폭발해서 다음날 모두 모여 정리했다.
validationActions:
- Audit
Audit 결과 모든 이미지가 OK로 떨어지는 걸 확인한 뒤에야 Enforce로 바꾼다.
validationActions:
- Enforce
그리고 third-party 이미지(cosign 서명이 없는 이미지)는 별도 정책으로 명시적 예외 처리 — matchImageReferences에 넣지 말고 따로 allowlist 정책을 운영하는 편이 관리가 깔끔하다.
검증 캐시 관련 함정
캐시 TTL이 너무 길면 키 로테이션이 적용되는 데 시간이 걸린다. 우리 팀은 처음에 24시간으로 잡아놨다가, 이미지 서명 키 한 번 갈았을 때 캐시 만료 전까지 옛 키 검증이 통과해서 좀 어색했다. 1시간(3600초)이 적당한 것 같다.
또 하나, IVP는 Pod만 매칭한다. Deployment/StatefulSet 같은 컨트롤러는 Pod이 만들어질 때 한 번 검증된다. 그래서 배포 단에서 "이미지가 막혔다"가 아니라 "Pod가 안 뜬다"로 증상이 나타난다. 이게 처음엔 좀 헷갈리니까 알람을 PolicyReport 기준으로 따로 잡아두는 게 좋다.
마치며
cosign + Kyverno 조합이 처음 도입할 때는 yaml이 좀 무서워 보이는데, 한 번 IVP 한 개 굴려보면 그다음부턴 attestor만 바꿔가면서 SBOM 검증, vulnerability attestation 같은 걸 줄줄이 붙일 수 있다. 다음 글에서는 SLSA provenance attestation을 IVP로 검증하는 부분을 다뤄볼까 한다.
혹시 다른 검증 도구(Connaisseur, ratify) 쓰시는 분들 있으면 비교 의견 댓글로 남겨주세요.
'IT > DevSecOps' 카테고리의 다른 글
| Sealed Secrets 마스터 키 백업 안 해놓고 클러스터 옮겼다가 시크릿 47개 복구한 이야기 (0) | 2026.05.03 |
|---|---|
| Trivy로 CVE 1,400개 알림 폭탄 맞은 후, 우리 팀이 한 일 (0) | 2026.04.29 |
| External Secrets Operator vs Vault Agent Injector, 우리 팀은 결국 둘 다 쓰기로 했다 (1) | 2026.04.28 |
| Kyverno vs OPA Gatekeeper, 결국 뭘 골라야 하나 (0) | 2026.04.27 |