LabHub
배우기 러닝패스 코스

ポリシーをコードで

Webhook を一つ Fail にしただけでクラスタが止まった

LabHub 에서 이어서 보기

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

목표

어드미션 웹훅의 적용 범위를 세 층에서 직접 좁혀 보고, 정책 서버가 죽은 상태를 진짜로 만들어 failurePolicytimeoutSeconds 가 무엇을 맞바꾸는지 시간을 재서 확인하고, 클러스터의 모든 웹훅에 대해 '이것이 죽으면 무엇이 멈추는가' 를 답하는 보고서와 검사기를 만듭니다.

왜 중요한가

정책 도입 사고의 기록을 모아 보면 규칙이 틀려서 난 사고는 드뭅니다. 대부분은 둘 중 하나입니다 — 너무 넓게 걸었거나, 고장 났을 때의 동작을 정하지 않았거나. 넓게 거는 비용은 정책 한 번의 실행 시간이 아니라 들어오는 모든 쓰기 요청에 붙는 지연세이고, 클러스터에는 사람이 보내는 요청보다 컨트롤러가 보내는 요청이 훨씬 많습니다. 더 무서운 것은 두 번째입니다. 정책이 kube-system 까지 보도록 걸어 두고 정책 서버가 죽으면, failurePolicy: Fail 아래에서 클러스터는 자기 자신을 복구할 수 없게 됩니다 — 서버를 되살리려는 배포도 그 정책을 지나야 하기 때문입니다. 범위와 고장 모드는 따로 고르는 값이 아니라 한 쌍으로 고르는 값이고, 이 실습은 그 한 쌍을 손으로 돌려 봅니다. 마지막에 만드는 폭발 반경 보고서는 정책을 켜기 전이 아니라 이미 켜 둔 클러스터에서 지금 당장 만들어 봐야 하는 물건입니다.

