Критическая уязвимость Kubelet: обход авторизации через nodes/proxy
Введение в архитектуру доступа: риски разрешений в Kubernetes
Управление доступом в Kubernetes опирается на ролевую модель (Role-Based Access Control, RBAC), сопоставляющую субъектов (пользователей или сервисные аккаунты ServiceAccounts) с разрешенными действиями. Каждое разрешение в RBAC задает группу API, тип ресурса и действие-глагол (verb): например, get, list или create.
В практике администрирования принято считать глаголы чтения (get и list) безопасными. Напротив, глаголы записи (такие как create, update или delete), особенно для выполнения команд внутри контейнеров (pods/exec), жестко ограничиваются.
Однако в этой модели существует серьезный архитектурный нюанс, связанный с ресурсом nodes/proxy. Доступ к nodes/proxy с глаголом get обычно рассматривается как безопасное разрешение «только для чтения». Этот доступ массово выдается агентам мониторинга и логирования (Prometheus, Grafana Promtail, Datadog), чтобы они могли опрашивать интерфейсы узлов кластера, читать системную статистику и собирать логи контейнеров. Но, как показывает исследование Graham Helton, сочетание этого разрешения с открытым сетевым доступом к агенту узла (Kubelet) ведет к удаленному выполнению кода (Remote Code Execution, RCE) в любых подах кластера напрямую через Kubelet API, полностью обходя изоляцию RBAC.
Механизм уязвимости: WebSocket-рукопожатие и авторизация Kubelet
Kubelet — это системный агент, работающий на каждом узле (ноде) кластера. Он управляет контейнерами и предоставляет программный интерфейс (Kubelet API) на защищенном порту 10250.
Когда администратор запускает сессию в контейнере (например, kubectl exec), запрос идет через центральный шлюз (API Server). Шлюз аутентифицирует пользователя, проверяет его права на создание подресурса pods/exec и перенаправляет запрос к Kubelet API. Поскольку выполнение команд требует интерактивного обмена данными, соединение переходит с HTTP на протокол WebSocket. Этот переход осуществляется отправкой HTTP-запроса GET с заголовками обновления соединения (Upgrade: websocket).
Суть уязвимости кроется в том, как Kubelet обрабатывает авторизацию для WebSocket-соединений напрямую, минуя API Server. Kubelet принимает решение о допуске на основе первоначального HTTP-запроса GET с заголовком Upgrade. Он проверяет, имеет ли субъект право выполнять GET на ресурс nodes/proxy. Если такое право есть, Kubelet одобряет установление WebSocket-соединения.
После завершения рукопожатия (handshake) и переключения канала в режим WebSocket Kubelet перестает контролировать соответствие действий исходным правам. Клиент может отправлять внутри установленного соединения любые управляющие фреймы, включая запросы на выполнение команд /exec в контейнерах. Kubelet ожидает, что выполнение команд требует операции уровня CREATE, но WebSocket-соединение уже установлено в рамках одобренного GET-запроса, и дополнительная проверка прав на этапе передачи команд не производится.
Таким образом, любой субъект, обладающий правами nodes/proxy GET и способный установить сетевое соединение с портом 10250 Kubelet на целевом узле, может выполнить произвольный код в любом контейнере на этом узле. При этом атакующему не требуются права на pods/exec или доступ к API Server.
Использование websocat для демонстрации вектора атаки
Для проверки инфраструктуры на наличие данной уязвимости исследователи используют утилиту websocat — консольный клиент для работы с WebSocket. Сценарий проверки выглядит так: атакующий извлекает токен авторизации (bearer token) сервисного аккаунта, которому выдана роль nodes/proxy GET. Имея сетевой доступ к порту 10250 Kubelet на узле, он отправляет запрос:
websocat -k -H "Authorization: Bearer <TOKEN>" \
--protocol v4.channel.k8s.io \
"wss://<NODE_IP>:10250/exec/<NAMESPACE>/<POD_NAME>/<CONTAINER_NAME>?output=1&error=1&command=id"
Параметр --protocol v4.channel.k8s.io указывает Kubelet использовать внутренний протокол обмена данными Kubernetes. После отправки такого запроса Kubelet одобряет подключение на основании того, что токен дает право на nodes/proxy GET. WebSocket-соединение открывается, команда id выполняется внутри контейнера, а результат возвращается клиенту.
Этот вектор не является классической ошибкой переполнения буфера или уязвимостью в коде. Разработчики Kubernetes классифицировали данное поведение как ожидаемое (intended behavior) и закрыли отчет со статусом «won't fix». Kubelet проектировался в предположении, что его порт 10250 полностью изолирован от недоверенного трафика и доступен только для API Server. Следовательно, ответственность за защиту этой плоскости целиком возлагается на администраторов кластеров, которые должны настраивать сетевые политики.
Масштаб проблемы и скрытность атаки
Проблема усугубляется тем, что выдача роли с правами nodes/proxy GET является распространенным паттерном для инфраструктурного ПО. Анализ Helm-чартов показал, что как минимум 69 известных проектов запрашивают это разрешение. В их числе популярные системы мониторинга и логирования: prometheus-community/prometheus, grafana/promtail, datadog/datadog, elastic/elastic-agent, а также сетевые плагины и средства безопасности cilium/cilium, opentelemetry-kube-stack, trivy-operator, newrelic-infrastructure и wiz-sec/sensor.
Эксплуатация зависит от включенных опций чарта и сетевой доступности порта 10250 Kubelet изнутри подов. Однако при компрометации токена агента атакующий получает готовый ключ для захвата узлов кластера.
При этом прямые запросы к Kubelet API идут в обход API Server. В журналах API Server отразятся лишь фоновые запросы SubjectAccessReview без информации о запущенной команде, что делает атаку крайне скрытной.
Пошаговый чек-лист для аудита прав и сетевой безопасности
Чтобы убедиться, что ваш кластер защищен, выполните аудит по следующему алгоритму:
- Инвентаризация ролей RBAC. Найдите все ClusterRole и Role, которые содержат разрешение на работу с ресурсом
nodes/proxyс использованием глаголаget:kubectl get clusterrole,role -A -o yaml | grep -B 5 -A 5 "nodes/proxy" - Анализ привязок ролей (RoleBindings). Определите, к каким субъектам привязаны найденные роли. Выполните поиск по имени роли:
kubectl get clusterrolebinding,rolebinding -A -o json | jq '.items[] | select(.roleRef.name == "<ROLE_NAME>") | {namespace: .metadata.namespace, name: .metadata.name, subjects: .subjects}' - Проверка автоматического монтирования токенов. Если для скомпрометированного ServiceAccount опция
automountServiceAccountTokenустановлена вtrue, токен в файловой системе контейнера (по пути/var/run/secrets/kubernetes.io/serviceaccount/token) может быть похищен злоумышленником. - Сетевой smoke-тест доступности Kubelet API. Проверьте, есть ли сетевой путь из подов до порта 10250 любого узла кластера:
Если запрос возвращает HTTP-ответ, сетевой путь открыт и Kubelet API доступен для атаки. Если запрос завершается по таймауту, доступ заблокирован сетевыми политиками.
curl -k -I https://<NODE_IP>:10250/metrics
Рекомендации по устранению рисков и защите кластера
Полная защита инфраструктуры требует сочетания ограничения прав RBAC с ужесточением сетевой изоляции.
- Минимизация привилегий (Least Privilege). Проанализируйте, требуется ли агентам мониторинга доступ к
nodes/proxy. Часто современные версии Helm-чартов позволяют заменить это разрешение на более узкие эндпоинты, такие какnodes/metrics(для сбора метрик) или использование выделенных метрических API (Metrics API). Проверить официальную документацию Helm-чартов и переопределить значения (values), отключив функции, требующие работы с прокси узла, если они не критичны. - Ограничение сетевого доступа (Network Isolation). Сетевой порт 10250 Kubelet не должен быть доступен из обычных подов. Настройте сетевые политики Kubernetes (
NetworkPolicy) для ограничения исходящего трафика (egress) из пространств имен приложений. Запретите подам отправлять пакеты на IP-адреса узлов кластера по порту 10250, разрешив этот доступ только для ограниченного списка системных компонентов. - Отключение анонимного доступа. Убедитесь, что на уровне конфигурации запуска Kubelet (файл
/var/lib/kubelet/config.yamlна узлах) отключен анонимный доступ к API. Параметрauthentication.anonymous.enabledдолжен быть установлен вfalse. Также включите обязательную авторизацию Webhook-запросов, установив параметрauthorization.modeвWebhook. - Политики безопасности как код (Policy as Code). Внедрите проверку манифестов с помощью Admission Controllers (Kyverno или OPA Gatekeeper). Запретите создание ClusterRole и Role с ресурсом
nodes/proxyи глаголомgetбез ручного одобрения безопасности.

