Залез читать изменения Kubernetes 1.36 и наткнулся на прекрасное.
У ServiceAccount вашего Prometheus, log collector или monitoring agent может быть такое правило:
resources: ["nodes/proxy"]
verbs: ["get"]
Выглядит безобидно.
Ну get же. Read-only. Просто почитать метрики и разойтись.
На деле этого права может хватить, чтобы выполнить команду в любом контейнере на доступной ноде.
Да, get.
Да, RCE.
Ага, Kubernetes moment.
Как мы вообще сюда пришли
У kubelet есть HTTPS API, обычно на порту 10250. Через него можно получать метрики, статистику, логи, список Pod — и делать exec в контейнеры.
Исторически почти все эти endpoint’ы авторизовывались через один широкий ресурс — nodes/proxy.
То есть monitoring agent, которому нужно было только сходить в /metrics, получал то же право, через которое можно добраться до /exec.
Но становится ещё веселее.
Для установки WebSocket-соединения используется HTTP GET. Kubelet сопоставлял этот запрос с RBAC-глаголом get и не выполнял дополнительную проверку на create перед началом интерактивной операции.
В результате ServiceAccount с get nodes/proxy и сетевым доступом к kubelet API мог открыть /exec и выполнять команды внутри контейнеров.
Kubernetes SIG Auth и SIG Node прямо называют nodes/proxy возможностью уровня node superuser.
Важная оговорка
Это не означает, что любой установленный Prometheus автоматически уязвим.
Должны совпасть два условия:
— у ServiceAccount есть get на nodes/proxy;
— из workload можно достучаться до kubelet API на ноде.
Но для агентов, которые собирают данные напрямую с kubelet, второй пункт вполне может быть частью архитектуры.
И если такой агент или его токен скомпрометируют, blast radius будет немного больше, чем «сломался один дашборд».
Но ведь Kubernetes 1.36 это исправил?
И да, и нет. :)
В 1.36 KubeletFineGrainedAuthz стал GA и всегда включён. Вместо широкого nodes/proxy теперь можно выдавать точечные права:
— nodes/metrics для /metrics/*;
— nodes/stats для /stats/*;
— nodes/log для /logs/*;
— nodes/pods для списка Pod.
Например:
resources: ["nodes/metrics", "nodes/stats"]
verbs: ["get"]
Но апгрейд не удаляет старые разрешения.
Для обратной совместимости kubelet продолжает принимать nodes/proxy. Поэтому кластер может быть на свежей версии, все Pod зелёные, а старый blast radius никуда не делся.
Совместимость бережно сохранила не только ваши интеграции, но и лишние права. Очень заботливо.
Что я бы проверил прямо сейчас
1. Найти все ClusterRole с nodes/proxy:
kubectl get clusterroles -o json | jq -r '
.items[]
| select(any(.rules[]?;
(.resources // []) | index("nodes/proxy")))
| .metadata.name'
2. Посмотреть, к каким ServiceAccount они привязаны. Особенно внимательно — monitoring, logging, security и разные node-level DaemonSet.
3. Проверить конкретный ServiceAccount
kubectl auth can-i get nodes/proxy \
--as=system:serviceaccount:<namespace>:<serviceaccount>
4. Понять, какие kubelet endpoint’ы агент реально использует, и заменить nodes/proxy на минимальный набор nodes/metrics, nodes/stats, nodes/log или nodes/pods.
Не выдавайте весь набор «на всякий случай». Мы именно так сюда и приехали.
5. Проверить scrape и сбор логов на canary, удалить старое право и запретить новые workload-binding’и с nodes/proxy через admission policy. Доступ к 10250 тоже стоит ограничить на сетевом уровне.
Только не удаляйте разрешение вслепую из системных ролей: оно легитимно нужно некоторым компонентам Kubernetes. Нас интересуют обычные workload’ы, которым его выдали ради observability.
Главный вывод тут даже не про kubelet.
get — это название операции, а не гарантия безопасности.
Если ресурс называется proxy, вы разрешаете не чтение объекта. Вы открываете туннель к чужому API.
А что можно сделать внутри этого туннеля — RBAC-глагол вам уже не расскажет.
А у вас monitoring ServiceAccount всё ещё живут с nodes/proxy или вы уже перешли на fine-grained RBAC?
#kubernetes #security #sre