단계

  1. /root/polscope 에서 작업합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). 먼저 네임스페이스 scope-app·scope-ops·scope-builtin 을 만들고, 세 곳 모두에 kubectl create sa default -n <네임스페이스>default 서비스 계정을 직접 만드세요(이 클러스터에는 컨트롤러 매니저가 없어 저절로 생기지 않습니다). /root/polscope/webhook-wide.yaml 에 ValidatingWebhookConfiguration scope-wide-audit 을 쓰세요 — 웹훅 이름은 wide.polscope.local, admissionReviewVersions["v1"], sideEffectsNone, failurePolicyIgnore, timeoutSeconds2, clientConfig.urlhttps://192.0.2.77:8443/validate 입니다. rules 는 가장 넓게 — apiGroups: ["*"], apiVersions: ["*"], operations: ["CREATE", "UPDATE"], resources: ["*"], scope: "*" 로 두고 셀렉터는 하나도 두지 않습니다. 시험용 매니페스트 셋도 만드세요 — /root/polscope/pod.yaml(파드 probe-app, 컨테이너 web, 이미지 registry.internal/app:1.0), /root/polscope/pod-guard.yaml(파드 probe-guard, 라벨 polscope.io/guard: "on", 같은 컨테이너), /root/polscope/cm.yaml(ConfigMap probe-cm, 데이터 a: b). 적용한 뒤 네 가지 요청의 소요 시간을 재세요 — kubectl create -n scope-app -f pod.yaml --dry-run=server, kubectl create -n scope-app -f cm.yaml --dry-run=server, kubectl create ns probe-ns --dry-run=server, kubectl get pods -n scope-app. 1초 이상 걸렸으면 SLOW, 아니면 FAST/root/polscope/01-reach.txtpod-create·configmap-create·namespace-create·pod-list 네 줄을 <이름> <낱말> 꼴로 적으세요.
  2. /root/polscope/webhook-strict.yaml 에 두 번째 설정 scope-strict 를 쓰세요 — 웹훅 이름은 strict.polscope.local, rulesscope-wide-audit똑같이 가장 넓게, 다른 것은 셋뿐입니다: failurePolicyFail, timeoutSeconds3, clientConfig.url 은 연결이 거부되는 주소 https://127.0.0.1:19443/validate 입니다. scope-wide-audit 은 지우지 말고 그대로 둡니다 — 같은 범위에 고장 모드만 다른 둘이 나란히 서 있는 것이 이 실습의 대조군입니다. 적용한 뒤 네 가지를 시도해 막히는지 보고, 막히면 BLOCK 통과하면 PASS/root/polscope/02-blocked.txt<이름> <낱말> 네 줄을 적으세요 — pod-create-scope-app(kubectl create -n scope-app -f pod.yaml --dry-run=server), configmap-kube-system(kubectl create -n kube-system -f cm.yaml --dry-run=server), namespace-create(kubectl create ns probe-ns --dry-run=server), webhookconfig-edit(kubectl apply -f webhook-strict.yaml 을 한 번 더).
  3. 먼저 면제 라벨로 빠져나가 보세요 — kubectl label ns kube-system polscope.io/admission=exempt --overwrite 를 돌려 그 출력을 표준 오류까지 함께 /root/polscope/03-lockout.txt 에 저장하세요(막힙니다). 그다음 /root/polscope/webhook-strict.yamlscope-strictnamespaceSelector.matchExpressions 두 개를 더해 다시 적용하세요 — (1) 키 kubernetes.io/metadata.name, 연산자 NotIn, 값 ["kube-system", "kube-node-lease", "kube-public"], (2) 키 polscope.io/admission, 연산자 NotIn, 값 ["exempt"]. 이제 라벨이 붙습니다 — kube-systemscope-opspolscope.io/admission=exempt 를 거세요. 마지막으로 /root/polscope/pod-guard.yaml 을 세 곳에 --dry-run=server 로 보내 /root/polscope/03-escape.txtkube-system·scope-ops·scope-app 세 줄을 <네임스페이스> <BLOCK|PASS> 꼴로 적으세요.
  4. /root/polscope/webhook-strict.yamlscope-strict 를 두 층에서 좁히세요 — rules 는 코어 그룹("")의 v1 pods 에 대한 CREATE·UPDATE 만, scopeNamespaced 로 바꾸고, objectSelector.matchLabelspolscope.io/guard: "on" 라벨이 붙은 객체만 보게 합니다. 3단계의 namespaceSelector 는 그대로 둡니다. 적용한 뒤 네 가지 요청을 다시 보내 /root/polscope/04-narrow.txt<이름> <좁히기전> <좁힌뒤> 네 줄을 적으세요 (좁히기 전 값은 2단계에서 본 것입니다) — pod-guard-scope-app(pod-guard.yamlscope-app 에), pod-plain-scope-app(pod.yamlscope-app 에), configmap-scope-app(cm.yamlscope-app 에), namespace-create(kubectl create ns probe-ns --dry-run=server). 판정은 BLOCK 또는 PASS 입니다.
  5. /root/polscope/webhook-strict.yamlscope-strictmatchConditions 두 개를 더해 다시 적용하세요 — skip-kube-system-sa 는 요청자가 system:serviceaccount:kube-system: 으로 시작하는 서비스 계정이면 웹훅을 부르지 않게 하고, skip-break-glass 는 요청자의 그룹에 polscope:break-glass 가 있으면 부르지 않게 합니다. 적용한 뒤 같은 /root/polscope/pod-guard.yamlscope-app 에 세 가지 주체로 보내 /root/polscope/05-conditions.txtkube-system-sa·break-glass·normal 세 줄을 <이름> <BLOCK|PASS> 꼴로 적으세요 — kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n scope-app -f pod-guard.yaml --dry-run=server, kubectl --as=oncall --as-group=polscope:break-glass --as-group=system:masters create -n scope-app -f pod-guard.yaml --dry-run=server, 그리고 지금 계정 그대로 한 번.
  6. /root/polscope/webhook-slow.yaml 에 세 번째 설정 scope-slow 를 쓰세요 — 웹훅 이름 slow.polscope.local, clientConfig.url 은 응답이 아예 오지 않는 주소 https://192.0.2.77:8443/validate, sideEffectsNone, namespaceSelector.matchExpressions 는 키 kubernetes.io/metadata.nameIn ["scope-ops"] 로, objectSelector.matchLabelspolscope.io/slow: "on", rules 는 코어 그룹의 v1 podsCREATE·UPDATE(scope: "Namespaced"), 최종 상태는 failurePolicy: FailtimeoutSeconds: 3 입니다. /root/polscope/pod-slow.yaml 에 파드 probe-slow(라벨 polscope.io/slow: "on", 컨테이너 web, 이미지 registry.internal/app:1.0)를 만드세요. 그리고 /root/polscope/timeout-probe.sh <Fail|Ignore> <초> 를 만드세요 — scope-slow 를 그 두 값으로 잠시 바꾸고, scope-opspod-slow.yaml--dry-run=server 로 한 번 보내 걸린 시간을 재고, 원래 값으로 반드시 되돌린 뒤, BLOCK <초> 또는 PASS <초> 한 줄만 출력합니다(초는 반올림한 정수). 이 스크립트로 세 번 재어 /root/polscope/06-timeout.txt<failurePolicy> <타임아웃> <BLOCK|PASS> <걸린초> 세 줄을 적으세요 — Fail 6, Ignore 6, Fail 4 로 각각 한 번씩입니다.
  7. 웹훅 서버가 없는 지금 이대로, 같은 자리에 내장 정책을 세워 비교합니다. 네임스페이스 scope-builtin 에 라벨 pod-security.kubernetes.io/enforce=baseline 을 거세요. /root/polscope/vap-owner.yaml 에 ValidatingAdmissionPolicy polscope-require-owner(코어 그룹 v1 podsCREATE·UPDATE 를 잡고, 파드 메타데이터에 owner 라벨이 있을 것을 요구)와 같은 이름의 ValidatingAdmissionPolicyBinding(validationActions["Deny"], matchResources.namespaceSelectorkubernetes.io/metadata.nameIn ["scope-builtin"])을 함께 쓰고 적용하세요. 시험용 파드 셋을 더 만드세요 — /root/polscope/pod-owned.yaml(파드 probe-owned, 라벨 owner: platform), /root/polscope/pod-host.yaml(파드 probe-host, 같은 라벨에 spec.hostNetwork: true), /root/polscope/pod-both.yaml(파드 probe-both, 라벨 owner: platformpolscope.io/guard: "on"). 컨테이너는 셋 다 web/registry.internal/app:1.0 입니다. 네 매니페스트를 scope-builtin--dry-run=server 로 보내 무엇이 판정했는지를 /root/polscope/07-builtin.txt<파일이름> <낱말> 네 줄로 적으세요 — 낱말은 VAP(ValidatingAdmissionPolicy 가 거부), PSA(PodSecurity 가 거부), WEBHOOK(웹훅 호출 실패), PASS(통과) 중 하나이고, 대상은 pod.yaml·pod-host.yaml·pod-both.yaml·pod-owned.yaml 입니다.
  8. /root/polscope/blast-radius.sh 를 만드세요 — 클러스터의 모든 ValidatingWebhookConfiguration 을 훑어 웹훅 하나마다 객체 하나를 담은 JSON 배열을 표준 출력으로 냅니다. 키는 정확히 여덟 개입니다: config(설정 이름) · webhook(웹훅 이름) · failurePolicy(없으면 "Fail") · timeoutSeconds(없으면 10) · resources(그 웹훅의 모든 rules[].resources 를 중복 없이 정렬한 배열) · wildcard(resources"*" 가 있으면 참) · excludesKubeSystem(namespaceSelector.matchExpressions 에 키가 kubernetes.io/metadata.name, 연산자가 NotIn, 값에 kube-system 이 든 항목이 있으면 참) · risk. riskwildcard 가 참이고 failurePolicyFail 이고 excludesKubeSystem 이 거짓이면 "high", 그 밖에 failurePolicyFail 이면 "medium", 나머지는 "low" 입니다. 배열은 config · webhook 순으로 정렬합니다. 그 출력을 /root/polscope/blast-radius.json 에 저장하세요. 그리고 /root/polscope/risky.sh 를 만드세요 — riskhigh 인 것만 <config>/<webhook> 꼴로 한 줄씩 내고, 하나라도 있으면 종료 코드 1, 하나도 없으면 아무것도 내지 않고 0 으로 끝납니다. 두 스크립트 모두 저장된 파일이 아니라 지금 클러스터에 서 있는 오브젝트를 읽어야 합니다.

