TT Lab
시작하기
배우기 러닝패스 코스

KCA — Kyverno 인증 어소시에이트

서명은 안전하다가 아니라 여기서 나왔다를 증명한다

TT Lab 에서 이어서 보기

한 줄 요약

이미지 서명은 그 이미지가 안전하다는 뜻이 아니다. 그 이미지가 특정 파이프라인에서 나왔다는 뜻이다. 취약점이 가득한 이미지도 서명될 수 있고, 그것이 서명의 실패가 아니라 정의다. verifyImages 규칙은 이 출처 증명을 어드미션에서 강제하는 장치다.

왜 이게 필요했나

레지스트리에 이미지가 있다는 사실은 아무것도 보장하지 않는다. 누군가 노트북에서 빌드해 손으로 푸시했을 수도 있고, 태그가 나중에 다른 내용으로 덮어써졌을 수도 있다. CI 파이프라인에 스캔 게이트를 아무리 촘촘히 걸어도, 그 파이프라인을 우회해 푸시한 이미지가 클러스터에 뜰 수 있다면 게이트는 권고에 가깝다.

서명은 이 구멍을 막는다. 파이프라인만 아는 키(또는 파이프라인의 OIDC 신원)로 이미지에 서명하고, 클러스터는 그 서명이 붙은 이미지만 받아들인다. 그러면 스캔 게이트가 실제로 강제된다. 스캔 결과를 강제력으로 바꾸는 지점이 여기다.

이미지 서명이 파드가 되기까지

verifyImages 규칙이 서명을 어드미션에서 강제하는 경로다. 서명이 말하는 것은 안전함이 아니라 출처다.

  1. 파이프라인이 서명한다스캔을 통과한 이미지에 파이프라인만 아는 키 또는 파이프라인의 OIDC 신원으로 서명한다. 서명되는 것은 안전함이 아니라 이 파이프라인에서 나왔다는 사실이다.
  2. 어드미션이 서명을 조회한다파드 스펙에서 이미지 참조를 꺼내 imageReferences 패턴에 걸리는 것만 레지스트리에 서명을 요청한다. 클러스터 밖으로 나가는 네트워크 호출이라 레지스트리가 파드를 만들 수 있는지도 정하는 경로가 된다.
  3. attestors 를 통과한 것만 파드가 된다정적 키, KMS, keyless 중 하나로 검증한다. keyless 는 서명 때의 issuer 와 subject 가 정책과 맞아야 한다. attestation 이 있으면 conditions 로 내용까지 검사할 수 있다.

여기서 구분할 것 서명이 있다고 취약점이 없는 것은 아니다. 스캔이 이미지를 얇게 만들고, 서명이 통과한 것을 표시하고, 클러스터가 서명된 것만 받는 것이 한 벌이다.

잠깐, 예측해 보세요 서명 검증에 더해 attestation 으로 이미지에 대해 무엇을 더 요구할 수 있을까?

설명 확인 · 채점 없는 자가 점검

SLSA provenance 로는 어떤 빌더가 어떤 소스에서 만들었는지를, SBOM 으로는 안에 어떤 컴포넌트가 들었는지를 conditions 로 검사한다. 예를 들어 SBOM 안의 라이브러리 버전이 취약 버전과 같지 않을 것을 요구할 수 있다.

근거 문서

어떻게 동작하나

verifyImages 규칙은 파드 스펙에서 이미지 참조를 모두 꺼내 imageReferences 패턴에 걸리는 것만 골라 레지스트리에 서명을 요청한다. 여기서 놓치기 쉬운 사실은 이 조회가 클러스터 밖으로 나가는 네트워크 호출이라는 점이다. 정책을 켜는 순간부터 레지스트리는 이미지를 내려받는 경로일 뿐 아니라 파드가 생성될 수 있는지를 결정하는 경로가 된다.

