LabHub
배우기 러닝패스 코스

ポリシーをコードで

ポリシーを適用したのに何もブロックされなかった

LabHub 에서 이어서 보기

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

목표

쿠버네티스에 기본으로 들어 있는 ValidatingAdmissionPolicy(CEL)로 파드를 검사하는 규칙을 만들고, 경고에서 차단으로 올리고, 메시지에 위반한 값을 담고, 예외와 범위와 파라미터까지 직접 붙여 봅니다.

왜 중요한가

외부 정책 엔진을 설치하지 않고도 API 서버 안에서 검증을 할 수 있게 된 것이 ValidatingAdmissionPolicy 입니다. 웹훅과 달리 네트워크 왕복도 인증서도 없고, 엔진이 죽어서 클러스터가 멈추는 일도 없습니다. 대신 배워야 할 것이 하나 늘어납니다 — 판정 규칙과 적용 범위가 서로 다른 오브젝트라는 점입니다. 이 분리 덕분에 같은 규칙을 어떤 팀에는 경고로, 어떤 팀에는 차단으로 붙일 수 있고, 규칙 값만 ConfigMap 으로 빼서 정책을 고치지 않고 바꿀 수 있습니다. 반대로 이 분리를 모르면 '정책은 분명히 적용했는데 아무것도 안 막힌다' 는 자리에서 오래 헤맵니다. 실무에서 정책을 처음 넣을 때는 항상 Warn 으로 시작해 무엇이 걸리는지 보고, 시스템 컴포넌트를 matchConditions 로 빼 둔 다음, 범위를 좁게 잡아 Deny 로 올립니다. 이 순서를 건너뛴 정책은 배포를 멈추고 되돌려지고, 그러면 아무도 다시 켜지 않습니다.