참고

웹훅 하나를 넓게 걸었더니 모든 쓰기가 2초씩 늦어졌다

/root/polscope 에서 작업합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). 먼저 네임스페이스 scope-app·scope-ops·scope-builtin 을 만들고, 세 곳 모두에 kubectl create sa default -n <네임스페이스>default 서비스 계정을 직접 만드세요(이 클러스터에는 컨트롤러 매니저가 없어 저절로 생기지 않습니다). /root/polscope/webhook-wide.yaml 에 ValidatingWebhookConfiguration scope-wide-audit 을 쓰세요 — 웹훅 이름은 wide.polscope.local, admissionReviewVersions["v1"], sideEffectsNone, failurePolicyIgnore, timeoutSeconds2, clientConfig.urlhttps://192.0.2.77:8443/validate 입니다. rules 는 가장 넓게 — apiGroups: ["*"], apiVersions: ["*"], operations: ["CREATE", "UPDATE"], resources: ["*"], scope: "*" 로 두고 셀렉터는 하나도 두지 않습니다. 시험용 매니페스트 셋도 만드세요 — /root/polscope/pod.yaml(파드 probe-app, 컨테이너 web, 이미지 registry.internal/app:1.0), /root/polscope/pod-guard.yaml(파드 probe-guard, 라벨 polscope.io/guard: "on", 같은 컨테이너), /root/polscope/cm.yaml(ConfigMap probe-cm, 데이터 a: b). 적용한 뒤 네 가지 요청의 소요 시간을 재세요 — kubectl create -n scope-app -f pod.yaml --dry-run=server, kubectl create -n scope-app -f cm.yaml --dry-run=server, kubectl create ns probe-ns --dry-run=server, kubectl get pods -n scope-app. 1초 이상 걸렸으면 SLOW, 아니면 FAST/root/polscope/01-reach.txtpod-create·configmap-create·namespace-create·pod-list 네 줄을 <이름> <낱말> 꼴로 적으세요.

