LabHub
学习 学习路径 课程

KCSA — Kubernetes 安全助理

今天的 nginx:latest 还是昨天那个镜像吗

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

같은 애플리케이션을 가리키는 세 가지 이미지 표기(다이제스트 고정 / latest 태그 / 사내 레지스트리 버전 태그)를 나란히 배포해 비교하고, imagePullPolicy·pull secret 을 출처에 맞게 설정한 뒤, ValidatingAdmissionPolicy 로 "허용 레지스트리이거나 다이제스트로 고정된 이미지만" 들이는 규칙을 집행하고 거절을 관찰합니다.

왜 중요한가

클라우드 네이티브 보안의 4C(Cloud·Cluster·Container·Code) 중 안쪽 두 층은 무엇을 돌리는가 의 문제입니다. 클러스터를 아무리 잘 잠가도, 파드가 끌어오는 이미지가 누군가 바꿔치기한 것이라면 방어는 안에서 무너집니다.

태그는 이름표일 뿐이라 레지스트리에서 언제든 다른 이미지로 다시 붙일 수 있습니다. nginx:latest 는 오늘과 내일이 다른 이미지일 수 있고, app:1.2.3 같은 버전 태그도 덮어쓰기를 막아 두지 않았다면 마찬가지입니다. 반면 @sha256:... 다이제스트는 이미지 내용의 해시라 한 글자만 달라져도 다른 값이 됩니다. 그래서 공급망 보안의 첫걸음은 "어떤 이미지가 태그로만 가리켜지고 있는가" 를 가려내는 것입니다.

imagePullPolicy 는 노드가 캐시를 믿을지 정합니다. 태그 이미지에 IfNotPresent 를 쓰면 노드마다 서로 다른 시점의 이미지를 돌리게 될 수 있습니다. 사내 레지스트리의 자격 증명은 pull secret 으로 주고, 서비스어카운트에 달아 두면 파드마다 반복하지 않아도 됩니다.

마지막으로 어드미션 정책은 생성 요청 시점에만 평가됩니다. 2단계에서 정책 없이 만든 Deployment 들은 6단계 정책이 켜진 뒤에도 그대로 남아 있습니다. 하지만 mutable(nginx:latest)의 파드가 지워져 ReplicaSet 이 새 파드를 만들려 하면 그 생성 요청은 거절됩니다 — 정책을 켜기 전에 기존 워크로드를 먼저 점검해야 하는 이유입니다.