단계

  1. /root/vap 에서 작업합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). /root/vap/policy.yamladmissionregistration.k8s.io/v1 의 ValidatingAdmissionPolicy require-cpu-limit 을 쓰세요. matchConstraints.resourceRules 는 코어 그룹("")의 v1 pods 에 대한 CREATE·UPDATE 를 잡고, validation 은 모든 컨테이너가 resources.limits.cpu 를 가질 것을 요구합니다. 시험용 파드 둘도 만드세요 — /root/vap/pod-nocpu.yaml 은 컨테이너 web-tier 가 memory 제한만 갖고, /root/vap/pod-ok.yaml 은 컨테이너 web-full 가 cpu·memory 제한을 모두 갖습니다. 정책을 적용한 뒤 라벨이 하나도 없는 네임스페이스 team-open 을 만들고, 거기에 pod-nocpu.yamlkubectl create --dry-run=server 로 보내 그 출력을 /root/vap/01-nobinding.txt 에 저장하세요.
  2. 네임스페이스 team-warn 을 만들고 라벨 cpu-limit=warn 을 붙이세요. /root/vap/binding-warn.yaml 에 ValidatingAdmissionPolicyBinding cpu-limit-warn 을 쓰세요 — policyNamerequire-cpu-limit, validationActions["Warn", "Audit"], matchResources.namespaceSelector.matchLabelscpu-limit: warn 입니다. 적용한 뒤 team-warnpod-nocpu.yaml--dry-run=server 로 보내 출력을 /root/vap/02-warn.txt 에 저장하세요(표준 오류까지 함께). 경고가 뜨는데도 파드는 만들어져야 합니다.
  3. 네임스페이스 team-deny 를 만들고 라벨 cpu-limit=deny 를 붙이세요. /root/vap/binding-deny.yaml 에 두 번째 바인딩 cpu-limit-deny 를 쓰세요 — 같은 policyNamevalidationActions["Deny"], 셀렉터는 cpu-limit: deny 입니다. 적용한 뒤 team-denypod-nocpu.yaml 을 보내 출력을 /root/vap/03-deny.txt 에 저장하고, 이어서 pod-ok.yaml 도 보내 통과하는지 확인하세요. 경고용 바인딩(cpu-limit-warn)은 지우지 말고 그대로 둡니다 — 두 범위가 동시에 살아 있는 것이 실제 전개 모습입니다.
  4. /root/vap/policy.yaml 을 고쳐 spec.variablesnoCpu 를 두세요 — cpu 제한이 없는 컨테이너의 이름 목록을 계산합니다. validation 은 size(variables.noCpu) == 0 으로 바꾸고, message 대신 messageExpression 으로 그 이름들과 파드 이름을 담은 메시지를 만드세요. 그리고 /root/vap/pod-two.yaml 에 컨테이너 둘을 가진 파드 probe-two 를 만드세요 — web-nolimitresources 자체가 없고, cache-ok 는 cpu·memory 제한을 모두 가집니다. 정책을 다시 적용한 뒤 team-deny 에 보내 거부 메시지를 /root/vap/04-message.txt 에 저장하세요. 메시지에는 web-nolimit 만 나와야 하고 cache-ok 는 나오면 안 됩니다.
  5. /root/vap/policy.yamlspec.matchConditions 를 더하세요. 조건 두 개입니다 — skip-kube-system-sa 는 요청자가 system:serviceaccount:kube-system: 으로 시작하는 서비스 계정이면 정책을 건너뛰게 하고, skip-exempt-pods 는 파드에 라벨 cpu-limit-exempt: "true" 가 있으면 건너뛰게 합니다. /root/vap/pod-exempt.yaml 에 그 라벨이 붙은 파드 probe-exempt(컨테이너 web-tier, memory 제한만)를 만드세요. 정책을 다시 적용한 뒤 두 가지를 team-deny 에서 확인해 출력을 /root/vap/05-skipped.txt 에 저장하세요 — (1) kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n team-deny -f pod-nocpu.yaml --dry-run=server, (2) pod-exempt.yaml 을 평소대로 보내기. 둘 다 만들어져야 합니다. 라벨 없는 pod-nocpu.yaml 은 여전히 거부돼야 합니다.
  6. 네임스페이스 vap-paramsteam-images(라벨 registry-check=on)를 만드세요. vap-params 에 ConfigMap allowed-registries 를 만들고 키 allowed 의 값을 정확히 registry.internal/,ghcr.io/labhub/ 로 두세요. /root/vap/policy-registries.yaml 에 두 번째 정책 allowed-registries 를 쓰세요 — spec.paramKindapiVersion: v1, kind: ConfigMap 이고, 같은 matchConstraints 로 파드를 잡고, params.data['allowed'] 를 쉼표로 나눈 목록 중 어느 것으로도 시작하지 않는 이미지를 거부합니다. /root/vap/binding-registries.yaml 에 바인딩 allowed-registries 를 쓰세요 — validationActions["Deny"], paramRef 는 그 ConfigMap(name·namespace, parameterNotFoundAction: Deny), 셀렉터는 registry-check: "on" 입니다. /root/vap/pod-img-bad.yaml 에 이미지가 docker.io/nginx:1.27 인 파드 probe-img-bad(컨테이너 web-img, cpu·memory 제한 포함)를 만들고, team-imagespod-ok.yamlpod-img-bad.yaml 을 차례로 보내 두 출력을 /root/vap/06-param.txt 에 저장하세요.
  7. /root/vap/scope.sh <네임스페이스> 를 만드세요. 그 네임스페이스에 /root/vap/pod-nocpu.yaml--dry-run=server 로 보내 보고, 거부되면 DENY 한 줄과 종료 코드 3, 경고만 뜨고 만들어지면 WARN 과 2, 아무 말 없이 만들어지면 OPEN 과 0 으로 끝냅니다. 출력은 그 낱말 한 줄뿐입니다. 네임스페이스 이름이나 라벨을 읽어 판단하지 말고 실제 요청의 결과로 판정하세요 — 채점기는 team-open 의 라벨을 잠시 바꿔 가며 이 스크립트를 부르고 원래대로 되돌립니다. 만든 뒤 team-deny·team-warn·team-open 세 곳에 차례로 돌려 결과를 /root/vap/07-scope.txt<네임스페이스> <낱말> 꼴로 한 줄씩 저장하세요.
  8. /root/vap/policy.yaml 에 두 번째 규칙을 더하세요 — 변수 noMem(memory 제한이 없는 컨테이너 이름 목록)과, size(variables.noMem) == 0 을 검사하는 두 번째 validation 입니다. 이 validation 의 messageExpression 에는 낱말 memory 와 그 컨테이너 이름이 들어가야 하고, 첫 번째 validation 의 메시지에는 낱말 cpu 와 그 컨테이너 이름이 들어가야 합니다. /root/vap/pod-mixed.yaml 에 파드 probe-mixed 를 만드세요 — 컨테이너 web-tier 는 memory 제한만, cache-tier 은 cpu 제한만 가집니다. 정책을 다시 적용한 뒤 이 파드를 team-warnteam-deny 에 차례로 보내 두 출력을 /root/vap/08-two-rules.txt 에 저장하세요. Warn 쪽은 두 규칙을 모두 알려 주고 Deny 쪽은 하나만 알려 줍니다.