웹훅의 주소가 192.0.2.0/24(TEST-NET-1) 이면 연결이 거부되지도 않고 응답도 오지 않습니다. API 서버는 timeoutSeconds 만큼 기다렸다가 failurePolicy 로 넘어가므로, Ignore 로 두면 요청은 전부 통과하지만 걸린 시간이 그 요청이 범위 안에 있었다는 증거가 됩니다. 범위 밖 요청은 기다림 없이 끝납니다. 어드미션은 쓰기에만 걸립니다 — 읽기 요청은 웹훅을 아예 지나지 않습니다. 시간은 s=$(date +%s%N)e=$(date +%s%N) 사이를 빼면 나노초로 잽니다. kubectl create -f <파일> --dry-run=server 는 어드미션을 그대로 태우되 오브젝트를 남기지 않습니다. 웹훅 호출도 거부 메시지도 진짜 요청과 똑같이 나옵니다.

고장 모드만 Fail 로 바꿨더니 그 범위의 쓰기가 전부 멈췄다

/root/polscope/webhook-strict.yaml 에 두 번째 설정 scope-strict 를 쓰세요 — 웹훅 이름은 strict.polscope.local, rulesscope-wide-audit똑같이 가장 넓게, 다른 것은 셋뿐입니다: failurePolicyFail, timeoutSeconds3, clientConfig.url 은 연결이 거부되는 주소 https://127.0.0.1:19443/validate 입니다. scope-wide-audit 은 지우지 말고 그대로 둡니다 — 같은 범위에 고장 모드만 다른 둘이 나란히 서 있는 것이 이 실습의 대조군입니다. 적용한 뒤 네 가지를 시도해 막히는지 보고, 막히면 BLOCK 통과하면 PASS/root/polscope/02-blocked.txt<이름> <낱말> 네 줄을 적으세요 — pod-create-scope-app(kubectl create -n scope-app -f pod.yaml --dry-run=server), configmap-kube-system(kubectl create -n kube-system -f cm.yaml --dry-run=server), namespace-create(kubectl create ns probe-ns --dry-run=server), webhookconfig-edit(kubectl apply -f webhook-strict.yaml 을 한 번 더).