단계

  1. 네임스페이스 kcsa-supply 를 만듭니다.
  2. pinned(다이제스트)·mutable(nginx:latestinternal(registry.example.com/app:1.2.3) Deployment 를 만듭니다.
  3. 각 이미지를 digest/tag 로 분류해 /root/kcsa-supply/classify.txt 에 적습니다.
  4. 태그 이미지는 imagePullPolicy: Always, 다이제스트 이미지는 IfNotPresent 로 바꿉니다.
  5. docker-registry 시크릿 regcred 를 만들고 서비스어카운트 deployer 의 imagePullSecrets 에 답니다.
  6. 허용 레지스트리 또는 다이제스트 고정만 통과시키는 정책 require-allowed-image 와 바인딩을 만듭니다.
  7. docker.io/library/evil:1 파드가 거절되는 메시지를 /root/kcsa-supply/denied-image.txt 에 저장합니다.
  8. 점검 결과를 /root/kcsa-supply/report.txt 에 남깁니다.

참고

출처를 따질 작업 구역 만들기

네임스페이스 kcsa-supply 를 만듭니다. 뒤 단계의 정책 바인딩은 자동으로 붙는 kubernetes.io/metadata.name=kcsa-supply 라벨로 이 네임스페이스를 고릅니다.

모든 네임스페이스에는 자기 이름을 값으로 하는 kubernetes.io/metadata.name 라벨이 자동으로 붙습니다. create 를 --dry-run=client 로 만들어 apply 하면 여러 번 돌려도 안전합니다.

같은 nginx 인데 출처 표기가 셋 다 다르다

네임스페이스 kcsa-supply 에 Deployment 세 개를 만듭니다(각 replicas 1, selector 와 파드 라벨을 맞출 것). pinned 는 이미지 nginx@sha256:0000000000000000000000000000000000000000000000000000000000000000, mutablenginx:latest, internalregistry.example.com/app:1.2.3. 각 Deployment 의 첫 번째 컨테이너 이미지가 정확히 이 문자열이어야 합니다.

이미지 참조는 [레지스트리/]저장소[:태그][@sha256:다이제스트] 모양입니다. kubectl create deployment 는 이미지 문자열에서 컨테이너 이름을 지어내는데, @sha256: 가 들어간 이미지에서는 이름이 규칙에 어긋나 실패할 수 있습니다. 컨테이너 이름을 직접 적는 YAML 매니페스트로 만드는 편이 안전합니다. 다이제스트는 16진수 64자리여야 문법상 유효합니다.

어느 이미지가 내일 바뀔 수 있는지 가려내기

세 Deployment 의 이미지를 digest(다이제스트로 고정됨) 또는 tag(태그로 가리킴)로 분류해 /root/kcsa-supply/classify.txt이름=분류 형식으로 한 줄씩(pinned, mutable, internal) 적습니다.

태그는 레지스트리 쪽에서 언제든 다른 이미지로 다시 가리킬 수 있는 이름표이고, 다이제스트는 이미지 내용의 해시라 바뀌지 않습니다. latest 뿐 아니라 1.2.3 같은 버전 태그도 결국 태그입니다. kubectl get deploy -o jsonpath 로 실제 이미지 문자열을 보고 @sha256: 가 있는지로 가르세요.

태그를 믿지 말고 매번 다시 확인하게 하기

Deployment 의 컨테이너 imagePullPolicy 를 바꿉니다 — 태그를 쓰는 mutableinternalAlways, 다이제스트로 고정한 pinnedIfNotPresent.

태그는 가리키는 대상이 바뀔 수 있으니 노드 캐시에 같은 이름의 옛 이미지가 있어도 레지스트리에 다시 물어봐야 합니다. 다이제스트는 내용 해시라 캐시에 있으면 그대로 써도 안전합니다. 기본값은 태그가 latest 이거나 없으면 Always, 그 외엔 IfNotPresent 라서 internal 은 직접 바꿔야 합니다. kubectl patchspec.template.spec.containers 의 해당 컨테이너(이름으로 매칭)를 고치세요.

사내 레지스트리 자격 증명을 서비스어카운트에 달기

네임스페이스 kcsa-supply 에 docker-registry 타입 시크릿 regcred(서버 registry.example.com, 사용자 u, 비밀번호 p)를 만들고, 서비스어카운트 deployer 를 만들어 그 imagePullSecretsregcred 를 넣습니다.

kubectl create secret 에는 docker-registry 하위 명령이 있어 kubernetes.io/dockerconfigjson 타입 시크릿을 만들어 줍니다(서버·사용자·비밀번호 플래그). 서비스어카운트에 imagePullSecrets 를 달아 두면 그 SA 로 뜨는 파드마다 시크릿을 따로 적지 않아도 됩니다. SA 를 만든 뒤 patch 로 필드를 추가하세요.

허용 레지스트리 밖의 이미지는 들이지 않는다

ValidatingAdmissionPolicy require-allowed-image 와 바인딩 require-allowed-image-binding 을 만듭니다. 정책은 파드 CREATE 에서 모든 컨테이너 이미지가 registry.example.com/ 으로 시작하거나 @sha256: 를 포함해야 통과시키고(메시지 image must come from registry.example.com or be digest-pinned, failurePolicy: Fail), 바인딩은 validationActions ["Deny"]kubernetes.io/metadata.name: kcsa-supply 네임스페이스에만 적용합니다.

CEL 의 문자열에는 startsWith()contains() 가 있습니다. 접두사 끝에 / 까지 넣어야 registry.example.com.evil.io/... 같은 이름이 빠져나가지 못합니다. 정책은 검사 내용, 바인딩은 적용 범위와 행동(Deny)을 정한다는 점을 기억하세요.

Docker Hub 에서 온 낯선 이미지가 문 앞에서 막히다

네임스페이스 kcsa-supply 에 이미지 docker.io/library/evil:1 인 파드를 만들어 보고, API 서버가 돌려준 거절 메시지 전체를 /root/kcsa-supply/denied-image.txt 에 저장합니다.

거절되면 kubectl 이 0 이 아닌 종료 코드로 끝나고 오류를 표준 에러에 씁니다. 표준 에러까지 파일로 받으세요. 바인딩 직후엔 반영에 1~2초 걸릴 수 있으니, 파드가 만들어져 버렸다면 지우고 다시 시도하세요. 2단계의 Deployment 파드가 그대로 살아 있는 것도 확인해 보세요 — 어드미션은 생성 요청에만 걸립니다.

이미지 출처 점검 결과를 장부로 남기기

점검 결과를 /root/kcsa-supply/report.txt 에 네 줄로 적습니다 — pinned=digest, mutable=tag, latest-pullpolicy=Always, untrusted-registry=denied. 채점기는 각 줄을 실제 Deployment spec 과 허용 목록 밖 이미지로 파드를 만들어 본 결과에 대조합니다.

앞 단계에서 직접 확인한 값을 옮기면 됩니다. latest-pullpolicymutable 컨테이너의 실제 imagePullPolicy 값(대소문자 그대로), untrusted-registry 는 7단계에서 본 결과(denied 또는 allowed)입니다.