참고

정책만 적용하면 아무 일도 일어나지 않는다

/root/vap 에서 작업합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). /root/vap/policy.yamladmissionregistration.k8s.io/v1 의 ValidatingAdmissionPolicy require-cpu-limit 을 쓰세요. matchConstraints.resourceRules 는 코어 그룹("")의 v1 pods 에 대한 CREATE·UPDATE 를 잡고, validation 은 모든 컨테이너가 resources.limits.cpu 를 가질 것을 요구합니다. 시험용 파드 둘도 만드세요 — /root/vap/pod-nocpu.yaml 은 컨테이너 web-tier 가 memory 제한만 갖고, /root/vap/pod-ok.yaml 은 컨테이너 web-full 가 cpu·memory 제한을 모두 갖습니다. 정책을 적용한 뒤 라벨이 하나도 없는 네임스페이스 team-open 을 만들고, 거기에 pod-nocpu.yamlkubectl create --dry-run=server 로 보내 그 출력을 /root/vap/01-nobinding.txt 에 저장하세요.

정책은 '판정하는 방법' 만 정의하고, '어디에 적용할지' 는 별도의 바인딩 오브젝트가 정합니다. 그래서 정책만 적용하면 API 서버는 그 식을 평가조차 하지 않습니다. CEL 에서 목록 전체를 검사할 때는 all(...) 을 쓰고, 없을 수도 있는 필드는 has(...) 로 먼저 확인해야 합니다. kubectl create -f <파일> --dry-run=server 는 어드미션을 그대로 통과시키되 오브젝트를 남기지 않습니다. 거부 메시지도, 경고도 진짜 요청과 똑같이 나옵니다.

바인딩을 붙이자 경고가 뜨기 시작한다

네임스페이스 team-warn 을 만들고 라벨 cpu-limit=warn 을 붙이세요. /root/vap/binding-warn.yaml 에 ValidatingAdmissionPolicyBinding cpu-limit-warn 을 쓰세요 — policyNamerequire-cpu-limit, validationActions["Warn", "Audit"], matchResources.namespaceSelector.matchLabelscpu-limit: warn 입니다. 적용한 뒤 team-warnpod-nocpu.yaml--dry-run=server 로 보내 출력을 /root/vap/02-warn.txt 에 저장하세요(표준 오류까지 함께). 경고가 뜨는데도 파드는 만들어져야 합니다.

바인딩은 '이 정책을 어디에, 어떤 강도로' 를 정합니다. Warn 은 응답 헤더로 경고만 돌려주고 요청은 통과시키고, Audit 은 감사 로그에 주석을 남깁니다. 경고는 표준 출력이 아니라 표준 오류로 나오므로 2>&1 을 붙여야 파일에 함께 담깁니다. namespaceSelector 는 네임스페이스 오브젝트의 라벨을 봅니다.

같은 정책을 Deny 로 올리자 요청이 막힌다

네임스페이스 team-deny 를 만들고 라벨 cpu-limit=deny 를 붙이세요. /root/vap/binding-deny.yaml 에 두 번째 바인딩 cpu-limit-deny 를 쓰세요 — 같은 policyNamevalidationActions["Deny"], 셀렉터는 cpu-limit: deny 입니다. 적용한 뒤 team-denypod-nocpu.yaml 을 보내 출력을 /root/vap/03-deny.txt 에 저장하고, 이어서 pod-ok.yaml 도 보내 통과하는지 확인하세요. 경고용 바인딩(cpu-limit-warn)은 지우지 말고 그대로 둡니다 — 두 범위가 동시에 살아 있는 것이 실제 전개 모습입니다.

