Представь, что у тебя в кластере живут 30 человек, 200 подов и десяток CI-роботов, и каждый из них стучится в один и тот же API-сервер. Сервер обязан на каждый запрос ответить на два вопроса. Первый: кто ты - это аутентификация. Второй: что конкретно тебе сейчас можно сделать - это авторизация, в Kubernetes реализованная через RBAC. Если ты этого не контролируешь, у тебя один протёкший токен превращается в полный захват кластера. Поэтому kubernetes rbac - не бюрократия для галочки, а основная стена между "сломали один под" и "сломали весь прод".
Ключевая вещь, которую надо принять сразу: в Kubernetes нет объекта User. Ты не найдёшь в etcd запись про человека Ивана. Пользователь - это просто строка, которую API-сервер вытащил из предъявленного удостоверения (сертификата, токена, заголовка). Аутентификация устанавливает identity (имя плюс набор групп), а дальше RBAC решает, что этой строке разрешено. Объекты в кластере есть только у ServiceAccount - машинных идентичностей. Эта асимметрия и есть корень половины граблей, которые мы разберём.

Аутентификация: четыре способа доказать, кто ты
Запрос проходит цепочку authenticator-ов по порядку, первый сработавший побеждает. kubernetes аутентификация на практике сводится к четырём механизмам.
1. Клиентские сертификаты x509. Это то, чем по умолчанию ходит сам admin. В сертификате поле CN (Common Name) становится именем пользователя, а поля O (Organization) - его группами. API-сервер проверяет, что сертификат подписан тем CA, которому он доверяет (флаг --client-ca-file). Разберём, что внутри твоего kubeconfig:
Код: Выделить всё
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJ... # CA кластера в base64
server: https://10.0.0.1:6443
name: prod
users:
- name: ivan
user:
client-certificate-data: LS0tLS1CRUdJ... # твой сертификат
client-key-data: LS0tLS1CRUdJ... # твой приватный ключ
2. OIDC - правильный способ пускать людей. kubernetes oidc - это интеграция с внешним провайдером идентичности (Keycloak, Dex, Okta, Azure AD, в RU-реалиях - Keycloak поверх Yandex Cloud Organization). Человек логинится в SSO, получает JWT (id_token), kubectl кладёт его в заголовок Authorization. API-сервер проверяет подпись токена по публичным ключам провайдера (JWKS), достаёт из claim-ов имя и группы. Никаких секретов на стороне кластера, токены короткоживущие, увольнение человека в SSO мгновенно закрывает ему доступ.
С Kubernetes 1.30 (бета) старые флаги --oidc-* устарели в пользу структурированной конфигурации аутентификации - отдельного файла, который передают через --authentication-config. Он позволяет подключить несколько JWT-провайдеров сразу и валидировать токены на CEL-выражениях, причём менять конфиг можно без рестарта kube-apiserver:
Код: Выделить всё
apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://keycloak.cyberlake.ru/realms/k8s
audiences:
- kubernetes
claimMappings:
username:
claim: email
prefix: "oidc:"
groups:
claim: groups
prefix: "oidc:"
claimValidationRules:
- expression: 'claims.email_verified == true'
message: "email must be verified"
3. ServiceAccount-токены - для подов и роботов. Это JWT, выпущенный самим кластером. Современный механизм - projected token: kubelet через TokenRequest API получает короткоживущий токен (по умолчанию час), привязанный (bound) к конкретному поду и ServiceAccount, и монтирует его в под. Истёк под - истёк токен. Подробнее ниже, это отдельная большая тема.
4. Webhook token authentication. API-сервер отдаёт предъявленный токен внешнему сервису по HTTP и спрашивает "это валидно, кто это". Так подключают облачные IAM: в managed-кластерах (Yandex Managed Kubernetes, VK Cloud) именно webhook связывает токен облачного аккаунта с identity в кластере.
Авторизация: как устроен RBAC под капотом
Теперь k8s authorization. RBAC живёт в apiGroup rbac.authorization.k8s.io/v1 и состоит ровно из четырёх типов объектов, которые бьются на две пары.
Носители прав:
- Role - набор разрешений в пределах одного namespace.
- ClusterRole - набор разрешений на весь кластер либо шаблон, применимый в любом namespace. Только ClusterRole может давать права на cluster-scoped ресурсы (nodes, namespaces, PV) и на нересурсные пути (/healthz, /metrics).
- RoleBinding - связывает Role или ClusterRole с субъектами внутри namespace.
- ClusterRoleBinding - связывает ClusterRole с субъектами во всём кластере.
Само правило (rule) описывается тремя осями:
Код: Выделить всё
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: payments
name: pod-reader
rules:
- apiGroups: [""] # "" - это core-группа (pods, services, configmaps)
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
resourceNames: ["api-7f9c"] # необязательно: сузить до конкретных имён
- apiGroups - к какой группе API относится ресурс. Пустая строка "" - это core-группа. apps - для Deployment/StatefulSet. networking.k8s.io - для Ingress. Промахнулся группой - правило молча не сработает.
- resources - тип ресурса во множественном числе (pods, не Pod). Бывают subresource через слэш: pods/log, pods/exec, deployments/scale. Дать get на pods и забыть pods/log - и человек не увидит логи.
- verbs - что делать: get, list, watch, create, update, patch, delete, deletecollection. Отдельно стоит list против get: list возвращает все объекты разом, get - один по имени. Дашь list на secrets "чтобы видел список" - человек прочитает содержимое всех секретов, потому что в RBAC list отдаёт полные объекты, а не только имена.
Агрегация ClusterRole: роли, которые собираются сами
Когда у тебя 15 операторов (cert-manager, Argo CD, Prometheus и т.д.), каждый хочет дописать свои права к роли admin. Чтобы не переписывать гигантскую роль руками, есть агрегация. Базовая ClusterRole оставляет rules пустым и объявляет aggregationRule с селектором по меткам:
Код: Выделить всё
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.cyberlake.ru/aggregate-to-monitoring: "true"
rules: [] # control plane заполнит это сам
ServiceAccount и bound-токены подов
serviceaccount - это identity для процессов внутри кластера. У каждого пода он есть, даже если ты его не указал - подставится default из namespace. И вот тут историческая мина: раньше под получал вечный токен из Secret, который лежал в etcd и не протухал никогда. Утёк такой токен с логами в Grafana - и он валиден годами.
Современный Kubernetes (1.22+) перешёл на bound service account tokens через TokenRequest API и projected volume. Что меняется механически:
- Токен выпускается на ограниченный срок (expirationSeconds, по умолчанию час) и kubelet прозрачно его ротирует.
- Токен привязан (bound) к конкретному поду и ServiceAccount - в claim-ах зашит UID пода. Удалили под - токен мгновенно становится невалидным, даже если время ещё не вышло.
- Токен имеет audience - для кого он предназначен. Это блокирует пересылку токена в чужой сервис.
Код: Выделить всё
spec:
serviceAccountName: payments-api
containers:
- name: app
image: registry.cyberlake.ru/payments:1.4
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: api-token
expirationSeconds: 3600
audience: vault
Код: Выделить всё
$ kubectl create token payments-api -n payments --duration=10m
eyJhbGciOiJSUzI1NiIsImtpZCI6 Il9...
# что внутри - расшифруй payload (вторую часть JWT):
$ kubectl create token payments-api -n payments --duration=10m | \
cut -d. -f2 | base64 -d 2>/dev/null
{"aud":["https://kubernetes.default.svc"],
"exp":1739000000,
"kubernetes.io":{"namespace":"payments",
"serviceaccount":{"name":"payments-api","uid":"a1b2..."}},
"sub":"system:serviceaccount:payments:payments-api"}
Практика: проверяем доступ через auth can-i
Главный инструмент диагностики - kubectl auth can-i. Он спрашивает у самого API-сервера через SubjectAccessReview, можно ли действие, и не выполняет его:
Код: Выделить всё
# а мне самому можно удалять поды в payments?
$ kubectl auth can-i delete pods -n payments
yes
# а вон тому ServiceAccount? (impersonation через --as)
$ kubectl auth can-i list secrets -n payments \
--as=system:serviceaccount:payments:default
no
# полный аудит своих прав одним списком:
$ kubectl auth can-i --list -n payments
Resources Non-Resource URLs Resource Names Verbs
pods [] [] [get list watch]
pods/log [] [] [get]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
Когда что-то отказано непонятно почему, включают audit log. В audit-policy ставят уровень логирования, и API-сервер пишет каждое решение authorizer-а: кто, какой verb, какой ресурс, какой вердикт (allow/forbid) и по какому правилу RBAC (annotation authorization.k8s.io/decision и reason). Это единственный надёжный способ понять, какой именно биндинг дал или не дал доступ в проде.
Грабли и антипаттерны
- Дефолтный ServiceAccount с широкими правами. Если ты в namespace навесил права на default SA, их получают все поды, которые явно не указали другой serviceAccountName. Утечка одного пода = доступ ко всему, что мог default. Правило: каждому workload - свой SA, default держать пустым.
- Раздача cluster-admin "чтобы заработало". cluster-admin - это verb * на resource * во всех группах. Это бэкдор. Любой, кому он выдан, может выпустить себе новые токены, отредактировать RBAC и стереть аудит. Принцип наименьших привилегий: начинай с view, добавляй точечно по факту отказов из can-i.
- automountServiceAccountToken: true там, где токен не нужен. Большинству подов API-сервер не нужен вообще. Ставь automountServiceAccountToken: false на уровне SA или пода - нет смонтированного токена, нечему утекать.
- list/watch на secrets вместо get по имени. Классическая дыра: дали list secrets для UI, отдали все секреты целиком.
- ClusterRoleBinding там, где хватило бы RoleBinding. Связал ClusterRole edit через ClusterRoleBinding - и человек может редактировать во всех namespace, а не в своём. Для ограничения namespace используй RoleBinding, ссылающийся на ClusterRole.
- Сертификаты для людей. Невозможность отзыва превращает каждую утечку в перевыпуск CA. Люди - через OIDC, сертификаты - только break-glass.
Мини-лаба (повтори руками)
- Создай namespace lab и ServiceAccount ci-bot в нём: kubectl create ns lab; kubectl create sa ci-bot -n lab.
- Опиши Role pod-reader (get/list/watch на pods и pods/log) и RoleBinding, привязывающий её к ci-bot. Примени.
- Проверь права именно у этого SA, не у себя: kubectl auth can-i list pods -n lab --as=system:serviceaccount:lab:ci-bot (ожидай yes) и kubectl auth can-i delete pods -n lab --as=system:serviceaccount:lab:ci-bot (ожидай no).
- Выпусти короткий токен: kubectl create token ci-bot -n lab --duration=15m. Раскодируй его payload через cut -d. -f2 | base64 -d и найди поля sub, aud, exp.
- Сломай и почини: добавь в Role verb list на secrets, примени, перепроверь can-i list secrets - убедись, что доступ появился, затем убери его. Прочувствуй аддитивность RBAC.
- Почему дать verb list на secrets опаснее, чем get по конкретному resourceNames, и почему resourceNames вообще не работает с list?
- Чем bound projected-токен принципиально безопаснее старого вечного токена из Secret, и что происходит с токеном при удалении пода?
- В чём разница между RoleBinding на ClusterRole и ClusterRoleBinding на ту же ClusterRole - какой даёт права во всех namespace?
- Где проходит граница ответственности между RBAC и admission-контроллером при создании пода?
Аутентификация устанавливает, кто ты (сертификат, OIDC, SA-токен, webhook), RBAC аддитивно решает, что тебе можно (apiGroups/resources/verbs, носители Role/ClusterRole, привязки Binding), а bound-токены и принцип наименьших привилегий держат поверхность атаки маленькой. Запомни три рычага на каждый день: kubectl auth can-i для проверки, --as для отладки под чужой личиной, audit log когда непонятно почему. cluster-admin - только в крайнем случае, дефолтный SA - пустой, люди - через SSO. Дальше RBAC передаёт эстафету admission, который смотрит уже внутрь манифестов.