검증자는 attestors 로 표현한다. 정적 공개키(keys.publicKeys), KMS, 그리고 keyless 세 방식이 있다. keyless 는 Fulcio 가 발급한 단명 인증서로 서명하고 Rekor 투명성 로그에 기록을 남기는 방식인데, 여기서 subject 와 issuer 가 핵심이다. issuer 는 어느 OIDC 제공자인지이고 subject 는 그 안에서 누구인지다. GitHub Actions 라면 subject 가 워크플로 파일 경로와 참조 태그까지 포함하므로, 릴리스 워크플로 파일 이름을 바꾸거나 태그 규칙을 바꾸면 그 순간부터 전부 막힌다. 이 정밀함이 목적이지만 운영 부담이기도 하다.

attestors.count 는 반드시 알아야 하는 함정이다. 지정하지 않으면 entries 의 모든 항목이 통과해야 한다. 키를 하나 추가했는데 갑자기 전부 실패한다면 십중팔구 이것이다. 옛 키와 새 키를 둘 다 넣고 count 를 1로 두면 어느 쪽으로 서명된 이미지든 통과하므로 교체 기간을 무중단으로 넘길 수 있고, count 를 빼면 두 키 모두로 서명된 것만 통과해 정반대 결과가 나온다.

attestation 은 서명 위에 얹는 진술이다. SLSA provenance 는 "이 이미지가 어떤 빌더에서 어떤 소스로부터 나왔다" 를 담고, CycloneDX 나 SPDX SBOM 은 "이 안에 어떤 컴포넌트가 들어 있다" 를 담는다. conditions 로 그 내용을 검사할 수 있어서, 예를 들어 SBOM 안의 특정 라이브러리 버전이 취약 버전과 같지 않을 것을 요구하는 정책을 쓸 수 있다. 서명만 확인하면 누가 빌드했는지는 검사하지 않은 것이라, provenance 조건을 함께 거는 편이 낫다.

플래그 네 개도 정리해 두자. required 는 매칭된 이미지가 전부 검증을 거쳤음을 강제하고, verifyDigest 는 다이제스트 사용 자체를 강제하며, skipImageReferences 는 매칭에서 빼는 패턴 목록이고, mutateDigest 는 태그를 다이제스트로 바꿔 쓴다. 마지막 것이 파이프라인에 미치는 영향이 크다. 켜면 파드 스펙에 남는 것이 태그가 아니라 @sha256: 로 시작하는 고정 참조라, 같은 태그를 레지스트리에서 덮어써도 스케일 아웃으로 새로 뜨는 파드까지 처음 고정된 이미지를 쓴다. 이것이 의도한 불변성이지만, 태그를 밀어 놓고 파드를 지워 새 이미지를 받게 하던 파이프라인은 이 시점부터 조용히 아무 일도 하지 않는다.

성능에는 캐시가 끼어든다. 이미지 검증 결과는 TTL 캐시에 담기고, 이것은 정책 필드가 아니라 설치 수준 설정이다. 기본값은 활성화 true, 최대 키 1000개, TTL 60분이다. 레플리카 스무 개짜리 롤아웃에서 레지스트리 왕복 비용을 실제로 내는 것은 처음 한 번뿐이라, 두 번째 측정이 첫 번째보다 훨씬 빠르게 나오고 TTL 만료 다음 첫 배포만 유독 느리다. 캐시를 끄고 admission 지연을 재야 레지스트리가 죽었을 때 겪을 최악의 모습에 가까운 값이 나온다.

현장에서 만나는 모습

