목록으로

Programming Notes

Kubernetes v1.36: 세분화된 Kubelet API 권한 부여(Authorization) 기능 GA 단계로 전환

Kubernetes SIG Auth 및 SIG Node를 대표하여, Kubernetes v1.36에서 세분화된(fine-grained) kubelet API 권한 부여 기능이 정식 출시(General Availability, GA)되었음을 알려드립니다!

KubeletFineGrainedAuthz 기능 게이트는 Kubernetes v1.32에서 선택 사양인 알파(alpha) 기능으로 도입되었으며, v1.33에서 베타(beta) 단계로 승급되어 기본적으로 활성화되었습니다. 이제 v1.36에서는 이 기능이 정식 버전이 되었으며 기능 게이트는 '활성화(enabled)' 상태로 고정됩니다. 이 기능을 통해 kubelet의 HTTPS API에 대해 보다 정밀한 최소 권한(least-privilege) 제어가 가능해졌으며, 일반적인 모니터링 및 관찰성(observability) 용도로 과도하게 넓은 권한이었던 nodes/proxy 권한을 부여할 필요가 없어졌습니다.

배경: nodes/proxy 문제

kubelet은 포드(pod) 목록, 노드 메트릭, 컨테이너 로그, 그리고 결정적으로 실행 중인 컨테이너 내부에서 명령을 실행할 수 있는 기능 등 다양한 민감도의 데이터를 제공하는 여러 API를 HTTPS 엔드포인트로 노출합니다.

이 기능이 도입되기 전, kubelet 권한 부여는 조대(coarse-grained) 모델을 사용했습니다. 웹훅(webhook) 권한 부여가 활성화되었을 때, 거의 모든 kubelet API 경로는 단일 nodes/proxy 하위 리소스(subresource)로 매핑되었습니다. 즉, kubelet에서 메트릭이나 상태 정보를 읽어야 하는 모든 워크로드는 nodes/proxy 권한이 필요했는데, 이 권한은 해당 노드에서 실행 중인 모든 컨테이너에 대해 임의의 명령을 실행할 수 있는 권한과 동일했습니다.

이것이 왜 문제인가요?

모니터링 에이전트, 로그 수집기 또는 상태 확인 도구에 nodes/proxy 권한을 부여하는 것은 최소 권한 원칙을 위반합니다. 만약 이러한 워크로드 중 하나라도 침해당한다면, 공격자는 해당 노드의 모든 컨테이너에서 명령을 실행할 수 있는 능력을 얻게 됩니다. nodes/proxy 권한은 사실상 노드 수준의 슈퍼유저(superuser) 권한이며, 이를 광범위하게 부여하는 것은 보안 사고 발생 시 피해 범위(blast radius)를 비약적으로 증가시킵니다.

