목록으로

Programming Notes

Kubernetes v1.36: 삭제가 불가능한 어드미션 정책

함대(fleet) 규모의 Kubernetes 클러스터 전반에 보안 정책을 적용해 보려 시도한 적이 있다면, 아마 좌절감을 주는 '닭이 먼저냐 달걀이 먼저냐' 하는 문제에 직면해 보셨을 것입니다. 어드미션 정책은 API 객체이므로 누군가 생성하기 전까지는 존재하지 않으며, 적절한 권한을 가진 사람이라면 누구나 삭제할 수 있습니다. 클러스터 부트스트랩 과정 중에는 정책이 아직 활성화되지 않은 공백기가 항상 존재하며, 권한이 있는 사용자가 정책을 제거하는 것을 막을 방법도 없었습니다.

Kubernetes v1.36에서는 이를 해결하기 위한 알파 기능인 **매니페스트 기반 어드미션 컨트롤(manifest-based admission control)**을 도입했습니다. 이 기능을 사용하면 어드미션 웹훅과 CEL 기반 정책을 디스크상의 파일로 정의할 수 있으며, API 서버는 어떤 요청을 처리하기 전인 시작 단계에서 이 파일들을 로드합니다.

우리가 해결하려는 간극

오늘날 대부분의 Kubernetes 정책 적용은 API를 통해 이루어집니다. ValidatingAdmissionPolicy나 웹훅 구성을 API 객체로 생성하면 어드미션 컨트롤러가 이를 감지합니다. 이 방식은 안정적인 상태(steady state)에서는 잘 작동하지만, 몇 가지 근본적인 한계가 있습니다.

클러스터 부트스트랩 중에는 API 서버가 요청을 받기 시작하는 시점과 정책이 생성되어 활성화되는 시점 사이에 간극이 존재합니다. 백업에서 복구하거나 etcd 장애를 복구하는 중이라면 이 간극은 상당히 커질 수 있습니다.

또한 '자가 보호(self-protection)' 문제도 있습니다. 어드미션 웹훅과 정책은 자기 자신의 구성 리소스에 대한 작업은 가로챌 수 없습니다. Kubernetes는 순환 의존성을 피하고자 ValidatingWebhookConfiguration 같은 유형에 대해서는 웹훅 호출을 건너뜁니다. 즉, 충분한 권한을 가진 사용자라면 핵심 어드미션 정책을 삭제할 수 있으며, 어드미션 체인 내에서는 이를 막을 방법이 없다는 뜻입니다.

Kubernetes SIG API Machinery 팀은 "이 정책들은 어떤 상황에서도 항상 켜져 있다"라고 단언할 수 있는 방법을 원했습니다.

작동 방식

이미 --admission-control-config-file을 통해 API 서버에 전달하고 있는 AdmissionConfiguration 파일에 staticManifestsDir 필드를 추가합니다. 해당 필드에 디렉터리를 지정하고 정책 YAML 파일들을 넣어두면, API 서버가 서비스를 시작하기 전에 이를 로드합니다.

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionPolicy
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: ValidatingAdmissionPolicyConfiguration
    staticManifestsDir: "/etc/kubernetes/admission/validating-policies/"

매니페스트 파일은 표준 Kubernetes 리소스 정의입니다. 유일한 요구 사항은 이 매니페스트가 정의하는 모든 객체의 이름이 반드시 .static.k8s.io로 끝나야 한다는 점입니다. 이 예약된 접미사는 API 기반 구성과의 충돌을 방지하며, 메트릭이나 감사 로그를 볼 때 어드미션 결정이 어디서 왔는지 쉽게 식별할 수 있게 해줍니다.

다음은 kube-system 이외의 네임스페이스에서 권한이 부여된(privileged) 컨테이너를 거부하는 전체 예시입니다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-privileged.static.k8s.io"
  annotations:
    kubernetes.io/description: "이 정책이 적용된 모든 곳에서 권한이 부여된 포드 실행을 거부합니다."
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  variables:
  - name: allContainers
    expression: >-
      object.spec.containers +
      (has(object.spec.initContainers) ? object.spec.initContainers : []) +
      (has(object.spec.ephemeralContainers) ? object.spec.ephemeralContainers : [])
  validations:
  - expression: >-
      !variables.allContainers.exists(c,
      has(c.securityContext) && has(c.securityContext.privileged) &&
      c.securityContext.privileged == true)
    message: "권한이 부여된 컨테이너는 허용되지 않습니다."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "deny-privileged-binding.static.k8s.io"
  annotations:
    kubernetes.io/description: "kube-system을 제외한 모든 네임스페이스에 deny-privileged 정책을 바인딩합니다."
spec:
  policyName: "deny-privileged.static.k8s.io"
  validationActions:
  - Deny
  matchResources:
    namespaceSelector:
      matchExpressions:
      - key: "kubernetes.io/metadata.name"
        operator: NotIn
        values: ["kube-system"]

이전에는 보호할 수 없었던 것들을 보호하기

우리가 가장 기대하는 부분은 어드미션 구성 리소스 자체에 대한 작업을 가로챌 수 있는 능력입니다.

API 기반 어드미션에서는 웹훅과 정책이 ValidatingAdmissionPolicyValidatingWebhookConfiguration 같은 유형에 대해 절대 호출되지 않습니다. 이 제한에는 합당한 이유가 있습니다. 만약 웹훅이 자기 자신의 구성을 변경하는 것을 거부할 수 있다면, API를 통해 이를 수정할 방법이 없는 채로 잠겨버릴 수 있기 때문입니다.