연결이 거부되는 주소는 기다림 없이 즉시 실패로 돌아옵니다. Fail 은 그 실패를 거부로 옮기고 Ignore 는 통과로 옮깁니다 — 같은 범위, 같은 죽은 서버인데 답이 반대입니다. 이것이 failurePolicy 가 맞바꾸는 것입니다. 네 번째 줄이 이 단계의 핵심입니다. 자원 하나가 어드미션 웹훅을 타지 않는다면 무엇이 막혀도 그것만은 고칠 수 있습니다 — 웹훅 설정을 고치는 요청이 그 웹훅에게 물어보러 간다면 아무도 클러스터를 되살릴 수 없을 테니까요. 결과를 예측하지 말고 네 가지를 직접 돌려 보세요.

라벨로 빼려 했더니 라벨 붙이는 일까지 막혀 있었다

먼저 면제 라벨로 빠져나가 보세요 — kubectl label ns kube-system polscope.io/admission=exempt --overwrite 를 돌려 그 출력을 표준 오류까지 함께 /root/polscope/03-lockout.txt 에 저장하세요(막힙니다). 그다음 /root/polscope/webhook-strict.yamlscope-strictnamespaceSelector.matchExpressions 두 개를 더해 다시 적용하세요 — (1) 키 kubernetes.io/metadata.name, 연산자 NotIn, 값 ["kube-system", "kube-node-lease", "kube-public"], (2) 키 polscope.io/admission, 연산자 NotIn, 값 ["exempt"]. 이제 라벨이 붙습니다 — kube-systemscope-opspolscope.io/admission=exempt 를 거세요. 마지막으로 /root/polscope/pod-guard.yaml 을 세 곳에 --dry-run=server 로 보내 /root/polscope/03-escape.txtkube-system·scope-ops·scope-app 세 줄을 <네임스페이스> <BLOCK|PASS> 꼴로 적으세요.

두 조건은 같은 일을 하는 두 가지 방법입니다. 첫째는 API 서버가 1.21부터 모든 네임스페이스에 자동으로 붙여 주는 kubernetes.io/metadata.name 라벨을 쓰는 것이라 아무 준비가 필요 없고, 둘째는 면제 목록을 직접 붙인 라벨로 관리하는 것이라 나중에 네임스페이스를 늘릴 때 웹훅 설정을 고치지 않아도 됩니다. 그런데 이미 막힌 클러스터에서는 둘째를 쓸 수 없습니다 — 라벨을 붙이는 것이 네임스페이스 UPDATE 이고, 그 요청이 바로 막혀 있으니까요. 그래서 순서가 정해집니다: 먼저 첫째 방법으로 탈출구를 내고, 그다음에 둘째 방법을 얹습니다. namespaceSelector 의 여러 matchExpressions 는 모두 만족해야 범위 안입니다.

범위를 좁히자 걸리던 넷 중 셋이 웹훅을 지나가지 않게 됐다