정책 하나에 바인딩을 여러 개 붙일 수 있고, 각 바인딩이 자기 범위와 자기 강도를 가집니다. 그래서 '같은 규칙을 어떤 팀에는 경고로, 어떤 팀에는 차단으로' 가 정책을 복사하지 않고 됩니다. 거부 메시지에는 어느 정책과 어느 바인딩이 막았는지가 함께 찍힙니다 — 여러 규칙이 겹칠 때 원인을 찾는 단서입니다.

거부 메시지가 어느 컨테이너인지 말해 준다

/root/vap/policy.yaml 을 고쳐 spec.variablesnoCpu 를 두세요 — cpu 제한이 없는 컨테이너의 이름 목록을 계산합니다. validation 은 size(variables.noCpu) == 0 으로 바꾸고, message 대신 messageExpression 으로 그 이름들과 파드 이름을 담은 메시지를 만드세요. 그리고 /root/vap/pod-two.yaml 에 컨테이너 둘을 가진 파드 probe-two 를 만드세요 — web-nolimitresources 자체가 없고, cache-ok 는 cpu·memory 제한을 모두 가집니다. 정책을 다시 적용한 뒤 team-deny 에 보내 거부 메시지를 /root/vap/04-message.txt 에 저장하세요. 메시지에는 web-nolimit 만 나와야 하고 cache-ok 는 나오면 안 됩니다.

variables 는 식 하나를 이름으로 묶어 두고 variables.<이름> 으로 다시 쓰는 장치입니다. 지연 평가라 validation 에서 쓰지 않으면 계산되지 않습니다. filter(...) 로 위반한 것만 남기고 map(...) 으로 이름만 뽑으면 목록이 됩니다. 문자열 목록은 join(', ') 으로 이어 붙일 수 있습니다. messageExpression 이 오류를 내면 API 서버는 조용히 message 로 되돌아갑니다 — 메시지가 바뀌지 않으면 식을 의심하세요.

정책이 아예 보지 않는 요청을 만든다

/root/vap/policy.yamlspec.matchConditions 를 더하세요. 조건 두 개입니다 — skip-kube-system-sa 는 요청자가 system:serviceaccount:kube-system: 으로 시작하는 서비스 계정이면 정책을 건너뛰게 하고, skip-exempt-pods 는 파드에 라벨 cpu-limit-exempt: "true" 가 있으면 건너뛰게 합니다. /root/vap/pod-exempt.yaml 에 그 라벨이 붙은 파드 probe-exempt(컨테이너 web-tier, memory 제한만)를 만드세요. 정책을 다시 적용한 뒤 두 가지를 team-deny 에서 확인해 출력을 /root/vap/05-skipped.txt 에 저장하세요 — (1) kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n team-deny -f pod-nocpu.yaml --dry-run=server, (2) pod-exempt.yaml 을 평소대로 보내기. 둘 다 만들어져야 합니다. 라벨 없는 pod-nocpu.yaml 은 여전히 거부돼야 합니다.

matchConditions 는 matchConstraints 가 고른 요청을 한 번 더 거릅니다. 조건이 모두 참일 때만 validations 가 평가되므로, '건너뛰고 싶다' 는 조건을 부정으로 씁니다. request 에는 요청자 정보(request.userInfo.username)가 들어 있습니다. 컨트롤러가 만드는 파드까지 막으면 클러스터가 스스로를 복구하지 못하게 되므로, 시스템 주체를 빼 두는 것이 전개의 첫 안전장치입니다. kubectl --as 로 다른 주체인 척할 수 있습니다.

규칙 값을 ConfigMap 으로 빼서 정책을 고치지 않고 바꾼다

네임스페이스 vap-paramsteam-images(라벨 registry-check=on)를 만드세요. vap-params 에 ConfigMap allowed-registries 를 만들고 키 allowed 의 값을 정확히 registry.internal/,ghcr.io/labhub/ 로 두세요. /root/vap/policy-registries.yaml 에 두 번째 정책 allowed-registries 를 쓰세요 — spec.paramKindapiVersion: v1, kind: ConfigMap 이고, 같은 matchConstraints 로 파드를 잡고, params.data['allowed'] 를 쉼표로 나눈 목록 중 어느 것으로도 시작하지 않는 이미지를 거부합니다. /root/vap/binding-registries.yaml 에 바인딩 allowed-registries 를 쓰세요 — validationActions["Deny"], paramRef 는 그 ConfigMap(name·namespace, parameterNotFoundAction: Deny), 셀렉터는 registry-check: "on" 입니다. /root/vap/pod-img-bad.yaml 에 이미지가 docker.io/nginx:1.27 인 파드 probe-img-bad(컨테이너 web-img, cpu·memory 제한 포함)를 만들고, team-imagespod-ok.yamlpod-img-bad.yaml 을 차례로 보내 두 출력을 /root/vap/06-param.txt 에 저장하세요.

