KCA — Kyverno 인증 어소시에이트 · 웹훅 장애를 구분하고 복구하기 · 이론
정책은 남았는데 웹훅이 사라졌다
한 줄 요약
정책 파일이 남아 있다는 것, API 서버에 웹훅이 등록돼 있다는 것, 그 웹훅이 응답한다는 것은 서로 다른 사실입니다. 웹훅 장애 실험에서는 셋을 따로 관측해야 합니다.
왜 이게 필요했나
배포 담당자가 이렇게 말합니다. “보안 정책을 Fail로 두었는데 엔진을 꺼도 위반 Pod가 만들어졌어요.” 곧바로 Kubernetes 버그라고 결론내릴 수 있을까요? 컨트롤러를 정상 종료하는 과정에서 호출 등록이 사라졌을 수도 있습니다. 호출할 웹훅 자체가 없으면, 그 웹훅의 호출 실패를 어떻게 처리할지 정한 failurePolicy를 시험한 것이 아닙니다.
2026-09-12 LabHub의 전용 검증 VM에서도 이 상황을 관측했습니다. 컨트롤러를 replicas=0으로 줄이고 Pod가 사라진 것을 확인했는데 요청이 통과했습니다. 당시 저장한 ValidatingWebhookConfiguration 목록에는 해당 리소스 웹훅이 없었습니다. 이를 “Fail이 고장 났다”는 증거로 쓰지 않았습니다. 이후 호출 등록을 남긴 채 같은 컨트롤러 프로세스의 응답만 짧게 멈추자, 매칭되는 요청이 3초 뒤 시간 초과로 거절됐습니다. 정상 종료와 응답 중단은 같은 실험이 아닙니다.
어떻게 동작하나
이 단원은 Kyverno v1.19.1과 k3s v1.35.8+k3s1에서 재현한 현상을 다룹니다. 다른 버전이나 설치 옵션에서도 정상 종료 시 똑같이 웹훅이 사라진다고 일반화하지 않습니다. 실험을 옮길 때는 설치 버전과 실제 등록을 다시 확인해야 합니다.
먼저 세 가지 설정을 나눕니다.
- validationActions의 Deny: 검증 결과가 위반일 때 요청을 거절하는 동작입니다.
- failurePolicy의 Fail·Ignore: 정책을 정상적으로 평가하지 못하는 오류를 어떻게 다룰지 정합니다. 여기서는 실제 웹훅 호출의 시간 초과를 관측합니다.
- webhookConfiguration.timeoutSeconds: 호출을 기다리는 제한입니다. 정상 위반 응답을 기다렸다가 허용으로 바꾸는 타이머가 아닙니다.
Ignore는 “검증하지 않는다”가 아닙니다. 서버가 정상적으로 위반을 판단해 거부 응답을 보냈다면 그 거부는 유지됩니다. 따라서 Ignore 실험도 먼저 정상 서버에 위반 요청을 보내 거부되는지 확인해야 합니다. 이 기준선 없이 장애 중 성공만 보면 정책이 애초에 적용되지 않았던 것과 구별할 수 없습니다.
[Kyverno ValidatingPolicy 공식 설명](https://kyverno.io/docs/policy-types/validating-policy/)에서 정책 필드와 자동 생성 동작을 확인할 수 있습니다. 이 API는 policies.kyverno.io/v1입니다. 구식 ClusterPolicy 예제의 필드 위치를 그대로 옮기지 말고, 설치된 CRD와 kubectl explain으로 대조하세요. Kubernetes 자체 ValidatingAdmissionPolicy로 실행하는 경로도 별개이므로, 엔진 프로세스 정지 실험에 섞지 않습니다.
네임스페이스가 곧 실험 범위다
대상 네임스페이스에는 environment 라벨이 필요한 Pod 생성 규칙을 적용하고, 대조 네임스페이스는 그 선택 범위 밖에 둡니다. namespaceSelector는 Pod의 라벨이 아니라 네임스페이스의 라벨을 고릅니다. Pod에 붙이는 environment와 네임스페이스를 고르는 kubernetes.io/metadata.name은 서로 다른 층입니다.
정책 선언만 읽으면 충분하지 않습니다. 생성된 웹훅에서 실제 service 경로, failurePolicy, timeoutSeconds, namespaceSelector, CREATE pods 규칙을 확인합니다. 추가 objectSelector나 matchConditions가 있으면 요청이 검증을 우회할 수 있으므로 함께 봐야 합니다. 범위 밖의 정상 동작은 대조군이지, 범위 안의 장애가 없었다는 증거가 아닙니다.
[Kubernetes 동적 어드미션 공식 문서](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)는 요청 매칭과 웹훅 호출 실패 처리를 구분합니다. “모든 생성 요청이 막힌다” 대신 “그 웹훅에 매칭되는 요청이 호출 실패 때문에 막힌다”라고 설명해야 정확합니다.
같은 요청 두 개와 대조군 하나
실험마다 새 Pod 이름을 사용합니다. 이미 존재하는 이름을 재사용하면 AlreadyExists가 발생해 정책의 거부나 시간 초과와 섞입니다. API 응답뿐 아니라 실제 저장 여부와 UID도 조회해서 남깁니다. 성공 문구 한 줄만으로 저장을 추정하지 마세요.
| 조건 | 대상의 정상 라벨 요청 | 대상의 위반 요청 | 범위 밖 위반 요청 |
| --- | --- | --- | --- |
| 웹훅 정상, Deny | 저장 | 명시적 정책 거부 | 별도 범위 검사 |
| 등록 보존·응답 중단, Fail | 시간 초과·미저장 | 시간 초과·미저장 | 저장 |
| 등록 보존·응답 중단, Ignore | 제한 뒤 저장 | 제한 뒤 저장 | 저장 |
| 웹훅 복구 | 기존 Pod를 별도 확인 | 다시 명시적 거부 | 복구 범위에 맞춰 검사 |
표의 장애 두 행은 위 전용 VM에서 관측한 결과입니다. 요청마다 실제 오류 원문을 남겼습니다. Fail의 오류는 해당 웹훅을 호출하다 context deadline exceeded가 발생한 것이고, 정상 위반의 오류는 해당 정책이 denied the request를 반환한 것입니다. 둘 다 터미널에서는 실패처럼 보이지만 원인은 다릅니다. DNS 오류나 connection refused는 또 다른 고장 유형이며, 응답만 멈추는 이번 실험의 성공으로 대신 세지 않습니다.
이미 실행 중인 Pod가 남는 이유
어드미션은 요청이 저장되기 전의 관문입니다. 이번 규칙의 대상은 CREATE pods이고 백그라운드 평가는 꺼 둡니다. 앞서 허용돼 실행 중인 Pod를 자동으로 삭제하는 컨트롤러를 만든 것이 아닙니다. 따라서 기존 Pod의 같은 UID와 Running을 별도 관측합니다. 같은 이름으로 새로 만든 Pod를 “살아남았다”고 세면 안 됩니다.
현장에서 만나는 모습
금요일 배포 직전 보안 엔진의 응답이 느려졌다고 가정해 봅시다. Ignore로 바꾸면 배포 가용성은 나아질 수 있지만, 평가되지 않은 변경을 받아들이는 위험이 생깁니다. Fail을 유지하면 해당 요청은 멈출 수 있지만, 검증 없이 저장되는 경로를 닫습니다. 어느 쪽이든 목적·범위·장애 허용 시간을 먼저 정해야 합니다. “대부분 Ignore가 낫다”는 한 문장으로 결정할 일이 아닙니다.
[Kubernetes 웹훅 설계 지침](https://kubernetes.io/docs/concepts/cluster-administration/admission-webhooks-good-practices/#fail-open-and-validate-the-final-state)의 fail-open 권고는 변형 웹훅과 최종 상태 검증을 함께 보는 문맥입니다. 이를 모든 보안 검증 웹훅에 대한 무조건적 Ignore 권고로 읽지 마세요. 고가용성·호출 범위 축소·충분한 자원·장애 알림을 함께 설계해야 합니다.
replicas 복구와 정책 복구는 같은 시각이 아니다
컨트롤러를 다시 1개로 늘렸다고 즉시 검증이 돌아오는 것은 아닙니다. 새 Pod 기동, 리더 인수, 웹훅 등록, 실제 정책 응답을 나누어 확인해야 합니다. 이 단원의 첫 학생 경로 시험에서는 23:49:53 UTC에 replicas를 복구했지만 새 프로세스가 리더 Lease를 얻은 것은 23:50:27이었습니다. 20초짜리 등록 대기는 그 전에 끝났습니다. 이는 Fail의 정책 위반 판정이 아니라 실습 실행기의 준비 대기 실패였습니다.
[Kyverno v1.19.1의 리더 선출 구현](https://github.com/kyverno/kyverno/blob/v1.19.1/pkg/leaderelection/leaderelection.go)은 Lease를 사용하며 종료 시 즉시 반납하지 않도록 설정합니다. 실제 대기 시간은 설치·부하에 따라 달라집니다. 관측한 34초를 모든 환경의 상수로 쓰지 말고 로그와 등록 상태를 확인하세요. 실습은 복구 중 대기에 실패하더라도 축소 중 관측한 정책·등록·요청 결과를 보존합니다. 재시도 버튼을 누르기 전에 무엇이 복구됐고 무엇이 아직 확인되지 않았는지부터 구분해야 합니다.
장애를 만드는 도구에도 복구 설계가 필요하다
일시 정지는 학생 전용 VM 내부의 단일 Kyverno 컨테이너에만 적용합니다. 컨테이너 ID·Pod UID·PID 시작 시각을 확인하고, Linux pidfd에 신호를 묶어 숫자 PID 재사용으로 다른 프로세스를 건드리지 않게 합니다. 별도 감시자가 20초 이내 자동 재개하며, 정지와 복구를 다음 단계 클릭 사이에 나누지 않습니다. 감시자는 실험 실행기가 강제 종료되거나 준비 응답 전에 사라진 경로에서도 대상 재개를 시도합니다.
이 장치는 정상 종료 절차, 고가용성 시험, 운영 장애 훈련을 모두 대신하지 않습니다. 감시자 자체까지 죽이거나 VM을 종료하는 상황은 별도 실패 영역입니다. 채점기는 장애를 다시 만들지 않고 보존한 결과를 읽습니다. 파일 기반 채점은 악성 root의 모든 증거 위조를 막는 보안 인증도 아닙니다. 학습에서 관측 근거가 비거나 서로 모순되는 일을 잡는 데 사용합니다.
다음 실습에서 할 것
첫 실습은 정상 축소와 등록 부재를 다룹니다. 대상과 대조 범위를 기록하고 ValidatingPolicy를 직접 작성한 뒤, 정상 기준선과 정상 축소 때의 등록 상태를 관측합니다. 복구 요청·등록 확인·실제 거부 응답의 시각을 구분하고, 서버 dry-run으로 검증과 저장이 다른 단계임을 확인합니다.
두 번째 실습은 새 VM에서 독립적으로 시작합니다. 첫 실습의 파일은 필요하지 않습니다. 정책과 정상 기준선을 다시 만든 뒤 호출 등록을 유지한 짧은 응답 중단으로 Fail과 Ignore를 비교합니다. 두 편 모두 정책 존재·등록·응답·기존 Pod 생존을 나누어 보고서를 씁니다. 결론보다 먼저 원자료가 있어야 합니다.
dry-run도 어디에서 했는지 확인한다
Kubernetes의 [서버 dry-run 공식 설명](https://kubernetes.io/docs/reference/using-api/api-concepts/#dry-run)에 따르면 서버 dry-run은 변경을 저장하지 않으면서 해당 요청의 검증 경로를 실행합니다. 반면 client dry-run은 로컬에서 출력물을 구성하므로 서버 웹훅이 거절할지 입증하지 못합니다. 파일 문법을 확인하는 것, 서버가 요청을 받아들이는 것, 객체가 실제 저장되는 것은 서로 다른 검증입니다.
이 실습에서는 고유한 이름으로 정상·위반 요청을 각각 server dry-run하고 같은 이름의 Pod를 별도로 조회합니다. 정상 응답의 kind·이름·namespace·라벨이 맞고 저장 결과는 없어야 합니다. 위반 요청은 정책 표식이 있는 명시적 거부여야 합니다. 성공 출력에 Pod 객체가 보인다는 이유만으로 저장됐다고 기록하거나, AlreadyExists를 정책 거부로 기록하지 마세요. 요청을 받는 웹훅도 dry-run과 부작용 처리를 지원해야 하므로, 다른 설치에 옮길 때는 실제 웹훅의 sideEffects 설정을 함께 확인해야 합니다.