/root/polscope/webhook-strict.yamlscope-strict 를 두 층에서 좁히세요 — rules 는 코어 그룹("")의 v1 pods 에 대한 CREATE·UPDATE 만, scopeNamespaced 로 바꾸고, objectSelector.matchLabelspolscope.io/guard: "on" 라벨이 붙은 객체만 보게 합니다. 3단계의 namespaceSelector 는 그대로 둡니다. 적용한 뒤 네 가지 요청을 다시 보내 /root/polscope/04-narrow.txt<이름> <좁히기전> <좁힌뒤> 네 줄을 적으세요 (좁히기 전 값은 2단계에서 본 것입니다) — pod-guard-scope-app(pod-guard.yamlscope-app 에), pod-plain-scope-app(pod.yamlscope-app 에), configmap-scope-app(cm.yamlscope-app 에), namespace-create(kubectl create ns probe-ns --dry-run=server). 판정은 BLOCK 또는 PASS 입니다.

범위를 좁히는 일은 보안을 줄이는 일이 아니라 묻는 횟수를 줄이는 일입니다. rules 에서 걸러진 요청은 API 서버가 이 웹훅을 떠올리지도 않고, namespaceSelectorobjectSelector 가 그다음입니다. 와일드카드로 받아 놓고 웹훅 서버 안에서 걸러 내면 결과는 같지만 비용은 전부 치릅니다. scopeNamespaced·Cluster·* 중 하나이고, 네임스페이스 범위 자원만 보겠다고 적으면 클러스터 범위 자원의 요청은 아예 오지 않습니다. objectSelector 는 요청 안의 객체 라벨을 보고, namespaceSelector 는 그 객체가 든 네임스페이스의 라벨을 봅니다 — 서로 다른 층입니다.

컨트롤러와 당번만 지나가는 문을 낸다

/root/polscope/webhook-strict.yamlscope-strictmatchConditions 두 개를 더해 다시 적용하세요 — skip-kube-system-sa 는 요청자가 system:serviceaccount:kube-system: 으로 시작하는 서비스 계정이면 웹훅을 부르지 않게 하고, skip-break-glass 는 요청자의 그룹에 polscope:break-glass 가 있으면 부르지 않게 합니다. 적용한 뒤 같은 /root/polscope/pod-guard.yamlscope-app 에 세 가지 주체로 보내 /root/polscope/05-conditions.txtkube-system-sa·break-glass·normal 세 줄을 <이름> <BLOCK|PASS> 꼴로 적으세요 — kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n scope-app -f pod-guard.yaml --dry-run=server, kubectl --as=oncall --as-group=polscope:break-glass --as-group=system:masters create -n scope-app -f pod-guard.yaml --dry-run=server, 그리고 지금 계정 그대로 한 번.

범위(rules·셀렉터)와 조건(matchConditions)은 다른 층입니다. 셀렉터는 무엇에 대한 요청인가만 보지만, 조건은 CEL 로 요청 전체를 봅니다 — 누가 보냈는지(request.userInfo), 어떤 동사인지, 어떤 네임스페이스인지까지. 대신 조건은 셀렉터보다 뒤에서 평가되므로, 셀렉터로 걸러 낼 수 있는 것을 조건으로 미루면 비용만 늡니다. 조건이 모두 참일 때 웹훅을 부르므로 '건너뛰고 싶다' 는 부정으로 적습니다. --as 로 다른 주체인 척할 수 있는데, 흉내 낸 주체의 권한만 갖게 되므로 인가까지 통과하려면 --as-group=system:masters 를 함께 줍니다 (인가는 어드미션보다 앞에 섭니다). 컨트롤러가 보내는 요청까지 막으면 클러스터가 스스로를 복구하지 못하게 되고, 당번을 위한 문이 없으면 장애 때 웹훅 설정을 지우는 것 말고는 길이 없습니다.

타임아웃을 6초로 두었더니 장애가 요청마다 6초씩 길어졌다

