Kubernetes RBAC-харденинг: убираем wildcard-биндинги и включаем audit-log
После очередного отчёта об утечках через незащищённый kube-apiserver прошлись по всем кластерам: убрали wildcard-биндинги, включили audit-log, ограничили NodePort.
Рост числа инцидентов с открытыми Kubernetes API вывел тему RBAC-харденинга на первый план
Новый год начался с очередного разбора полётов - на этот раз не у нас, но где-то очень близко по профилю инфраструктуры. В сети снова всплыл отчёт об утечках через открытый kube-apiserver: кто-то поднял кластер, не закрыл 6443, и кто-то другой этим воспользовался. Мы решили не ждать, пока это коснётся нас напрямую, и устроили собственный внеплановый аудит.
Спойлер: нашли что поправить.
Что искали
Точкой отсчёта взяли три вопроса: кто может делать * на *, кто слышит всё что происходит в кластере, и через какие порты снаружи вообще торчит что не должно.
Wildcard ClusterRoleBinding. Первый же kubectl get clusterrolebindings -o json | jq показал несколько биндингов с resources: ["*"] и verbs: ["*"] - часть из них появилась во время первоначальной настройки как «временное, разберёмся потом», и, конечно, так и осталась. Сервис-аккаунты CI-пайплайна с правами cluster-admin - это не архитектура, это авария в ожидании повода.
Отсутствие audit-log. В нескольких кластерах --audit-log-path не был выставлен вообще. То есть, если кто-то и делал kubectl exec в продакшн-поды ночью - мы бы об этом не узнали. Это неприятное осознание.
NodePort-диапазон по умолчанию. Стандартный диапазон 30000-32767 - широкий, и несколько сервисов висело на портах, которые снаружи ничем не были прикрыты, потому что правила файрвола описывались руками и не всегда синхронизировались с тем, что реально поднималось в кластере.
Что сделали
Начали с инвентаризации прав. Прошли по каждому ClusterRole и Role, выписали все биндинги с wildcard-ами и разобрали каждый: нужен ли он, кому, с каким реальным scope. Большинство оказались либо избыточными, либо переназначаемыми с конкретным набором ресурсов и глаголов. Там где пайплайну нужен был только get и list на ConfigMap в конкретном namespace - он и получил именно это.
Параллельно включили audit-log на всех контрол-плейнах. Конфигурацию policy написали в два уровня:
Noneдля/healthz,/readyz,/livezи всего что от system-компонентов - иначе логи затапливает мусором.RequestResponseдля всего что касается секретов, configmap-ов, exec и attach - именно это интересно при разборе инцидента.
Audit-лог пишем на диск и забираем fluentd в централизованный ELK. Retention - 90 дней, хватит для разбора.
NodePort-диапазон сузили через --service-node-port-range на kube-apiserver. Конкретный диапазон - дело договорённостей с сетевиками, но сам факт его ограничения резко упрощает жизнь при написании правил файрвола: теперь правила статичны и не зависят от того, какой случайный порт выбрал Kubernetes.
Что оказалось неожиданным
Audit-log на загруженном кластере пишет заметно больше, чем кажется. Первые сутки мы немного недооценили объём и поймали предупреждение о месте на диске. Пришлось сразу аккуратнее выставить rotation и размер файла. Ничего критичного, но приятного мало.
Ещё один момент: несколько разработчиков работали с namespace-ами через kubectl, используя куберовые дефолтные сервис-аккаунты - которые, как выяснилось, имели чуть больше прав, чем надо. После урезания прав пара скриптов сломалась. Это был честный разговор о том, что «работает» и «правильно» - не одно и то же.
Ссылка на проведённую работу - аудит безопасности инфраструктуры.
Итог
Кластеры стали прозрачнее и жёстче одновременно. Wildcard-ов в биндингах не осталось, audit включён, NodePort-диапазон ограничен. Это не финальная точка - впереди ещё работа с Network Policy и проверка того, что admission controller настроен разумно. Но то, что было совсем открыто - закрыто.
Если у вас в кластере есть биндинг с verbs: ["*"] на resources: ["*"] и вы не можете с ходу объяснить зачем - это сигнал. Не завтра, сейчас.