저자의 홈랩 레지스트리는 Harbor 이고 MetalLB 풀에서 받은 10.0.0.202 에 떠 있다. 사내 레지스트리를 쓰는 순간 반드시 만나는 문제가 자격 증명 경로가 둘일 수 있다는 점이다. 파드가 이미지를 잘 받아오는 것과 Kyverno 가 서명을 조회하는 것은 별개의 경로다. 파드는 자기 imagePullSecrets 를 쓰고 Kyverno 는 자기 자격 증명을 쓴다. Kyverno 의 자격 증명은 Kyverno 배포의 --imagePullSecrets 인자나 정책의 imageRegistryCredentials 로 준다. 저자가 겪은 때는 이 둘이 완전히 갈라져 있었다. Kyverno 공식 문서에 따르면 1.18 부터는 평가 중인 파드의 spec.imagePullSecrets 를 레지스트리 자격 증명으로 자동으로 쓴다. 그래서 쓰는 버전이 1.18 이후라면 파드에 풀 시크릿이 있는 경우 정책에 따로 적지 않아도 되고, 이전 버전이라면 위의 둘 중 하나를 직접 줘야 한다. 어느 쪽이든 Kyverno 가 프라이빗 레지스트리를 읽지 못하면 서명이 멀쩡히 있어도 없는 것처럼 보인다.

스캔 쪽 경험도 같은 결론으로 모인다. 어떤 이미지에서 총 1,247건의 취약점이 나왔고 Critical 이 9건이었는데, 수정본이 있는 것만 남기니 4건, 실제 악용이 확인된 목록과 대조하니 1건이었다. 그리고 그 1,247건을 없앤 것은 개별 패치가 아니라 베이스 이미지 교체였다. 애플리케이션이 실제로 링크하는 공유 라이브러리는 여덟 개인데 이미지에는 패키지가 432개 들어 있었기 때문이다. 디스트로리스로 옮기니 OS 패키지 취약점이 0이 되고 남은 것은 애플리케이션의 npm 의존성 두 건뿐이었다. 서명과 스캔은 이렇게 이어진다. 스캔으로 이미지를 얇게 만들고, 통과한 것에 서명을 붙이고, 클러스터가 서명된 것만 받아들이게 하는 것이 한 벌이다.

한 가지 더. 미러 레지스트리에 이미지만 복사하고 서명 아티팩트를 함께 옮기지 않으면 이 정책은 예외 없이 전부 실패한다. 폐쇄망으로 이미지를 들여올 때 가장 흔히 벌어지는 일이고, repository 로 서명 위치를 따로 지정하거나 미러링 파이프라인을 고치기 전에는 Enforce 를 켜면 안 된다.

키를 하나 더했을 때 count 가 갈라놓는 것

교체 기간에 옛 키와 새 키를 attestors 의 entries 에 둘 다 넣은 장면이다. 정책의 필드 하나만 다르다.

  • count 를 지정하지 않음entries 의 모든 항목이 통과해야 한다. 두 키 모두로 서명된 이미지만 통과하므로 키를 하나 더했더니 갑자기 전부 실패하는 일이 생긴다.
  • count 를 1 로 둠entries 가운데 하나만 통과하면 된다. 옛 키로 서명된 이미지도 새 키로 서명된 이미지도 통과해 교체 기간을 무중단으로 넘길 수 있다.

여기서 구분할 것 링크한 문서의 예시에도 count 와 entries 가 나오지만 이 도식이 그리는 count 의 의미는 본문이 근거다. 설치된 버전에서 직접 확인하고 쓴다.

잠깐, 예측해 보세요 교체 기간에 옛 키와 새 키를 둘 다 두고 count 를 1 로 두었다. 옛 키로만 서명된 이미지와 새 키로만 서명된 이미지는 각각 어떻게 될까?

설명 확인 · 채점 없는 자가 점검

둘 다 통과한다. 어느 쪽으로 서명됐든 entries 하나만 통과하면 되기 때문이다. count 를 빼 두었다면 두 키로 모두 서명된 이미지만 통과해 정반대 결과가 나온다.

근거 문서

다음 실습에서 할 것

/root/kca-verify/ 에 정적 키와 keyless 를 함께 쓰는 verifyImages 규칙, SLSA provenance 와 SBOM attestation 규칙, 허용 레지스트리 validate 규칙을 작성한다. 그리고 레지스트리 자격 증명 Secret 과 그것을 매단 ServiceAccount, 다이제스트로 고정된 Deployment 를 실제 클러스터에 올린다.