/root/polscope/webhook-slow.yaml 에 세 번째 설정 scope-slow 를 쓰세요 — 웹훅 이름 slow.polscope.local, clientConfig.url 은 응답이 아예 오지 않는 주소 https://192.0.2.77:8443/validate, sideEffectsNone, namespaceSelector.matchExpressions 는 키 kubernetes.io/metadata.nameIn ["scope-ops"] 로, objectSelector.matchLabelspolscope.io/slow: "on", rules 는 코어 그룹의 v1 podsCREATE·UPDATE(scope: "Namespaced"), 최종 상태는 failurePolicy: FailtimeoutSeconds: 3 입니다. /root/polscope/pod-slow.yaml 에 파드 probe-slow(라벨 polscope.io/slow: "on", 컨테이너 web, 이미지 registry.internal/app:1.0)를 만드세요. 그리고 /root/polscope/timeout-probe.sh <Fail|Ignore> <초> 를 만드세요 — scope-slow 를 그 두 값으로 잠시 바꾸고, scope-opspod-slow.yaml--dry-run=server 로 한 번 보내 걸린 시간을 재고, 원래 값으로 반드시 되돌린 뒤, BLOCK <초> 또는 PASS <초> 한 줄만 출력합니다(초는 반올림한 정수). 이 스크립트로 세 번 재어 /root/polscope/06-timeout.txt<failurePolicy> <타임아웃> <BLOCK|PASS> <걸린초> 세 줄을 적으세요 — Fail 6, Ignore 6, Fail 4 로 각각 한 번씩입니다.

timeoutSeconds 는 '웹훅 서버가 정상일 때 걸리는 시간보다 조금 길게' 정하는 값입니다. 넉넉하게 잡으면 장애 때 API 서버가 요청마다 그만큼 붙잡히고, 짧게 잡으면 느린 응답이 실패로 처리되어 failurePolicy 로 넘어갑니다. 그래서 두 손잡이는 따로 고르는 값이 아닙니다 — Fail 은 타임아웃이 길수록 장애가 길어지고, Ignore 는 타임아웃이 길수록 통과는 하되 느려집니다. 어느 쪽도 공짜가 아닙니다. 스크립트에는 trap ... EXIT 로 되돌리기를 걸어 두세요. 중간에 죽어도 웹훅이 이상한 값으로 남으면 안 됩니다. 설정을 바꾼 뒤 한두 번의 왕복만큼 늦게 반영되니 재기 전에 잠시 기다립니다. 이 클러스터에는 1단계의 2초짜리 감사 웹훅이 계속 살아 있고 웹훅은 병렬로 호출되므로, 걸린 시간은 둘 중 큰 쪽으로 나옵니다.

웹훅이 전부 죽은 클러스터에서 내장 정책만 판정을 내놓았다

웹훅 서버가 없는 지금 이대로, 같은 자리에 내장 정책을 세워 비교합니다. 네임스페이스 scope-builtin 에 라벨 pod-security.kubernetes.io/enforce=baseline 을 거세요. /root/polscope/vap-owner.yaml 에 ValidatingAdmissionPolicy polscope-require-owner(코어 그룹 v1 podsCREATE·UPDATE 를 잡고, 파드 메타데이터에 owner 라벨이 있을 것을 요구)와 같은 이름의 ValidatingAdmissionPolicyBinding(validationActions["Deny"], matchResources.namespaceSelectorkubernetes.io/metadata.nameIn ["scope-builtin"])을 함께 쓰고 적용하세요. 시험용 파드 셋을 더 만드세요 — /root/polscope/pod-owned.yaml(파드 probe-owned, 라벨 owner: platform), /root/polscope/pod-host.yaml(파드 probe-host, 같은 라벨에 spec.hostNetwork: true), /root/polscope/pod-both.yaml(파드 probe-both, 라벨 owner: platformpolscope.io/guard: "on"). 컨테이너는 셋 다 web/registry.internal/app:1.0 입니다. 네 매니페스트를 scope-builtin--dry-run=server 로 보내 무엇이 판정했는지를 /root/polscope/07-builtin.txt<파일이름> <낱말> 네 줄로 적으세요 — 낱말은 VAP(ValidatingAdmissionPolicy 가 거부), PSA(PodSecurity 가 거부), WEBHOOK(웹훅 호출 실패), PASS(통과) 중 하나이고, 대상은 pod.yaml·pod-host.yaml·pod-both.yaml·pod-owned.yaml 입니다.

