LabHub
배우기 러닝패스 코스

정책을 코드로 · 태그는 이름이고 다이제스트는 신원이다 · 실습

태그는 그대로인데 어제와 다른 것이 떴다

LabHub 에서 이어서 보기

목표

이미지 참조를 모양별로 가르는 눈을 만들고, 다이제스트 고정을 요구하는 규칙을 어드미션에 경고로 붙였다가 좁은 자리에만 차단으로 올리고, 태그를 다이제스트로 바꿔 적는 잠금 파일과 해석기를 쓰고, 같은 규칙을 클러스터 밖 빌드 산출물에도 걸어 봅니다.

왜 중요한가

태그는 옮길 수 있는 이름표이고 다이제스트는 내용의 해시라 옮길 수 없습니다. 이미지 정책의 거의 전부가 이 한 문장의 따름정리입니다. 같은 태그에 새 이미지를 밀어 넣는 것을 레지스트리가 막지 않으므로, 태그로만 배포한 팀은 '어제와 오늘이 다른' 사고를 재현하지 못하고 '되돌릴 대상이 사라진' 롤백을 만납니다. 그래서 규칙의 첫 줄은 latest 금지가 아니라 다이제스트 고정입니다. 다만 고정이 보장하는 것은 내용 동일성 하나뿐입니다 — 그 내용이 안전한지, 누가 만들었는지, 어떻게 만들었는지는 해시 안에 없고, 취약점 검사와 서명과 출처 증명이 각각 다른 축에서 그 자리를 채웁니다. 규칙을 쓰는 쪽에도 함정이 둘 있습니다. 컨테이너 목록은 spec.containers 하나가 아니라 initContainersephemeralContainers 까지 셋이고, 이미지 참조는 파드에만 있는 것이 아니라 워크로드의 파드 템플릿에도 한 단계 깊은 경로로 들어 있습니다. 두 가지를 빠뜨린 정책은 켜 놓고도 뚫립니다. 그리고 고정은 갱신 절차와 한 쌍입니다. 잠금 파일이 그 절차가 사는 자리입니다.

단계

1. /root/imgpolicy 에서 작업합니다. /root/imgpolicy/manifests/ 에 매니페스트 넷을 만드세요 — web.yaml 은 파드 probe-web(initContainers migrateregistry.internal/migrate:2.1, containers webregistry.internal/api:1.4.0), cache.yaml 은 파드 probe-cache(containers cacheregistry.internal/sidecar@sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266), batch.yaml 은 파드 probe-batch(containers batch 는 태그 없는 registry.internal/runtime, tailbusybox:latest), api.yaml 은 Deployment probe-api(템플릿 컨테이너 apiregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc). 그다음 /root/imgpolicy/classify.sh <디렉터리> 를 만드세요. 그 디렉터리의 .yaml·.yml 에서 이미지 참조를 모두 뽑아 중복 없이 정렬해 한 줄에 <참조> <모양> 으로 냅니다. 모양은 정확히 네 낱말 digest·latest·tag·notag 중 하나입니다. 마지막으로 ./classify.sh manifests > /root/imgpolicy/01-shapes.txt 로 결과를 저장하세요.
2. export KUBECONFIG=/root/.kube/configkubectl config use-context kwok-lab 로 시작합니다. /root/imgpolicy/policy.yamladmissionregistration.k8s.io/v1 의 ValidatingAdmissionPolicy require-image-digest 를 쓰세요 — matchConstraints.resourceRules 는 코어 그룹("")의 v1 pods 에 대한 CREATE·UPDATE 를 잡고, 변수 floatingobject.spec.containers 의 이미지 중 @sha256: 이 없는 것들의 목록이며, validation 은 size(variables.floating) == 0 이고 messageExpression 이 그 목록을 담습니다. 네임스페이스 img-warn 을 만들고 라벨 image-policy=warn 을 붙이세요. /root/imgpolicy/binding-warn.yaml 에 ValidatingAdmissionPolicyBinding image-digest-warn 을 쓰세요 — policyNamerequire-image-digest, validationActions["Warn", "Audit"], matchResources.namespaceSelector.matchLabelsimage-policy: warn 입니다. 시험용 파드 /root/imgpolicy/pod-tag.yaml 을 만드세요 — 파드 probe-tag, initContainers migrateregistry.internal/migrate:2.1, containers webregistry.internal/api:1.4.0 입니다. 정책과 바인딩을 적용한 뒤 img-warnpod-tag.yaml--dry-run=server 로 보내 출력을 /root/imgpolicy/02-warn.txt 에 표준 오류까지 함께 저장하세요. 경고가 뜨는데도 파드는 만들어져야 합니다.
3. 먼저 /root/imgpolicy/pod-init.yaml 을 만드세요 — 파드 probe-init, initContainers migrateregistry.internal/migrate:2.1(고정 안 됨), containers appregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc 입니다. 이것을 img-warn--dry-run=server 로 보내 보면 경고가 뜨지 않습니다. 그다음 /root/imgpolicy/policy.yaml 을 고치세요 — 변수 allImagesspec.containersspec.initContainersspec.ephemeralContainers 를 모두 이어 붙인 이미지 목록이 되게 하고(없을 수 있는 필드는 has(...) 로 먼저 확인), floating 은 그중 @sha256: 이 없는 것만 남깁니다. matchConstraintsresourcespods/ephemeralcontainers 도 더하세요. 그리고 /root/imgpolicy/pod-pinned.yaml 에 파드 probe-pinned(containers appregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc)를 만들어 img-warn실제로 만드세요(kubectl apply -n img-warn -f pod-pinned.yaml). 임시 컨테이너는 파드를 만들 때 붙일 수 없으므로 하위 자원으로 보냅니다:

kubectl get -n img-warn pod probe-pinned -o json | jq '.spec.ephemeralContainers = [{"name":"dbg","image":"busybox:latest"}]' | kubectl replace --raw "/api/v1/namespaces/img-warn/pods/probe-pinned/ephemeralcontainers?dryRun=All" -f -

고친 정책을 적용한 뒤 (1) pod-init.yamlimg-warn 에 다시 보내고 (2) 위 임시 컨테이너 요청을 보내, 두 출력을 /root/imgpolicy/03-lists.txt 에 표준 오류까지 함께 저장하세요. 이번에는 둘 다 경고가 떠야 합니다.
4. 네임스페이스 둘을 더 만드세요 — img-prod 에는 라벨 image-policy=enforce 를 붙이고, img-legacy 에는 아무 라벨도 붙이지 않습니다(범위 밖). 경고용 바인딩은 지우지 말고 그대로 둡니다. /root/imgpolicy/binding-deny.yaml 에 두 번째 바인딩 image-digest-deny 를 쓰세요 — 같은 policyNamevalidationActions["Deny"], 셀렉터는 image-policy: enforce 입니다. 이어서 /root/imgpolicy/policy.yamlmatchConstraintsapps 그룹 v1deployments 에 대한 CREATE·UPDATE 규칙을 더하고, 변수 podSpec 을 두어 has(object.spec.template) 이면 object.spec.template.spec 을, 아니면 object.spec 을 고르게 하세요. allImages 는 이제 variables.podSpec 의 세 목록에서 뽑습니다. /root/imgpolicy/dep-tag.yaml 에 Deployment probe-dep(템플릿 컨테이너 apiregistry.internal/api:1.4.0)를 만들고, img-prodimg-legacy 에 차례로 --dry-run=server 로 보내 두 출력을 /root/imgpolicy/04-deny.txt 에 저장하세요. img-prod 에서는 거부되고 img-legacy 에서는 그대로 만들어져야 합니다.
5. /root/imgpolicy/images.lock 에 잠금 파일을 JSON 객체 하나로 쓰세요. 열쇠는 태그까지 붙은 참조이고 값은 다이제스트입니다 — registry.internal/api:1.4.0sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc, registry.internal/migrate:2.1sha256:3e7c7af932c7b5e39c7812b321bbbd291da943717e96860e5976ac609ef84030, registry.internal/runtime:3.20sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c, registry.internal/sidecar:1.0sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266 입니다. 그다음 해석기 /root/imgpolicy/resolve.sh <매니페스트> [잠금파일] 을 만드세요. 잠금 파일 인자를 생략하면 /root/imgpolicy/images.lock 을 씁니다. 매니페스트를 읽어 태그 참조를 <이름>@<다이제스트> 로 바꾼 전체 매니페스트를 표준 출력에 내고, 이미 다이제스트로 고정된 줄은 그대로 흘려보냅니다. 잠금 파일에 없는 참조는 다이제스트를 지어내지 말고 표준 오류에 LOCK MISS <참조> 를 한 줄씩 내고 종료 코드 3 으로 끝냅니다(그때 표준 출력에는 아무것도 내지 않습니다). 만든 뒤 ./resolve.sh manifests/web.yaml > /root/imgpolicy/resolved/web.yaml 로 해석 결과를 저장하고, ./resolve.sh manifests/batch.yaml 의 표준 오류를 /root/imgpolicy/05-miss.txt 에 저장한 다음 그 파일 끝에 EXIT <종료코드> 한 줄을 덧붙이세요.
6. /root/imgpolicy/Dockerfile 을 쓰세요 — 첫 단계는 FROM registry.internal/builder:3.2 AS build, 두 번째 단계는 FROM registry.internal/runtime@sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c 입니다. /root/imgpolicy/build.json 에 빌드 산출물 목록을 쓰세요. 열쇠는 셋입니다 — build(문자열 api-2026-09-17), base_images(Dockerfile 의 FROM 참조를 적힌 순서 그대로 담은 문자열 배열), artifacts(nameimage 를 가진 객체 배열: apiregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc, workerregistry.internal/worker:2.0). /root/imgpolicy/policies/image-refs.yamlapiVersion: json.kyverno.io/v1alpha1 · kind: ValidatingPolicy 인 정책을 쓰세요. 규칙 둘입니다 — base-pinnedbase_images@sha256: 이 없는 것의 수가 0 이어야 하고, artifact-pinnedartifacts[].image@sha256: 이 없는 것의 수가 0 이어야 합니다. 그다음 게이트 /root/imgpolicy/jsongate.sh <페이로드> [정책] 을 만드세요(정책 인자를 생략하면 /root/imgpolicy/policies/image-refs.yaml). kyverno json scan 의 사람이 읽는 출력을 파싱해 위반 한 줄마다 FAIL <그 줄> 을 내고, 마지막에 RESULT failed=<개수> 를 낸 뒤 위반이 있으면 종료 코드 1, 없으면 0 으로 끝냅니다. 판정 줄(PASSED·FAILED·ERROR:)이 하나도 없으면 통과로 보지 말고 종료 코드 2 로 끝냅니다. 마지막으로 /root/imgpolicy/06-gate.txt 를 만드세요 — 첫 줄은 정책을 그대로 돌렸을 때의 종료 코드를 적은 SCAN EXIT <코드>, 그 아래에 ./jsongate.sh build.json 의 출력, 마지막 줄은 GATE EXIT <코드> 입니다.
7. 다이제스트가 보장하는 것은 내용 동일성 하나입니다. 보장하지 않는 세 가지를 다른 축의 기구가 채웁니다. /root/imgpolicy/limits.tsv 에 정확히 세 줄을 쓰세요. 각 줄은 탭 하나로 나뉜 두 칸이고, 첫 칸은 content-safety·author·build-path 셋이 한 번씩, 둘째 칸은 그 자리를 채우는 기구로 scan·signature·provenance 중 알맞은 것 하나입니다(첫 칸의 순서는 이 순서대로 적습니다). 그리고 /root/imgpolicy/revoked.txt 에 폐기된 다이제스트를 한 줄에 하나씩 적으세요 — 지금은 한 줄뿐입니다. registry.internal/sidecar:1.0 이 가리키는 다이제스트가 취약한 것으로 밝혀졌으니, 그 값을 /root/imgpolicy/images.lock 에서 찾아 그대로 적습니다(@ 앞의 이름은 적지 않고 sha256: 으로 시작하는 값만 적습니다).
8. /root/imgpolicy/audit.sh <매니페스트디렉터리> [폐기목록] 을 만드세요. 폐기 목록 인자를 생략하면 /root/imgpolicy/revoked.txt 를 씁니다. 그 디렉터리의 .yaml·.yml 에서 이미지 참조를 모두 뽑아 다이제스트로 고정되지 않은 참조마다 FLOATING <파일이름> <참조> 를, 고정은 됐지만 그 다이제스트가 폐기 목록에 있으면 REVOKED <파일이름> <참조> 를 한 줄씩 냅니다(그 줄들은 정렬해 냅니다). 마지막 줄은 정확히 RESULT floating=<수> revoked=<수> ready=<yes|no> 이고, 둘 다 0 일 때만 ready=yes 입니다. 종료 코드는 ready=yes 면 0, 아니면 1 입니다. 만든 뒤 ./audit.sh manifests 의 출력을 /root/imgpolicy/audit.txt 에 저장하고 그 파일 끝에 EXIT <종료코드> 한 줄을 덧붙이세요. 1단계에서 만든 매니페스트 묶음이 그대로 대상입니다.

참고

단계 8개

  1. 같은 묶음 안에 네 가지 모양의 이미지 참조가 있다
  2. 다이제스트를 요구하는 규칙을 경고로 붙인다
  3. 컨테이너 목록을 하나만 보면 그대로 뚫린다
  4. 차단은 넓게 켜지 않고 좁은 자리에만 켠다
  5. 태그를 다이제스트로 바꿔 적어 두고 그것만 보고 고친다
  6. 같은 규칙을 클러스터 밖 빌드 산출물에도 건다
  7. 고정은 신원을 정할 뿐 안전을 정하지 않는다
  8. 지금 차단으로 올려도 되는지 저장소가 답하게 한다