Манифест, который создаёт объект Kubernetes, может выдать роль roles/owner на всю организацию Google Cloud. Это не ошибка в одной системе, а следствие того, что две системы авторизации — Kubernetes RBAC и Google Cloud IAM — принимают решения независимо и ни одна из них не видит цепочку целиком. Разберём механику: как устроена защита Kubernetes от эскалации, где именно она перестаёт работать и что видит клиент в момент применения манифеста.
Два независимых слоя авторизации
В одном YAML-манифесте объединяются объект Kubernetes RBAC (ClusterRoleBinding) и объект Config Connector (IAMPolicyMember). Config Connectorинструмент, который позволяет управлять ресурсами Google Cloud через объекты Kubernetes нужен, чтобы описывать изменения в Google Cloud декларативно, из кластера. Объект IAMPolicyMember описывает желаемое изменение IAM в Google Cloud: он содержит member (кого назначают), role (какую роль дают) и resourceRef (на какой ресурс). Вредоносный вариант указывает member как serviceAccount:[email protected], role как roles/owner и resourceRef с kind: Organization и external: "123456789", то есть выдаёт злоумышленнику роль владельца на всю организацию GCP.
Атакующий применяет этот YAML через Kubernetes. Config Connector читает его и запрашивает изменение IAM у Google Cloud от имени своего сервисного аккаунта, у которого есть на это разрешение, поэтому запрос принимается, и атакующий получает контроль над организацией, не имея учётных данных Google Cloud.
Эскалация возникает из-за двух раздельных систем авторизации. Kubernetes RBAC проверяет лишь, разрешено ли пользователю создавать ресурс IAMPolicyMember в пространстве имён, и ничего не знает о Google Cloud. Google Cloud IAM проверяет лишь, есть ли у сервисного аккаунта Config Connector право установить эту привязку, и не знает, какой пользователь Kubernetes инициировал запрос. Это confused deputy problemситуация, когда привилегированный посредник выполняет инструкции менее полномочного пользователя, не проверяя его право на это: Config Connector обладает широкими полномочиями и выполняет инструкции менее полномочных пользователей, не проверяя, должны ли они иметь к ним доступ.
Уязвимая конфигурация сочетает сервисный аккаунт Config Connector с правами IAM на уровне организации и широкий доступ на запись в управляемые Config Connector пространства имён. По отдельности каждое условие управляемо, но вместе они создают путь повышения привилегий, не требующий учётных данных Google Cloud.
Как KCC аутентифицируется в Google Cloud
KCCИнструмент, который позволяет управлять ресурсами Google Cloud через объекты Kubernetes. аутентифицируется в Google Cloud через Workload Identityмеханизм, привязывающий Kubernetes ServiceAccount к Google service account, чтобы поды получали доступ к Google Cloud без долгоживущих ключей, используя Google service accountучётную запись Google Cloud, от имени которой выполняются операции в Google Cloud; в отличие от Kubernetes ServiceAccount, которая действует внутри кластера, она действует в Google Cloud (KCC GSA), контролируемую командой платформы. Этот аккаунт может иметь широкие роли, такие как role/owner или roles/resourcemanager.organizationAdmin, поскольку KCC управляет инфраструктурой в нескольких проектах, папках или всей организации.
Когда разработчик отправляет ресурс, KCC подхватывает его и вызывает Google Cloud IAM, создавая разрешение, при этом разработчик никогда не касается учётных данных Google Cloud. Все пространства имён используют единую организационную идентичность KCC, и KCC выполняет каждую операцию в Google Cloud через свой собственный service account, независимо от того, какой пользователь Kubernetes отправил ресурс. Если у аккаунта есть разрешения на уровне организации, пользователь с ограниченным доступом к кластеру может косвенно воспользоваться этими разрешениями.
Механика Kubernetes RBAC: как создаётся ClusterRoleBinding
Команда kubectl create clusterrolebinding строит объект ClusterRoleBinding пошагово. Обязательным флагом является только --clusterrole: он регистрируется в наборе флагов команды, после чего помечается как обязательный. Флаги --user, --group, --serviceaccount и --field-manager добавляются как необязательные; отдельно регистрируются опции аннотации применения, валидации и dry-run, значения которых затем читаются при обработке команды.
В основном проходе объект создаётся функцией createClusterRoleBinding, затем добавляется аннотация последнего применения, если соответствующий флаг включён. Создание на сервере выполняется, только если стратегия dry-run не равна DryRunClient; при DryRunClient создание на сервере пропускается. В параметры создания передаются FieldManager и директива валидации, а DryRun=[metav1.DryRunAll] устанавливается только при DryRunServer. В конце результат печатается.
Сам объект формируется с RoleRef на ClusterRole, а пользователи, группы и serviceaccounts добавляются в Subjects. Serviceaccount при этом должен иметь формат namespace:name.
Canonicalize ничего не нормализует
Функция Canonicalize в strategy.go для ClusterRoleBinding не выполняет никакой нормализации полей: она лишь приводит переданный объект к типу *rbac.ClusterRoleBinding и отбрасывает результат. Никакие поля объекта не изменяются и не нормализуются до записи в etcd. Тело функции состоит из одного приведения типа с присваиванием в пустой идентификатор.
Где вызывается проверка на предотвращение эскалации
NewStorage в пакете policybased создаёт обёртку над обычным хранилищем ClusterRoleBinding, которая предотвращает повышение привилегий: она принимает нижележащее хранилище, авторизатор и преобразователь правил. Проверка выполняется не в самом NewStorage, а в методах Create и Update этой обёртки.
В Create сначала проверяется, разрешена ли эскалация — если да, вызов сразу делегируется нижележащему хранилищу. Затем проверяется, явно ли пользователь уполномочен связывать эту роль — если да, создание тоже делегируется. Если обе проверки не прошли, ссылка на роль конвертируется, правила роли получаются через преобразователь правил, и вызывается ConfirmNoEscalationпроверка, которая сравнивает выдаваемые RBAC-права с уже имеющимися у пользователя и отклоняет создание привязки, если он пытается выдать права, которыми сам не обладает; при ошибке возвращается Forbidden, иначе создание делегируется.
В Update аналогичная логика обёрнута в rest.WrapUpdatedObjectInfo: сначала пропускаются изменения только GC-полей, затем проверяется авторизация на связывание роли, затем выполняются те же конвертация, получение правил и ConfirmNoEscalation.
Защита от эскалации внутри Kubernetes: ConfirmNoEscalation
ConfirmNoEscalation берёт пользователя и namespace из контекста запроса, собирает его собственные правила через RulesFor и проверяет, покрывают ли они запрашиваемые правила с помощью validation.Covers; если нет — формирует ошибку с перечнем недостающих прав.
GetRoleReferenceRules резолвит ссылку на роль по полю roleRef.Kind: для Role берёт Role в namespace биндинга и возвращает его правила, для ClusterRole берёт ClusterRole и возвращает его правила, иначе возвращает ошибку о неподдерживаемом виде ссылки на роль.
Функция appliesTo перебирает Subjects биндинга и возвращает индекс первого, который подходит пользователю. appliesToUser сравнивает: для UserKind — имя пользователя с именем subject, для GroupKind — наличие имени subject среди групп пользователя, для ServiceAccountKind — совпадение через serviceaccount.MatchesUsername с namespace subject или namespace биндинга.
В storage.go создание ClusterRoleBinding отклоняется, если не разрешён escalate, не авторизован bind, и ConfirmNoEscalation вернул ошибку — тогда возвращается Forbidden. То есть биндинг отклоняется, когда у пользователя нет всех прав из резолвнутой роли и нет явной авторизации на bind.
Чем можно обойти проверку
Обойти ConfirmNoEscalation позволяет явное разрешение на глагол escalate для ресурсов roles или clusterroles в API-группе rbac.authorization.k8s.io. Это описано в разделе «Restrictions on role binding creation or update», потому что там же изложены ограничения на создание и обновление привязок ролей: привязку можно создать, только если у пользователя уже есть все права из роли, либо если ему разрешён глагол bind на конкретную роль.
Глагол bind, как и escalate, позволяет обойти встроенные защиты от повышения привилегий, давая создавать привязки к ролям с правами, которых у пользователя нет. В примере ClusterRole role-grantor право bind выдаётся с resourceNames: ["admin","edit","view"], что ограничивает привязку только этими ролями, а комментарий «omit resourceNames to allow binding any ClusterRole» показывает, что отсутствие resourceNames снимает это ограничение.
Поведение при удалении объекта во время reconcile
При запуске reconcile сначала выполняется Get ожидаемого binding; если объект не найден, результат сразу формируется как ReconcileCreate со всеми ожидаемыми субъектами в MissingSubjects. Если объект существует, computeReconciledRoleBinding сравнивает roleRef: при различии возвращается ReconcileRecreate, иначе копия existing с merge аннотаций и меток и вычислением MissingSubjects/ExtraSubjects.
При ReconcileRecreate сначала вызывается Delete с UID-предусловием; если Delete вернул NotFound, объект уже отсутствует, и выполняется переход к Create. При ReconcileCreate вызов Create может вернуть AlreadyExists, что означает, что объект появился заново между Get и Create; тогда reconcile запускается повторно. Аналогично при ReconcileUpdate ошибка NotFound (объект удалён после начала reconcile) приводит к повторному запуску. Повторные запуски ограничены: если число попыток превышает 3, возвращается ошибка «exceeded maximum attempts», что не даёт бесконечно пересоздавать binding, который появляется и исчезает.
Дефолтные роли и агрегация: где прячется cluster-admin
Функция ClusterRoleBindings возвращает срез объектов ClusterRoleBinding — это привязки, которые связывают предопределённые ClusterRole с группами или пользователями. Каждая привязка создаётся вызовом конструктора для имени роли с указанием групп, пользователей или сервисных аккаунтов, после чего собирается объект ClusterRoleBinding.
Полный доступ суперпользователя даёт привязка роли cluster-admin к группе system:masters — встроенной группе Kubernetes, члены которой обладают неограниченными правами в кластере. Остальные привязки дают ограниченные права: system:monitoring — группе MonitoringGroup, system:discovery и system:basic-user — всем аутентифицированным, system:public-info-viewer — всем аутентифицированным и неаутентифицированным, system:node-proxier — пользователю KubeProxy, system:kube-controller-manager — пользователю KubeControllerManager, system:kube-dns — сервисному аккаунту kube-dns в kube-system, system:kube-scheduler и system:volume-scheduler — пользователю KubeScheduler. Дополнительно добавляется привязка system:service-account-issuer-discovery к группе AllServiceAccountsGroup, а при включённом ClusterTrustBundle — system:cluster-trust-bundle-discovery к той же группе.
Функция ClusterRolesToAggregate возвращает карту соответствия старых имён кластерных ролей новым агрегируемым: admin → system:aggregate-to-admin, edit → system:aggregate-to-edit, view → system:aggregate-to-view. Функция ClusterRoleBindingsToSplit перебирает все привязки и для привязки с именем system:public-info-viewer помещает её в карту под ключом system:discovery, чтобы скопировать Subjects, Annotations и Labels в шаблон назначения.
Как работает агрегация ClusterRole
Агрегированный ClusterRoleобъект, у которого задано поле aggregationRule с селектором меток; контроллер плоскости управления объединяет правила подходящих ClusterRole в его поле rules — это объект, у которого задано поле aggregationRule; оно содержит селектор меток, по которому контроллер плоскости управления находит другие ClusterRole и объединяет их правила в поле rules этого объекта. Контроллер, работающий в составе плоскости управления, следит за ClusterRole с установленным aggregationRule и использует этот селектор для сопоставления объектов, которые должны быть объединены.
Плоскость управления перезаписывает любые значения, вручную указанные в поле rules агрегированной роли, поэтому менять или добавлять правила нужно в тех ClusterRole, которые выбираются селектором. Когда создаётся новый ClusterRole, чьи метки совпадают с селектором существующей агрегированной роли, это событие запускает добавление новых правил в агрегированную роль. Например, создание ClusterRole monitoring-endpointslices с меткой rbac.example.com/aggregate-to-monitoring: "true" приводит к тому, что его правила добавляются в ClusterRole monitoring.
Auto-reconciliation дефолтных ролей
При старте API-сервер создаёт набор дефолтных ClusterRole и ClusterRoleBinding, помеченных kubernetes.io/bootstrapping=rbac-defaultsметка, которой API-сервер помечает все создаваемые им по умолчанию ClusterRole и ClusterRoleBinding; среди них есть пользовательские роли: cluster-admin (биндится на группу system:masters), а admin, edit, view создаются без биндинга.
При каждом старте API-сервер выполняет auto-reconciliation: он добавляет отсутствующие разрешения в дефолтные ClusterRole и отсутствующих субъектов в дефолтные ClusterRoleBinding, что позволяет исправлять случайные изменения. Отключить это можно, установив аннотацию rbac.authorization.kubernetes.io/autoupdate в false на дефолтной роли или биндинге.
Функция computeReconciledRoleBinding сравнивает существующий и ожидаемый биндинг: если roleRef отличается, биндинг полностью пересоздаётся; иначе аннотации и метки сливаются, а субъекты либо добавляются (при removeExtraSubjects=false), либо полностью заменяются на ожидаемые (при removeExtraSubjects=true).
Мост в GCP: как KCC превращает Kubernetes-объект в IAM-изменение
KCC читает манифест IAMPolicyMember и от имени собственного сервисного аккаунта (KCC GSA) обращается к Google Cloud IAM, чтобы создать привязку роли. Scopeобласть действия привязки — проект, папка или организация, к которой применяется роль привязки задаётся в spec.resourceRef: поля kind и external указывают, к какому ресурсу применяется роль — например, kind: Project с external: "my-project" или kind: Organization с external: "123456789". Член привязки задаётся полем spec.member, а назначаемая роль — полем spec.role.
KCC аутентифицируется в Google Cloud через Workload Identity, используя сервисный аккаунт, который может иметь широкие права, включая roles/owner или roles/resourcemanager.organizationAdmin.
Условия IAM, ограничивающие область действия
В binding можно использовать два условия IAM: ограничение по времени через request.time < timestamp('TIMESTAMP') и ограничение по тегу ресурса через resource.matchTag('TAG_KEY', 'TAG_VALUE').
Binding без таких ограничений становится organization-wide потому, что KCC выполняет запрос от своего service account, а Google Cloud проверяет только права этого service account, не зная, какой пользователь Kubernetes инициировал запрос. В примере атаки resourceRef указывает на kind: Organization с external: "123456789", а роль — roles/owner, что и даёт контроль над организацией.
Почему одного YAML достаточно: разрыв между двумя системами авторизации
Проверка ConfirmNoEscalation в Kubernetes работает только с RBAC-правилами: она получает правила пользователя через RulesFor и сравнивает их с запрашиваемыми правилами через validation.Covers, то есть проверяет лишь то, покрывают ли текущие RBAC-права пользователя выдаваемые им RBAC-права. Она не обращается к Google Cloud IAM и не знает о правах KCC-сервис-аккаунта на уровне организации.
KCC же выполняет операцию в Google Cloud через собственный сервис-аккаунт, и Google Cloud IAM проверяет только его права, а не Kubernetes-пользователя, инициировавшего запрос. Поэтому Kubernetes видит только создание ресурса внутри кластера, а Google Cloud — только сервис-аккаунт KCC, и ни одна из систем не проверяет всю цепочку целиком.
Документация не делает связь между двумя решениями очевидной, потому что администраторы обычно рассматривают их по отдельности: какие типы Kubernetes-ресурсов может создавать команда и что может делать сервисный аккаунт KCC в Google Cloud. Однако в KCC эти решения связаны: разрешение команде создавать ресурс IAMPolicyMember может дать этой команде способ использовать полномочия KCC в Google Cloud.
Вне поля зрения RBAC остаются шаги: атакующий применяет YAML через Kubernetes, KCC читает его и запрашивает изменение IAM в Google Cloud от имени своего сервисного аккаунта, обладающего нужным разрешением, после чего запрос принимается. В результате KCC может получить запрос от пользователя с ограниченным доступом и выполнить его с полномочиями, которых у этого пользователя в Google Cloud нет. Google называет такое поведение работой по проекту, поскольку администратор сам выбрал выдать KCC сервисный аккаунт уровня организации и разрешить разработчикам создавать IAMPolicyMember в управляемых KCC пространствах имён.
Что видит клиент и как это воспроизвести
При применении манифеста, создающего ClusterRoleBinding, API-сервер сначала проверяет, разрешена ли эскалация: если да, объект создаётся напрямую через хранилище. Иначе проверяется авторизация на привязку роли, и при её отсутствии резолвятся правила роли и вызывается ConfirmNoEscalation. Если прав пользователя не хватает, возвращается ошибка Forbidden с сообщением о попытке выдать неподдерживаемые RBAC-права.
Успешная эскалация отличается тем, что запрос проходит без ошибки, а отклонённая — статусом Forbidden и текстом вида «is attempting to grant RBAC permissions not currently held».
Проверка защиты ConfirmNoEscalation выполняется при создании или обновлении RBAC-объектов: функция получает правила из запроса и сравнивает их с правами пользователя, возвращая ошибку, если прав не хватает. Сначала она извлекает пользователя из контекста и возвращает ошибку, если его там нет, затем получает правила пользователя через RulesFor и проверяет покрытие через validation.Covers.
Воспроизвести шаги можно через kubectl auth reconcile с манифестом RBAC-объектов и флагом --dry-run=client, который показывает изменения без применения. Для привязки ClusterRole ко всему кластеру используется kubectl create clusterrolebinding, который выдаёт разрешения ClusterRole во всех пространствах имён. Защита сработает, если у пользователя нет выдаваемых прав и нет явного разрешения на глагол escalate для roles или clusterroles, либо глагола bind для привязываемой роли.
Снижение риска: ограничение обеих сторон
На стороне Kubernetes защита строится на встроенных проверках RBAC: создать или обновить привязку роли можно только если у пользователя уже есть все права из этой роли в том же масштабе, либо ему явно разрешён глагол bind на эту роль. Аналогично для создания ролей: пользователь не сможет включить в ClusterRole права, которых у него самого нет, если ему не выдан глагол escalate.
Поэтому выдачу bind и escalate следует ограничивать, так как bind позволяет обойти встроенные защиты от повышения привилегий. От использования wildcards в правилах следует отказаться, поскольку они дают избыточные права на чувствительные ресурсы.
На стороне GCP нужно ограничить права сервисного аккаунта KCC: проверить его роли на уровнях проекта, папки и организации и убрать широкие роли, такие как roles/owner и roles/resourcemanager.organizationAdmin, если они не строго необходимы. Также следует ограничить, кто может создавать ресурсы IAMPolicyMember, IAMPolicy и IAMPartialPolicy — это должно быть доступно только одобренным платформенным или инфраструктурным командам.
Про VPC Service Controls сказано, что они действуют в пределах периметра, но не обеспечивают контроль доступа для межпериметровых запросов на основе федеративных идентичностей.
Как это соотносится с общими рекомендациями по RBAC
Рекомендации из rbac-good-practices описывают, как избыточные RBAC-права внутри кластера позволяют обойти границы доверия, а разобранный случай показывает конкретный пример, когда такое избыточное право в Kubernetes превращается в захват GCP-организации.
Так, право patch на Namespace позволяет менять метки, что при использовании Pod Security Admission даёт настроить более мягкую политику, а при NetworkPolicy — открыть доступ к сервисам; аналогично именно право создавать IAMPolicyMember в KCC-управляемом namespace запускает цепочку. Ключевая параллель — отсутствие проверки полной цепочки: RBAC проверяет только право создать IAMPolicyMember в namespace, а Google Cloud IAM проверяет только права сервисного аккаунта KCC, из-за чего возникает confused deputy problem.
Рекомендация ограничивать создание PersistentVolume и доступ к nodes/proxy (который даёт права на Kubelet API и обходит audit logging) по духу совпадает с чек-листом: ограничить и то, что может сервисный аккаунт KCC в GCP, и то, какие пользователи Kubernetes могут отправлять ему ресурсы. В обоих источниках вывод один: границы внутри namespace считаются слабыми, поэтому нужно следовать least privilegeпринципу предоставления минимально необходимых прав и ограничивать обе стороны авторизации.
Что из этого следует на практике
- Разделение ответственности между Kubernetes RBAC и Google Cloud IAM — не гарантия безопасности, а источник разрыва: каждая система проверяет только свою часть цепочки, и ни одна не видит её целиком.
- Право создавать IAMPolicyMember в управляемом KCC пространстве имён равносильно праву использовать все полномочия KCC GSA в Google Cloud — включая права уровня организации, если они есть.
- Встроенная защита Kubernetes от эскалации (ConfirmNoEscalation) работает только против RBAC-эскалации внутри кластера и не может ничего проверить за его пределами.
- Глаголы
bindиescalateснимают встроенные защиты RBAC, поэтому их выдача должна быть ограничена; отсутствиеresourceNamesпри выдачеbindпозволяет привязывать любую ClusterRole. - Агрегация ClusterRole означает, что создание нового ClusterRole с подходящей меткой автоматически расширяет права уже существующей агрегированной роли — это ещё один путь непрямого расширения прав.
- Смягчение требует действий с обеих сторон: ограничить роли KCC GSA на уровнях проекта, папки и организации и одновременно ограничить круг пользователей Kubernetes, которые могут отправлять KCC ресурсы.
Где смотреть в коде
- policy.go: ClusterRoleBindings
- rule.go: ConfirmNoEscalation
- rule.go: appliesTo
- storage.go: NewStorage
- create_clusterrolebinding.go: NewCmdCreateClusterRoleBinding
- rule.go: NewDefaultRuleResolver
- reconcile_rolebindings.go: GetSubjects
- reconcile_rolebindings.go: computeReconciledRoleBinding