매니페스트 기반 정책은 이런 문제가 없습니다. 잘못된 정책이 차단해서는 안 될 것을 차단하고 있다면, 디스크의 파일을 수정하기만 하면 API 서버가 변경 사항을 반영합니다. 복구 경로가 API를 거치지 않기 때문에 순환 의존성이 발생하지 않습니다.

이는 곧 핵심 API 기반 어드미션 정책의 삭제를 방지하는 매니페스트 기반 정책을 작성할 수 있음을 의미합니다. 공유 클러스터를 관리하는 플랫폼 팀에게 이는 상당한 개선입니다. 이제 클러스터 관리자가 실수로든 의도적으로든 기본 보안 정책을 제거할 수 없음을 보장할 수 있습니다.

실제로 적용하면 다음과 같습니다. 이 정책은 platform.example.com/protected: "true" 레이블이 붙은 어드미션 리소스의 수정이나 삭제를 방지합니다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "protect-policies.static.k8s.io"
  annotations:
    kubernetes.io/description: "보호된 어드미션 리소스의 수정 또는 삭제를 방지합니다."
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: ["admissionregistration.k8s.io"]
      apiVersions: ["*"]
      operations: ["DELETE", "UPDATE"]
      resources:
      - "validatingadmissionpolicies"
      - "validatingadmissionpolicybindings"
      - "validatingwebhookconfigurations"
      - "mutatingwebhookconfigurations"
  validations:
  - expression: >-
      !has(oldObject.metadata.labels) ||
      !('platform.example.com/protected' in oldObject.metadata.labels) ||
      oldObject.metadata.labels['platform.example.com/protected'] != 'true'
    message: "보호된 어드미션 리소스는 수정하거나 삭제할 수 없습니다."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "protect-policies-binding.static.k8s.io"
  annotations:
    kubernetes.io/description: "모든 어드미션 리소스에 protect-policies 정책을 바인딩합니다."
spec:
  policyName: "protect-policies.static.k8s.io"
  validationActions:
  - Deny

이 정책이 설정되면, platform.example.com/protected: "true" 레이블이 지정된 모든 API 기반 어드미션 정책이나 웹훅 구성은 변조로부터 보호됩니다. 보호 로직 자체가 디스크에 존재하므로 API를 통해서는 제거할 수 없습니다.

알아두어야 할 몇 가지 사항

매니페스트 기반 구성은 의도적으로 자기 완결적(self-contained)입니다. API 리소스를 참조할 수 없으므로, 정책의 paramKind를 사용할 수 없고, 어드미션 웹훅에 대한 Service 참조도 불가능하며(URL만 가능), 바인딩은 동일한 매니페스트 세트 내의 정책만 참조할 수 있습니다. 이러한 제한은 etcd를 사용할 수 없는 시작 시점을 포함하여, 클러스터 상태 없이도 구성이 작동해야 하기 때문에 존재합니다.

여러 개의 API 서버 인스턴스를 실행하는 경우, 각 인스턴스는 독립적으로 자체 매니페스트 파일을 로드합니다. 서버 간의 동기화 기능은 내장되어 있지 않습니다. 이는 'at-rest' 암호화와 같은 다른 파일 기반 API 서버 설정 모델과 동일합니다. 이 기능이 활성화되면 Kubernetes는 관련 메트릭에 구성 해시를 레이블로 노출하므로, 설정 불일치(drift)를 감지할 수 있습니다.

파일은 런타임에 변경 사항이 감시(watch)되므로, 정책을 업데이트하기 위해 API 서버를 재시작할 필요가 없습니다. 매니페스트 파일을 업데이트하면 API 서버는 새 구성을 검증하고 원자적으로 교체합니다. 검증에 실패하면 이전의 정상적인 구성을 유지하고 오류를 기록합니다. 즉, Ansible, Puppet 또는 마운트된 ConfigMap과 같은 표준 구성 관리 도구를 사용하여 API 서버 중단 없이 전체 함대에 정책 변경 사항을 배포할 수 있습니다.

시작 시의 초기 로드는 더 엄격합니다. 매니페스트 중 하나라도 유효하지 않으면 API 서버가 시작되지 않습니다. 이는 의도된 설계입니다. 시작 단계에서는 예상된 정책 없이 실행되는 것보다 즉시 실패(fail fast)하는 것이 더 안전하기 때문입니다.

시도해 보기

Kubernetes v1.36에서 이 기능을 사용해 보려면 다음 단계를 따르세요.

  1. 각 kube-apiserver에서 ManifestBasedAdmissionControlConfig 피처 게이트를 활성화합니다.
  2. 정적 매니페스트 파일을 담을 디렉터리를 생성합니다. API 서버가 실행되는 포드에 해당 디렉터리를 마운트해야 한다면 마운트 설정을 추가합니다. 읽기 전용이어도 괜찮습니다.
  3. AdmissionConfiguration에서 staticManifestsDir에 해당 디렉터리 경로를 설정합니다.
  4. AdmissionConfiguration 파일을 가리키는 --admission-control-config-file 옵션과 함께 API 서버를 시작합니다.

전체 문서는 Manifest-Based Admission Control에서 확인할 수 있으며, 진행 상황은 KEP-5793에서 팔로우할 수 있습니다.

여러분의 피드백을 기다립니다. Kubernetes Slack의 #sig-api-machinery 채널을 통해 의견을 들려주세요 (초대가 필요하다면 https://slack.k8s.io/를 방문하세요).

참여 방법

이 기능이나 다른 SIG API Machinery 프로젝트에 기여하고 싶다면 Kubernetes Slack의 #sig-api-machinery에 참여해 주세요. 또한 격주 수요일마다 열리는 SIG API Machinery 회의에도 언제든지 참석하실 수 있습니다.