이 문제는 커뮤니티 내에서 수년간 인지되어 왔으며(kubernetes/kubernetes#83465 참조), 이번 개선 사항인 KEP-2862를 추진하게 된 핵심 동기가 되었습니다.

nodes/proxy GET WebSocket RCE 위험성

상황은 처음 보는 것보다 훨씬 더 심각합니다. 보안 연구원들은 2026년 초에 모니터링 도구에 일상적으로 부여되는 최소한의 읽기 전용 권한인 nodes/proxy GET만으로도 접근 가능한 노드의 모든 포드에서 명령을 실행(RCE)하는 데 악용될 수 있음을 입증했습니다.

근본적인 원인은 WebSocket 연결 방식과 kubelet이 HTTP 메서드를 RBAC 동사(verb)에 매핑하는 방식 사이의 불일치에 있습니다. WebSocket 프로토콜(RFC 6455)은 초기 연결 핸드셰이크를 위해 HTTP GET 요청을 요구합니다. kubelet은 이 GET을 RBAC get 동사로 매핑하고, 이후에 이어지는 쓰기 작업에 대해 CREATE 권한이 있는지는 별도로 확인하지 않은 채 요청을 승인합니다. 공격자는 websocat과 같은 WebSocket 클라이언트를 사용하여 10250 포트의 kubelet /exec 엔드포인트에 직접 접속하여 임의의 명령을 실행할 수 있습니다.

websocat --insecure \
 --header "Authorization: Bearer $TOKEN" \
 --protocol v4.channel.k8s.io \
 "wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=id"

uid=0(root) gid=0(root) groups=0(root)

세분화된 kubelet 권한 부여: 작동 원리

KubeletFineGrainedAuthz를 통해 kubelet은 이제 기존의 nodes/proxy 하위 리소스로 넘어가기 전에 추가적인 정밀 권한 체크를 수행합니다. 자주 사용되는 여러 kubelet API 경로가 전용 하위 리소스로 매핑되었습니다.

kubelet API 리소스 (Resource) 하위 리소스 (Subresource)
/stats/* nodes stats
/metrics/* nodes metrics
/logs/* nodes log
/pods nodes pods, proxy
/runningPods/ nodes pods, proxy
/healthz nodes healthz, proxy
/configz nodes configz, proxy
/spec/* nodes spec
/checkpoint/* nodes checkpoint
기타 모든 것 nodes proxy

이제 독자적인 하위 리소스가 생긴 엔드포인트(/pods, /runningPods/, /healthz, /configz)에 대해, kubelet은 먼저 해당 하위 리소스에 대한 SubjectAccessReview를 보냅니다. 해당 확인이 성공하면 요청이 승인됩니다. 만약 실패하면 하위 호환성을 위해 조대 권한인 nodes/proxy 하위 리소스로 재시도합니다.

이러한 이중 확인 방식은 부드러운 전환 경로를 보장합니다. nodes/proxy 권한을 가진 기존 워크로드는 계속 작동하며, 새로운 배포는 첫날부터 최소 권한 원칙을 적용할 수 있습니다.

실제 적용 예시

kubelet에서 /metrics를 긁어와야 하는 Prometheus 노드 익스포터(node exporter)나 모니터링용 DaemonSet을 가정해 봅시다. 이전에는 다음과 같은 RBAC ClusterRole이 필요했습니다.

# 기존 방식: 과도하게 넓은 권한
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
 name: monitoring-agent
rules:
- apiGroups: [""]
 resources: ["nodes/proxy"]
 verbs: ["get"]

이는 모니터링 에이전트에 필요 이상의 접근 권한을 부여합니다. 세분화된 권한 부여를 사용하면 이제 다음과 같이 정밀하게 범위를 좁힐 수 있습니다.

# 새로운 방식: 최소 권한 적용
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
 name: monitoring-agent
rules:
- apiGroups: [""]
 resources: ["nodes/metrics", "nodes/stats"]
 verbs: ["get"]

이제 모니터링 에이전트는 컨테이너에서 명령을 실행할 권한 없이도 kubelet에서 메트릭과 통계 정보를 읽을 수 있습니다.

system:kubelet-api-admin ClusterRole 업데이트

RBAC 권한 부여가 활성화되면, 기본 제공되는 system:kubelet-api-admin ClusterRole이 모든 새로운 세분화된 하위 리소스에 대한 권한을 포함하도록 자동으로 업데이트됩니다. 이를 통해 API 서버의 kubelet 클라이언트를 포함하여 이미 이 역할을 사용 중인 클러스터 관리자는 수동 설정 변경 없이도 전체 액세스 권한을 유지할 수 있습니다.

해당 역할은 이제 다음 권한들을 포함합니다:

  • nodes/proxy
  • nodes/stats
  • nodes/metrics
  • nodes/log
  • nodes/spec
  • nodes/checkpoint
  • nodes/configz
  • nodes/healthz
  • nodes/pods

업그레이드 시 고려 사항

kubelet이 이중 권한 확인(세분화된 권한 확인 후 nodes/proxy로 대체)을 수행하므로, v1.36으로의 업그레이드는 대부분의 클러스터에서 원활하게 이루어집니다.

  • 기존 워크로드: nodes/proxy 권한을 가진 워크로드는 변경 없이 계속 작동합니다. nodes/proxy로의 폴백(fallback) 기능이 하위 호환성을 보장합니다.
  • API 서버: system:kubelet-api-admin을 통해 항상 nodes/proxy 권한을 가지므로, kube-apiserverkubelet 간의 통신은 기능 게이트 상태에 관계없이 영향을 받지 않습니다.
  • 버전 혼합 클러스터: 유연하게 처리됩니다. kubelet은 세분화된 권한 부여를 지원하지만 API 서버가 지원하지 않거나 그 반대인 경우에도 nodes/proxy 권한이 폴백 역할을 합니다.

기능 활성화 여부 확인

kubelet 메트릭 엔드포인트를 확인하여 특정 노드에서 기능이 활성화되었는지 확인할 수 있습니다. 10250 포트의 메트릭 엔드포인트는 권한 부여가 필요하므로, 먼저 요청을 보내는 포드나 ServiceAccount에 적절한 RBAC 바인딩을 생성해야 합니다.

1단계: ServiceAccountClusterRole 생성

apiVersion: v1
kind: ServiceAccount
metadata:
 name: kubelet-metrics-checker
 namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
 name: kubelet-metrics-reader
rules:
- apiGroups: [""]
 resources: ["nodes/metrics"]
 verbs: ["get"]

2단계: ClusterRoleServiceAccount에 바인딩

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
 name: kubelet-metrics-checker
subjects:
- kind: ServiceAccount
 name: kubelet-metrics-checker
 namespace: default
roleRef:
 kind: ClusterRole
 name: kubelet-metrics-reader
 apiGroup: rbac.authorization.k8s.io

두 매니페스트를 적용합니다:

kubectl apply -f serviceaccount.yaml
kubectl apply -f clusterrole.yaml
kubectl apply -f clusterrolebinding.yaml

3단계: ServiceAccount를 사용하는 포드를 실행하고 기능 플래그 확인

kubectl run kubelet-check \
 --image=curlimages/curl \
 --serviceaccount=kubelet-metrics-checker \
 --restart=Never \
 --rm -it \
 -- sh

그런 다음 포드 내부에서 노드 IP를 가져와 메트릭 엔드포인트를 쿼리합니다.

# 토큰 가져오기
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# kubelet 메트릭을 쿼리하고 기능 게이트 필터링
curl -sk \
 --header "Authorization: Bearer $TOKEN" \
 https://$NODE_IP:10250/metrics \
 | grep kubernetes_feature_enabled \
 | grep KubeletFineGrainedAuthz

기능이 활성화되어 있다면 다음과 같은 출력이 나타납니다:

kubernetes_feature_enabled{name="KubeletFineGrainedAuthz",stage="GA"} 1

참고: $NODE_IP를 확인하려는 노드의 IP 주소로 바꾸세요. 노드 IP는 kubectl get nodes -o wide로 확인할 수 있습니다.

알파에서 GA까지의 여정

릴리스 단계 세부 내용
v1.32 Alpha 기능 게이트 KubeletFineGrainedAuthz 도입, 기본 비활성화
v1.33 Beta 기본 활성화; /pods, /runningPods/, /healthz, /configz에 대한 세분화된 체크 도입
v1.36 GA 기능 게이트가 활성화로 고정됨; 세분화된 kubelet 권한 부여가 항상 활성화됨

향후 전망

세분화된 kubelet 권한 부여가 GA 단계에 도달함에 따라, Kubernetes 커뮤니티는 모니터링 및 관찰성 워크로드에 nodes/proxy 대신 특정 하위 리소스를 사용할 것을 권장하고 결국 강제하기 시작할 것입니다. 이러한 전환의 시급함은 nodes/proxy GET이 WebSocket 프로토콜을 통한 기록되지 않는 원격 코드 실행(RCE)에 악용될 수 있다는 연구 결과로 인해 더욱 강조됩니다. 이 위험은 널리 배포된 수십 개의 Helm 차트의 기본 RBAC 설정에 존재합니다. 시간이 지남에 따라 다음과 같은 변화를 기대할 수 있습니다.

  • 생태계 채택: Prometheus, Datadog 에이전트 및 기타 DaemonSets와 같은 모니터링 도구는 기본 RBAC 설정을 nodes/proxy 대신 nodes/metrics, nodes/stats, nodes/pods를 사용하도록 업데이트할 수 있습니다. 이는 해당 워크로드에 대한 WebSocket RCE 공격 표면을 직접적으로 제거합니다.
  • 정책 강제: Admission 컨트롤러 및 정책 엔진은 세분화된 대안이 존재함에도 nodes/proxy 권한을 부여하는 RBAC 바인딩을 감지하거나 거부하여, 조직이 규모에 맞게 최소 권한 액세스를 채택하도록 도울 수 있습니다.
  • 지원 중단 경로: 채택률이 높아짐에 따라 nodes/proxy는 모니터링 용도로의 사용이 점차 중단(deprecated)될 수 있으며, 이는 Kubernetes 클러스터의 공격 표면을 더욱 줄여줄 것입니다.

참여하기

이 개선 사항은 SIG Auth 및 SIG Node에 의해 추진되었습니다. Kubernetes의 보안 및 권한 부여 기능에 기여하고 싶다면 저희와 함께해 주세요.

이 기능에 대한 여러분의 의견과 경험을 기다리겠습니다!