CKS — 쿠버네티스 보안 전문가 · 공급망 보안 · 이론
무엇이 들어오는지 아는 것이 먼저다
한 줄 요약
공급망 보안의 핵심 질문은 "이 이미지가 안전한가"가 아니라 "지금 클러스터에서 도는 그 바이트가
정확히 무엇이고, 어디서 왔고, 바꿔치기당하지 않았는가"입니다.
왜 이게 필요했나
myapp:v1.2 같은 태그는 변경 가능한 포인터입니다. 같은 태그에 다른 이미지를 푸시할 수 있고,latest 는 더합니다. 파드 스펙에 태그만 적혀 있으면 지금 노드에서 도는 바이트가 무엇인지
아무도 증명할 수 없습니다. 롤백을 해도 같은 태그가 다른 것을 가리킬 수 있습니다.
다이제스트(@sha256:...)는 내용의 해시이므로 이 문제를 원천적으로 없앱니다.
같은 논리가 언어 패키지에도 적용됩니다. 어떤 프로젝트에서 직접 적은 의존성은 37 개인데
실제 설치된 패키지는 1,123 개였고, 그중 486 개가 프로덕션 런타임에 함께 배포됐습니다.
검토하고 고른 것은 37 개, 신뢰하기로 한 것은 1,123 개입니다. 락파일의 무결성 해시는 "이
패키지는 안전하다"가 아니라 "이 패키지는 내가 마지막으로 본 것과 같다"만 보장합니다.
두 문장의 차이가 공급망 보안의 거의 전부입니다.
어떻게 동작하나
통제는 네 층으로 쌓입니다.
| 층 | 무엇을 답하는가 | 도구·필드 |
| --- | --- | --- |
| 고정 | 지금 도는 바이트가 무엇인가 | 이미지 다이제스트, imagePullPolicy |
| 출처 | 어디서 왔는가 | 허용 레지스트리 목록, imagePullSecrets |
| 증명 | 우리 파이프라인이 만든 것인가 | 서명(cosign), SBOM, 출처 증명 |
| 강제 | 아닌 것을 어떻게 막는가 | 어드미션 정책(VAP, Kyverno, Gatekeeper) |
imagePullPolicy 는 자주 오해받습니다. Always 는 매번 레지스트리에서 매니페스트를 확인하고,IfNotPresent 는 노드에 없을 때만 받아 옵니다. 태그를 쓰면 Always 가 안전해 보이지만,
그건 태그가 바뀔 수 있다는 문제를 매번 다시 겪겠다는 뜻이기도 합니다. 다이제스트로 고정하면
내용이 정의상 바뀌지 않으므로 IfNotPresent 로 두는 편이 합리적이고, 레지스트리 장애가
배포 장애로 번지지 않습니다.
스캐너에 대한 오해도 짚어야 합니다. 이미지 스캐너가 하는 일은 두 단계뿐입니다. 이미지 레이어를
풀어 설치된 패키지 목록을 만들고, 그 목록을 취약점 데이터베이스와 대조합니다. 그래서curl 로 받아 복사한 바이너리나 소스에서 직접 빌드한 라이브러리는 아예 보이지 않습니다.
스캔 결과가 깨끗하다는 것이 안전하다는 뜻이 아닌 첫 번째 이유입니다.
마지막 층이 어드미션입니다. 스캔 결과나 서명은 그 자체로 강제력이 없습니다. 파이프라인을
우회해 손으로 푸시한 이미지가 클러스터에 들어오지 못하게 만드는 것은 어드미션 정책뿐입니다.
그전까지 스캔 게이트는 우회 가능한 권고에 가깝습니다.
현장에서 만나는 모습
스캔 리포트를 처음 붙이면 대개 이런 숫자를 봅니다. payments:1.4.2 이미지 하나에
1,247 건(LOW 812, MEDIUM 289, HIGH 137, CRITICAL 9). 이 상태로 팀 채널에 던지면 아무 일도
일어나지 않습니다. --ignore-unfixed 하나를 붙이면 4 건(HIGH 3, CRITICAL 1)으로 줄어듭니다.
아무것도 안전해지지 않았지만, 지금 조치할 수 있는 항목만 남았습니다. 여기에 CISA KEV 목록과
대조하면 실제 악용이 확인된 것은 1 건이었습니다. 오늘 할 일은 그 하나입니다.
그리고 나머지 1,200 여 건을 없앤 것은 개별 패치가 아니라 베이스 이미지 교체였습니다.
같은 애플리케이션을 node:22(패키지 432 개)에서 distroless 베이스(19 개)로 옮기니
수정 가능한 CRITICAL 이 9 건에서 0 건이 됐습니다. 셸과 패키지 관리자가 사라진 것이
덤이 아니라 본질입니다. 침입자가 쓸 도구가 이미지에 없다는 뜻이기 때문입니다.
대신 kubectl exec 로 들어갈 수 없게 되므로 디버깅은 임시 컨테이너를 붙이는 방식으로 바뀝니다.
폐쇄망에서는 레지스트리 통제가 곧 가용성 문제이기도 합니다. containerd 1.x 는 pause 이미지
주소를 sandbox_image 키로 읽었는데 2.x 에서 pinned_images 아래 sandbox 키로 옮겨
갔습니다. 사내 레지스트리로 바꿔 둔 클러스터가 2.x 로 올라가면 옛 키가 조용히 무시되고
기본값인 공개 레지스트리로 돌아갑니다. 결과는 그 노드에서 파드가 하나도 뜨지 않는 것입니다.
특정 워크로드가 아니라 노드 전체가 죽기 때문에 네트워크 장애로 오인하기 쉽습니다.
다음 실습에서 할 것
먼저 이미지를 다이제스트로 고정하고, 허용 레지스트리 목록을 만들고, 프라이빗 레지스트리
인증용 Secret 을 ServiceAccount 에 붙입니다. 서명·SBOM 검증 정책은 매니페스트 파일로
작성합니다. 그다음 실습에서는 클러스터에 실제로 적용 가능한 ValidatingAdmissionPolicy 로
"태그만 쓴 이미지"를 CEL 표현식으로 거부하는 정책을 직접 만듭니다.