paramKind 는 '이 정책이 파라미터로 무엇을 읽는가' 를, 바인딩의 paramRef 는 '그중 어느 오브젝트인가' 를 정합니다. 그래서 같은 정책을 팀마다 다른 값으로 붙일 수 있습니다. CEL 의 문자열에는 splitstartsWith 가 있고, 목록에는 exists 가 있습니다. 파라미터를 못 찾았을 때 어떻게 할지는 parameterNotFoundAction 이 정합니다 — 보안 규칙이라면 여는 쪽이 아니라 막는 쪽이 기본이어야 합니다. 이 실습의 뒷부분에서 채점기가 ConfigMap 값을 잠시 바꿔 보고 되돌립니다. 허용 목록을 식 안에 적어 두면 그때 들통납니다.

같은 요청이 네임스페이스마다 다른 답을 받는다

/root/vap/scope.sh <네임스페이스> 를 만드세요. 그 네임스페이스에 /root/vap/pod-nocpu.yaml--dry-run=server 로 보내 보고, 거부되면 DENY 한 줄과 종료 코드 3, 경고만 뜨고 만들어지면 WARN 과 2, 아무 말 없이 만들어지면 OPEN 과 0 으로 끝냅니다. 출력은 그 낱말 한 줄뿐입니다. 네임스페이스 이름이나 라벨을 읽어 판단하지 말고 실제 요청의 결과로 판정하세요 — 채점기는 team-open 의 라벨을 잠시 바꿔 가며 이 스크립트를 부르고 원래대로 되돌립니다. 만든 뒤 team-deny·team-warn·team-open 세 곳에 차례로 돌려 결과를 /root/vap/07-scope.txt<네임스페이스> <낱말> 꼴로 한 줄씩 저장하세요.

경고는 표준 오류로 나오므로 출력을 합쳐 받아야 보입니다. 거부는 종료 코드가 0 이 아니고, 경고는 종료 코드가 0 입니다 — 이 둘을 먼저 가르고 나서 경고 여부를 봅니다. set -e 를 걸면 거부에서 스크립트가 먼저 죽습니다. 어드미션은 네임스페이스의 라벨을 보고 범위를 정하므로, 같은 매니페스트가 자리에 따라 다른 답을 받습니다.

규칙 둘을 한 정책에 넣자 Deny 와 Warn 이 서로 다른 것을 알려 준다

/root/vap/policy.yaml 에 두 번째 규칙을 더하세요 — 변수 noMem(memory 제한이 없는 컨테이너 이름 목록)과, size(variables.noMem) == 0 을 검사하는 두 번째 validation 입니다. 이 validation 의 messageExpression 에는 낱말 memory 와 그 컨테이너 이름이 들어가야 하고, 첫 번째 validation 의 메시지에는 낱말 cpu 와 그 컨테이너 이름이 들어가야 합니다. /root/vap/pod-mixed.yaml 에 파드 probe-mixed 를 만드세요 — 컨테이너 web-tier 는 memory 제한만, cache-tier 은 cpu 제한만 가집니다. 정책을 다시 적용한 뒤 이 파드를 team-warnteam-deny 에 차례로 보내 두 출력을 /root/vap/08-two-rules.txt 에 저장하세요. Warn 쪽은 두 규칙을 모두 알려 주고 Deny 쪽은 하나만 알려 줍니다.

validations 는 목록이고 각 항목이 자기 expression 과 자기 messageExpression 을 가집니다. 변수는 여러 validation 이 나눠 씁니다. Deny 는 처음 실패한 항목에서 요청을 끝내지만 Warn 은 실패한 것을 모두 경고로 돌려줍니다 — 그래서 전개 초기의 Warn 단계가 '무엇을 다 고쳐야 하는지' 를 한 번에 보여 줍니다. 두 메시지가 구분되지 않으면 어느 규칙이 걸렸는지 아무도 모릅니다.