LabHub

KCA — Kyverno 인증 어소시에이트 · validate 규칙 · 이론

앵커 네 개와, 정책을 쪼개지 않고 점진 도입하는 법

LabHub 에서 이어서 보기

한 줄 요약

validate 규칙은 세 가지 문법을 갖는다. 오브젝트 모양을 그대로 그리는 pattern, 조건식으로 거부하는 deny.conditions, 그리고 CEL 표현식이다. 여기에 강제 수준을 규칙 단위로 정하는 failureAction 이 붙으면서, 정책을 둘로 쪼개지 않고도 새 규칙만 관찰 모드로 얹는 흐름이 가능해졌다.

왜 이게 필요했나

정책 언어를 새로 배우게 하지 않겠다는 것이 Kyverno 의 출발점이다. OPA/Gatekeeper 는 Rego 를 익혀야 하고 ConstraintTemplate 과 Constraint 두 오브젝트를 짝지어야 한다. Kyverno 는 검사하려는 오브젝트를 그대로 YAML 로 그리고 값 자리에 연산자를 넣는다. Pod 의 컨테이너에 메모리 리밋이 있어야 한다면, Pod 스펙 모양 그대로 쓰고 값 자리에 "비어 있지 않음" 을 뜻하는 연산자를 넣는 식이다.

그런데 모양을 그대로 그리는 방식에는 한계가 있다. "이 필드가 있으면 저 조건도 봐야 한다" 나 "이 키는 아예 없어야 한다" 같은 관계는 값으로 표현되지 않는다. 그래서 키 이름에 붙이는 앵커가 생겼다.

어떻게 동작하나

값 자리 연산자는 다음과 같다. ?* 는 비어 있지 않은 값, * 는 null 을 포함한 모든 값, X|Y 는 둘 중 하나, !X 는 X 가 아닌 값, 그리고 >=256Mi 처럼 수치 비교가 가능하다.

키 자리 앵커는 네 종류이고, 여기서 사고가 가장 많이 난다.

| 앵커 | 이름 | 뜻 |
| --- | --- | --- |
| () | 조건부 | 이 키가 이 값과 맞을 때만 나머지를 검사한다 |
| =() | 동등 | 이 키가 존재하면 값이 조건을 만족해야 한다 |
| ^() | 존재 | 배열 안에 조건을 만족하는 원소가 최소 하나 있어야 한다 |
| X() | 부정 | 이 키가 존재하면 안 된다 |

가장 흔한 오용이 부정 앵커다. X(privileged) 를 "privileged 값이 달라야 한다" 로 읽는 사람이 많은데, 정확한 뜻은 "privileged 라는 키 자체가 없어야 한다" 이다. 값이 false 로 명시돼 있어도 키가 있으면 걸린다. 값을 비교하고 싶으면 앵커가 아니라 값 연산자나 deny 조건을 써야 한다.

foreach 는 컬렉션의 각 원소를 검사한다. 여기서도 함정이 하나 있는데, foreach.list 는 JMESPath 표현식 자체를 받으므로 중괄호로 감싸면 안 된다. 그리고 initContainers 까지 함께 봐야 할 때 쓰는 관용구가 request.object.spec.[initContainers, containers][] 다. initContainers 를 빼먹은 정책은 실제로 아주 흔하고, 공격자가 아니라 그냥 초기화 컨테이너를 쓰는 평범한 팀이 정책을 우회하게 된다.

deny.conditions 는 조건이 참일 때 거부한다. all 과 any 로 묶고 각 조건은 key, operator, value 세 칸이다. pattern 으로 표현이 어색한 것 대부분이 여기로 온다. CEL 은 쿠버네티스 1.25 이후 표준이 된 표현식 언어이고, Kyverno 에도 validate.cel 로 들어와 있다. 다만 이미 Kyverno 를 운영 중일 때 유용한 선택지이지 Kyverno 를 도입할 이유는 되지 못한다. 필드 하나를 검사하는 것이 전부라면 쿠버네티스 내장 ValidatingAdmissionPolicy 로 충분하고, 컴포넌트를 하나 덜 운영한다는 것은 업그레이드 대상이 하나 줄고 웹훅 인증서 갱신을 신경 쓸 일이 하나 준다는 뜻이다.

마지막이 강제 수준이다. 예전에는 spec.validationFailureAction 이 정책 전체의 강제 수준을 정했다. 그래서 한 정책 안의 어떤 규칙은 확신이 서서 막고 싶고 다른 규칙은 아직 관찰만 하고 싶으면 정책을 둘로 쪼개야 했고, match 블록이 복제되면서 관리 대상이 늘었다. 지금은 spec.rules[*].validate[*].failureAction 으로 내려왔고 값은 Enforce 와 Audit 둘이다. 지정하지 않으면 기본은 Audit 이다. 새 규칙을 기존 정책에 Audit 으로 얹어 며칠 리포트를 보고 그 규칙만 Enforce 로 올리는 흐름이, 정책을 쪼개지 않고 가능해진 것이 이 변경의 값어치다. 같이 움직인 필드로 webhookTimeoutSecondsfailurePolicy 가 있고 각각 webhookConfiguration.timeoutSeconds, webhookConfiguration.failurePolicy 로 옮겨가며 1.13부터 deprecated 로 표시된다.

현장에서 만나는 모습

저자의 이미지 스캔 경험이 이 주제와 정확히 맞물린다. 어떤 이미지를 처음 스캔했을 때 총 1,247건이 나왔고 그중 Critical 이 9건이었다. 이 리포트를 그대로 팀 채널에 붙이면 결과는 둘 중 하나다. 모두가 무시하거나, 아무도 손대지 못한 채 릴리스가 막힌다. 수정본이 있는 것만 남기는 옵션 하나를 붙였더니 4건이 됐고, 실제 악용이 확인된 목록과 대조하니 오늘 처리해야 할 것은 1건이었다.

정책도 똑같다. 처음부터 Enforce 로 전면 적용하면 배포가 통째로 막히고, 결국 정책이 아니라 정책을 만든 사람이 병목이 된다. 그렇다고 전부 Audit 으로 두면 아무도 리포트를 보지 않는다. 답은 규칙 단위 failureAction 이다. 확실한 규칙 몇 개만 Enforce 로 올려 두고 나머지는 Audit 으로 관찰하면, 지금 조치할 수 있는 항목만 사람 앞에 남는다. 스캔 리포트에서 배운 것과 같은 원리다.

다음 실습에서 할 것

/root/kca-validate/ 에 ClusterPolicy 를 쓰면서 match·pattern·deny·exclude 를 한 조각씩 채우고, 규칙마다 다른 failureAction 을 준다. 그리고 같은 규칙을 쿠버네티스 내장 ValidatingAdmissionPolicy 와 Binding 으로 실제 클러스터에 올려, 정책 엔진 없이 어디까지 되는지를 눈으로 비교한다.