VAP 는 API 서버 프로세스 안에서 CEL 을 평가하고 PSA 는 내장 어드미션 플러그인입니다. 둘 다 네트워크 왕복도 인증서도 엔진 파드도 없어서, 웹훅이 전부 죽은 지금도 평소와 똑같이 판정합니다. 고장은 '식 평가 오류' 나 '잘못 붙인 라벨' 같은 국소적 형태로만 옵니다. 그래서 실무의 배치는 엔진 없이 표현되는 검증은 내장 기구로 내려 보내 고장 표면을 줄이고, 꼭 필요한 것만 웹훅에 남긴다 가 됩니다. 판정 순서도 눈여겨보세요 — 내장 어드미션이 앞에 서므로, 내장 정책에서 이미 거부된 요청은 웹훅까지 가지도 않습니다. 그래서 웹훅의 고장을 보려면 내장 정책을 모두 통과하는 파드를 보내야 합니다. baseline 수준이 무엇을 막는지는 거부 메시지가 직접 알려 줍니다.

이것이 죽으면 무엇이 멈추는가를 한 장으로 답한다

/root/polscope/blast-radius.sh 를 만드세요 — 클러스터의 모든 ValidatingWebhookConfiguration 을 훑어 웹훅 하나마다 객체 하나를 담은 JSON 배열을 표준 출력으로 냅니다. 키는 정확히 여덟 개입니다: config(설정 이름) · webhook(웹훅 이름) · failurePolicy(없으면 "Fail") · timeoutSeconds(없으면 10) · resources(그 웹훅의 모든 rules[].resources 를 중복 없이 정렬한 배열) · wildcard(resources"*" 가 있으면 참) · excludesKubeSystem(namespaceSelector.matchExpressions 에 키가 kubernetes.io/metadata.name, 연산자가 NotIn, 값에 kube-system 이 든 항목이 있으면 참) · risk. riskwildcard 가 참이고 failurePolicyFail 이고 excludesKubeSystem 이 거짓이면 "high", 그 밖에 failurePolicyFail 이면 "medium", 나머지는 "low" 입니다. 배열은 config · webhook 순으로 정렬합니다. 그 출력을 /root/polscope/blast-radius.json 에 저장하세요. 그리고 /root/polscope/risky.sh 를 만드세요 — riskhigh 인 것만 <config>/<webhook> 꼴로 한 줄씩 내고, 하나라도 있으면 종료 코드 1, 하나도 없으면 아무것도 내지 않고 0 으로 끝납니다. 두 스크립트 모두 저장된 파일이 아니라 지금 클러스터에 서 있는 오브젝트를 읽어야 합니다.

이 보고서가 답하는 질문은 하나입니다 — 이 웹훅 서버가 죽으면 클러스터에서 무엇이 멈추는가. failurePolicy: Fail 이면 그 범위의 쓰기가 멈추고, 그 범위가 resources 와 셀렉터입니다. 그래서 위험한 조합은 언제나 같습니다: 넓은 범위 + Fail + 시스템 네임스페이스 미제외. 셋이 동시에 맞으면 엔진을 되살리려는 요청까지 막혀 클러스터가 스스로 복구하지 못합니다. kubectl get validatingwebhookconfigurations -o json.items[].webhooks[] 를 jq 로 펼치면 한 번에 만들 수 있습니다. jq// 는 값이 false 일 때도 오른쪽으로 새므로, 기본값은 has("키") 로 있는지 먼저 물어 채우세요. 채점기는 위험한 설정을 하나 잠시 세워 두고 risky.sh 를 다시 부릅니다 — 이름을 외워 둔 답은 그때 떨어집니다.