Kubernetes для самых маленьких. Часть 29: Как устроены аутентификация и RBAC?
Все части серии «Kubernetes для самых маленьких»До сих пор мы работали с кластером от имени администратора из файла admin.conf, который kubeadm создал в части 4, и ни разу не получили отказа. А в прошлой части контроллеру понадобились права на greetings и greetings/status, а оператору PostgreSQL — на Secret во всех неймспейсах, и мы отложили вопрос, откуда они берутся и чем опасны. Сегодня о том, как kube-apiserver узнаёт, кто к нему пришёл, и решает, что этому субъекту можно. Протокол TLS, сертификаты и цепочки доверия разобраны в части 23 сетевой серии; здесь важно одно: всё общение с apiserver идёт по TLS, и сертификат клиента может быть его удостоверением.
Всё в этой части проверено на учебном кластере: сертификаты и CSR, RBAC, токены ServiceAccount, ограниченная имперсонация и AuthenticationConfiguration — с тестовым провайдером OIDC, выдававшим подписанные токены. Без проверки остались kubelogin и вход через настоящий провайдер (Keycloak, Dex), webhook-аутентификация, статический файл токенов и удаление старых Secret с токенами — о них по документации.
Путь запроса
Каждый запрос к API проходит одну и ту же цепочку:
клиент (kubectl, kubelet, контроллер, под)
│ HTTPS :6443
▼
┌─────────────────────┐
│ аутентификация │ кто это? → имя, UID, группы, доп. атрибуты
│ (authenticators) │ не узнали → system:anonymous или 401
└──────────┬──────────┘
▼
┌─────────────────────┐
│ авторизация │ можно ли ему этот глагол на этот ресурс?
│ (Node, RBAC, ...) │ нет → 403 Forbidden
└──────────┬──────────┘
▼
┌─────────────────────┐
│ admission │ изменить или отклонить объект по правилам
│ (mutating, │ (квоты, Pod Security, вебхуки — часть 30)
│ validating) │
└──────────┬──────────┘
▼
etcd
Аутентификация отвечает на вопрос «кто», авторизация — «можно ли», admission — «можно ли такой объект». Сегодня — первые два звена.
Аутентификация: пользователей нет
В Kubernetes нет объекта «пользователь». Нельзя сделать kubectl create user, нет списка пользователей, нет паролей в etcd. Пользователь — это строка, которую apiserver извлёк из удостоверения, предъявленного в запросе, и которой он доверяет, потому что удостоверение подписал тот, кому доверяет он сам: свой центр сертификации, свой ключ подписи токенов, внешний провайдер.
Результат аутентификации — набор атрибутов:
| Атрибут | Пример |
|---|---|
| имя (username) | alice, system:serviceaccount:demo:hello, system:node:k8s-node-1 |
| UID | уникальный идентификатор, если источник его даёт |
| группы | platform-admins, system:authenticated |
| extra | дополнительные пары ключ-значение (например, имя пода у токена ServiceAccount или отпечаток сертификата) |
Все прошедшие аутентификацию автоматически получают группу system:authenticated. Запрос без удостоверения становится запросом пользователя system:anonymous в группе system:unauthenticated — анонимный доступ включён по умолчанию, но RBAC по умолчанию не даёт ему почти ничего, кроме проверок здоровья (/healthz, /livez, /readyz) и /version. Префикс system: зарезервирован за самим Kubernetes, и выдавать такие имена людям нельзя.
Аутентификаторов несколько, apiserver пробует их по очереди, и первый узнавший запрос побеждает. Разберём каждый.
Клиентские сертификаты X.509
Если клиент при установке TLS предъявил сертификат, подписанный CA из --client-ca-file (в kubeadm — /etc/kubernetes/pki/ca.crt), apiserver берёт:
- CN (Common Name) субъекта — как имя пользователя;
- каждое O (Organization) — как группу.
Сертификат CN=alice, O=platform-admins, O=developers — это пользователь alice в группах platform-admins, developers и system:authenticated. Всё. apiserver не ходит ни в какую базу: подпись CA верна, срок не истёк — значит, alice.
Так аутентифицируются компоненты самого кластера: kubelet каждого узла — сертификатом CN=system:node:k8s-node-1, O=system:nodes, kube-scheduler и kube-controller-manager — своими. И администратор из admin.conf:
user@k8s-master-1:~$ sudo grep client-certificate-data /etc/kubernetes/admin.conf | awk '{print $2}' \
| base64 -d | openssl x509 -noout -subject -enddate
subject=O=kubeadm:cluster-admins, CN=kubernetes-admin
notAfter=Oct 24 10:31:34 2027 GMT
Группа kubeadm:cluster-admins получает роль cluster-admin через ClusterRoleBinding, который kubeadm создал при установке. Рядом лежит ещё super-admin.conf:
user@k8s-master-1:~$ sudo grep client-certificate-data /etc/kubernetes/super-admin.conf | awk '{print $2}' \
| base64 -d | openssl x509 -noout -subject
subject=O=system:masters, CN=kubernetes-super-admin
Группа system:masters обходит авторизацию целиком: запрос от её члена разрешается до проверки RBAC, и никакие изменения ролей её не ограничат. Поэтому kubeadm с версии 1.29 и разделил два файла: admin.conf — для работы, super-admin.conf — для аварии (например, если сломали RBAC), и его стоит держать на мастере, не копируя на ноутбуки. Выдавать кому-либо сертификат с O=system:masters нельзя никогда.
Токены
Второй способ — заголовок Authorization: Bearer <токен>. Токен бывает нескольких видов.
Токены ServiceAccount. ServiceAccount — единственный объект «учётной записи», который в Kubernetes есть, и он для программ, а не для людей. Его полное имя — system:serviceaccount:<неймспейс>:<имя>, группы — system:serviceaccounts и system:serviceaccounts:<неймспейс>. Токен — это JWT (JSON Web Token: три части в base64 через точку — заголовок, набор утверждений и подпись), подписанный закрытым ключом /etc/kubernetes/pki/sa.key. apiserver проверяет подпись открытым ключом sa.pub, срок, аудиторию и то, что связанные с токеном объекты ещё существуют.
Токены выдаёт API TokenRequest — подресурс serviceaccounts/token. Каждый токен:
- ограничен по сроку (
exp):kubectl create tokenпо умолчанию даёт час, меньше 10 минут не выдаёт; - ограничен по аудитории (
aud): для кого выпущен. apiserver принимает только токены со своей аудиторией (по умолчанию — адресhttps://kubernetes.default.svc.cluster.local), а токен, выпущенный для внешней системы (kubectl create token --audience=vault.example.com), у apiserver не пройдёт, и наоборот; - привязан к объекту: токен, выданный поду, содержит его имя и UID и перестаёт приниматься, как только под удалён.
В под токен попадает проецируемым томом (projected volume) kube-api-access-…: kubelet сам запрашивает токен через TokenRequest, кладёт его в /var/run/secrets/kubernetes.io/serviceaccount/token вместе с ca.crt (из ConfigMap kube-root-ca.crt, который есть в каждом неймспейсе) и файлом namespace, и обновляет до истечения — после 80% срока. Срок такого токена — около часа, но для совместимости со старыми клиентскими библиотеками, не умеющими перечитывать файл, apiserver по умолчанию продлевает его ровно до года и ведёт учёт таких «устаревших» использований в метриках и в журнале аудита (записи apiserver о том, кто какой запрос сделал).
Раньше, до 1.24, для каждого ServiceAccount автоматически создавался Secret с бессрочным токеном, не привязанным ни к чему. Сейчас такие Secret не создаются, а неиспользуемые старые сначала помечаются недействительными, а затем удаляются. Создать бессрочный токен руками (Secret типа kubernetes.io/service-account-token) по-прежнему можно, но это стоит делать только для внешней системы, которая иначе не умеет, и с отдельным ServiceAccount минимальных прав.
Bootstrap-токены — короткоживущие токены вида abcdef.0123456789abcdef в Secret в kube-system, которыми новые узлы аутентифицируются при kubeadm join (часть 4), чтобы запросить себе сертификат.
Статический файл токенов (--token-auth-file) — CSV с токенами и именами, читаемый при старте apiserver. Существует, но для людей не годится: токены бессрочные, менять — перезапуском apiserver.
Webhook. apiserver отправляет токен на внешний HTTPS-сервис объектом TokenReview и получает в ответ имя и группы. Так интегрируются облачные провайдеры и самописные системы.
OIDC: вход людей через провайдера
Для людей правильный механизм — OpenID Connect (OIDC): слой поверх OAuth 2.0 (протокола, которым приложение получает от провайдера доступ от имени пользователя, не видя его пароля), при котором пользователь входит в стороннем провайдере удостоверений и получает ID-токен: JWT, подписанный провайдером, с утверждениями о том, кто он (sub, email) и в каких группах (groups). Провайдером может быть Keycloak (открытый сервер единого входа) или Dex — лёгкий посредник, который сам проверяет пользователя в GitHub, в Active Directory или в любом каталоге LDAP (протокол чтения каталога пользователей и групп по сети: клиент подключается к серверу каталога, выполняет вход и ищет записи по фильтру). apiserver проверяет подпись ID-токена по открытым ключам провайдера, которые сам скачивает по адресу из документа /.well-known/openid-configuration, и берёт имя и группы из утверждений.
Плюсы перед сертификатами: пароли и второй фактор живут у провайдера, токен живёт минуты, уволенный сотрудник отключается в одном месте, группы приходят из каталога.
Настраивается двумя способами. Старый — флаги --oidc-issuer-url, --oidc-client-id, --oidc-username-claim, --oidc-groups-claim и т. д.: один провайдер, менять — перезапуском. Новый — структурированная конфигурация аутентификации: файл AuthenticationConfiguration, передаваемый флагом --authentication-config. Она стабильна с 1.34, позволяет несколько провайдеров, проверки утверждений на CEL (Common Expression Language — языке выражений без циклов и побочных эффектов, который apiserver вычисляет сам) и перечитывается на лету при изменении файла. Совмещать с флагами --oidc-* нельзя: apiserver с обоими не запустится.
# /etc/kubernetes/auth/authentication-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://sso.example.com/realms/infra
audiences:
- kubernetes
claimValidationRules:
- expression: "claims.email.endsWith('@example.com')"
message: разрешены только адреса @example.com
claimMappings:
username:
claim: email
prefix: "oidc:"
groups:
claim: groups
prefix: "oidc:"
anonymous:
enabled: true
conditions:
- path: /livez
- path: /readyz
- path: /healthz
Что здесь происходит:
urlдолжен совпадать с утверждениемissв токене, аaudiences— сaud: токен, выпущенный провайдером для Grafana, к apiserver не подойдёт. Если сертификат провайдера выдан внутренним CA, его PEM кладут вissuer.certificateAuthority.- Когда имя берётся из
email, apiserver сам проверяет утверждениеemail_verified: приfalseтокен отвергается, при отсутствии утверждения — принимается. Писать для этого своё правило не нужно. claimValidationRules— дополнительные проверки на CEL над утверждениями токена; здесь пускаем только адреса своего домена.- Префикс
oidc:у имён и групп — защита от совпадений: группаadminsиз провайдера не превратится случайно в какую-то внутреннюю, а пользователь с адресом, похожим наsystem:…, станетoidc:system:…и привилегий системного имени не получит. Есть иuserValidationRules— проверки на CEL уже итогового имени и групп; например, правило!user.username.startsWith('system:')нужно тем, кто обходится без префикса. - Блок
anonymousзаодно сужает анонимный доступ до трёх путей проверки здоровья:/versionбез удостоверения теперь отвечает 401.
Клиенту apiserver о причине отказа ничего не сообщает — только Unauthorized. Причина — в журнале apiserver:
"Unable to authenticate the request" err="[invalid bearer token, oidc: email not verified]"
"Unable to authenticate the request" err="[invalid bearer token, oidc: verify token: oidc: expected audience \"kubernetes\" got [\"grafana\"]]"
"Unable to authenticate the request" err="[invalid bearer token, oidc: error evaluating claim validation expression: validation expression 'claims.email.endsWith('@example.com')' failed: разрешены только адреса @example.com]"
Изменённый файл apiserver подхватывает примерно за 15 секунд и пишет в журнал reloaded authentication config. Если в новом файле ошибка (например, незакрытая скобка в выражении CEL), он остаётся на прежней конфигурации, пишет failed to validate authentication config и увеличивает метрику apiserver_authentication_config_controller_automatic_reloads_total{status="failure"}. Но при старте apiserver с ошибочным файлом не запустится, так что проверять файл стоит до перезапуска.
В kubeadm файл подключается через ClusterConfiguration:
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
apiServer:
extraArgs:
- name: authentication-config
value: /etc/kubernetes/auth/authentication-config.yaml
extraVolumes:
- name: auth-config
hostPath: /etc/kubernetes/auth
mountPath: /etc/kubernetes/auth
readOnly: true
pathType: Directory
На новом кластере это часть конфигурации kubeadm init. На работающем те же поля дописывают в ConfigMap kubeadm-config в kube-system (из него kubeadm берёт настройки при обновлении, иначе флаг пропадёт при перегенерации манифеста) и создают манифест apiserver заново командой kubeadm init phase control-plane apiserver --config <файл>; с --dry-run она пишет манифест во временный каталог, и его можно сначала сравнить с текущим.
Сам kubectl в провайдер не ходит. Токен ему выдаёт exec-плагин — внешняя программа, которую kubectl вызывает перед запросом и которая печатает токен в формате ExecCredential. Для OIDC это обычно kubelogin (kubectl oidc-login): он открывает браузер, проводит вход, кеширует и обновляет токен.
kubeconfig
Файл kubeconfig (по умолчанию ~/.kube/config) связывает три списка:
apiVersion: v1
kind: Config
clusters: # куда подключаться
- name: kubernetes
cluster:
server: https://k8s-api.example.com:6443
certificate-authority-data: LS0tLS1CRUdJTi… # CA, которому верить при проверке apiserver
users: # чем представляться
- name: alice-cert
user:
client-certificate-data: LS0tLS1CRUdJTi…
client-key-data: LS0tLS1CRUdJTi…
- name: alice-oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: kubectl
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://sso.example.com/realms/infra
- --oidc-client-id=kubernetes
interactiveMode: IfAvailable
contexts: # пара «кластер + пользователь» и неймспейс по умолчанию
- name: lab-alice
context:
cluster: kubernetes
user: alice-oidc
namespace: demo
current-context: lab-alice
Кластер называется kubernetes, как в admin.conf от kubeadm. Контекст — просто удобное имя для пары; переключение — kubectl config use-context. Пользователь в kubeconfig — это не «пользователь кластера», а способ получить удостоверение; имя alice-cert видно только локально, apiserver узнает имя из CN.
Файл с client-key-data — это пароль: права 600, не в Git, не в общих каталогах. Файл с exec-плагином секретов не содержит, и его можно раздавать.
Выдача доступа администратору сертификатом
Иногда без сертификата не обойтись: провайдера удостоверений нет, нужен аварийный доступ, или это служебная учётная запись внешней системы. Подписать сертификат можно руками ключом CA, но правильнее — через API: объект CertificateSigningRequest (CSR), который одобряет администратор, а подписывает kube-controller-manager тем же CA. Закрытый ключ при этом не покидает машину пользователя.
$ openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout alice.key -out alice.csr -subj "/CN=alice/O=platform-admins"
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: alice
spec:
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K… # base64 -w0 alice.csr
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 604800 # неделя
usages:
- client auth
signerName: kubernetes.io/kube-apiserver-client — встроенный подписчик клиентских сертификатов для apiserver. expirationSeconds — желаемый срок; без него используется значение флага --cluster-signing-duration контроллера, по умолчанию год.
Почему срок так важен: выданный сертификат нельзя отозвать. apiserver не проверяет списки отзыва (CRL) и не спрашивает OCSP — сервис онлайн-проверки статуса сертификата. Сертификат действует, пока не истёк срок или пока CA не заменён. Что остаётся, если ключ alice утёк:
- удалить RoleBinding и ClusterRoleBinding на пользователя
alice— но если права выданы группеplatform-admins, а она прописана в самом сертификате, придётся отбирать права у всей группы; - заменить CA кластера — с перевыпуском всех сертификатов всех компонентов и всех kubeconfig;
- ждать истечения срока.
Отсюда практика: людям — OIDC; сертификаты — только служебным и аварийным учётным записям, с коротким сроком и правами через RoleBinding на имя пользователя, а не на группу из сертификата. И никогда — O=system:masters.
Авторизация: RBAC
После аутентификации запрос описывается атрибутами: пользователь и группы, глагол (get, list, create…), группа API, ресурс и подресурс, неймспейс, имя объекта. Авторизаторы перечислены во флаге --authorization-mode; kubeadm ставит Node,RBAC. Node — специальный авторизатор для kubelet: узел может читать Secret и ConfigMap только тех подов, что запланированы на него, и менять статус только своего объекта Node. Всё остальное решает RBAC (role-based access control).
RBAC — четыре типа объектов группы rbac.authorization.k8s.io:
| Объект | Область | Что делает |
|---|---|---|
| Role | неймспейс | набор правил для ресурсов этого неймспейса |
| ClusterRole | кластер | набор правил: для кластерных ресурсов (Node, PV, CRD), для URL вне ресурсов (/metrics) или как шаблон для многих неймспейсов |
| RoleBinding | неймспейс | выдаёт Role или ClusterRole субъектам — в пределах своего неймспейса |
| ClusterRoleBinding | кластер | выдаёт ClusterRole субъектам во всех неймспейсах |
Правило — это apiGroups + resources + verbs, опционально resourceNames:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: hello-deployer
namespace: demo
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: ["apps"]
resources: ["deployments/scale"]
verbs: ["patch", "update"]
- apiGroups: [""] # "" — основная группа: pods, services, configmaps…
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["hello-config"] # только этот объект
verbs: ["get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-hello-deployer
namespace: demo
subjects:
- kind: ServiceAccount
name: ci
namespace: demo
- kind: Group
name: oidc:hello-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: hello-deployer
apiGroup: rbac.authorization.k8s.io
Детали, которые важны на практике:
- Только разрешения. Запретов в RBAC нет: права складываются из всех привязок, и «всё, кроме Secret» выразить нельзя — только перечислить всё, что можно.
- Глаголы. Для ресурсов:
get,list,watch,create,update,patch,delete,deletecollection. Особые:use(PodSecurityPolicy в прошлом, сейчас — некоторые CRD),bindиescalate(для ролей),impersonate,approveиsign(для CSR).listиwatchотдают объекты целиком — правоlistна Secret равно праву читать их все. - Подресурсы — отдельные ресурсы для RBAC:
pods/log,pods/exec,pods/portforward,pods/eviction,deployments/scale,services/proxy,greetings/status. Право наpodsне даёт права наpods/exec. resourceNamesограничивает правило конкретными объектами, но не работает сcreate(имени ещё нет) и сlist/watchбез фильтра по имени.- RoleBinding на ClusterRole — главный способ переиспользования: одна ClusterRole
hello-deployerи по RoleBinding в каждом неймспейсе, где она нужна. roleRefнеизменяем: чтобы сменить роль, привязку удаляют и создают заново.
Встроенные роли и агрегация
В кластере уже есть роли для людей:
| ClusterRole | Что даёт |
|---|---|
cluster-admin |
всё на всё; через ClusterRoleBinding — на весь кластер, через RoleBinding — полный контроль одного неймспейса |
admin |
почти всё в неймспейсе, включая Role и RoleBinding внутри него; квоты (resourcequotas) только читает, сам неймспейс не меняет |
edit |
чтение и запись большинства объектов, включая Secret, pods/exec и выпуск токенов любого ServiceAccount неймспейса; без ролей и привязок |
view |
чтение большинства объектов, кроме Secret; без pods/exec |
admin, edit и view — агрегированные роли: их правила не записаны в них самих, а собираются контроллером из всех ClusterRole с определёнными метками. Так CRD из прошлой части становится доступен держателям стандартных ролей без правки самих ролей:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: greetings-view
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rules:
- apiGroups: ["demo.example.com"]
resources: ["greetings"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: greetings-edit
labels:
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rules:
- apiGroups: ["demo.example.com"]
resources: ["greetings", "greetings/scale"]
verbs: ["create", "update", "patch", "delete", "deletecollection"]
Хорошие операторы ставят такие роли сами; при оценке оператора (часть 28) стоит проверить, не добавляет ли он в view чтение чего-то чувствительного.
Помимо ролей для людей, есть десятки system:* ролей для компонентов (system:kube-scheduler, system:node, system:controller:*): на стенде из 84 ClusterRole 66 — системные. Их не трогают: apiserver восстанавливает их при каждом запуске.
Опасные права
RBAC выглядит так, будто права независимы. На деле некоторые из них — ступенька к другим, и выдача одного равносильна выдаче большего.
get, list, watch на secrets. Прямое чтение всех паролей, ключей и токенов неймспейса (или кластера, если через ClusterRoleBinding). list достаточно — get по имени не нужен.
create на pods (и на всё, что создаёт поды: deployments, jobs, cronjobs, daemonsets, statefulsets). Создатель пода выбирает, что в него смонтировать и под каким ServiceAccount его запустить. Значит, он может:
- смонтировать любой Secret неймспейса томом и прочитать его — права на
secretsему не нужны, kubelet прочтёт Secret сам; - запустить под от любого ServiceAccount неймспейса и получить его токен — то есть все его права;
- если Pod Security не запрещает, создать привилегированный под с
hostPath: /и стать root на узле (часть 30).
Поэтому create pods в неймспейсе — фактически права всех ServiceAccount и доступ ко всем Secret этого неймспейса.
create на pods/exec, pods/attach. Выполнение команд в существующих подах — с их токенами, их файлами и их сетевым доступом. kubectl exec открывает соединение по WebSocket — протоколу, который начинается с обычного HTTP-запроса GET с заголовком Upgrade: websocket и после ответа 101 Switching Protocols превращает то же TCP-соединение в двусторонний канал для потоков ввода и вывода. Поэтому исторически роли, где на pods/exec выдан только get, фактически давали exec. С 1.35 (бета, включено по умолчанию) apiserver требует для такого подключения ещё и create. Пользователь bob, которому Role выдаёт get и list на pods и только get на pods/exec, получает отказ:
error: unable to upgrade connection: pods "ci-runner-8695fbd774-jdh5k" is forbidden: User "bob" cannot create resource "pods/exec" in API group "" in the namespace "demo"
Но роли с get на pods/exec всё равно стоит считать опасными: на кластерах старше 1.35 они дают exec.
escalate и bind. RBAC защищён от повышения прав: создать Role или RoleBinding можно, только если у вас уже есть все права, которые они выдают. Даже с правом создавать роли alice получит отказ, если в роли есть то, чего нет у неё самой:
$ kubectl -n demo create role sneaky --verb=get --resource=secrets
Error from server (Forbidden): roles.rbac.authorization.k8s.io "sneaky" is forbidden: user "alice" (groups=["platform-admins" "system:authenticated"]) is attempting to grant RBAC permissions not currently held:
{APIGroups:[""], Resources:["secrets"], Verbs:["get"]}
escalate на roles/clusterroles снимает эту защиту при создании ролей, bind — при привязке ролей, прав которых у вас нет. Любой из них — это путь к cluster-admin.
impersonate на users, groups, serviceaccounts. Позволяет выполнять запросы от чужого имени (заголовки Impersonate-User, Impersonate-Group; в kubectl — --as, --as-group). Тот, кто может притвориться группой kubeadm:cluster-admins, — администратор. С 1.36 включена по умолчанию (бета) ограниченная имперсонация: глагол impersonate:user-info на ресурс users с resourceNames разрешает притворяться только названными пользователями, а глаголы вида impersonate-on:user-info:list на обычные ресурсы — делать от их имени только перечисленные действия:
rules: # ClusterRole: кем можно притворяться
- apiGroups: ["authentication.k8s.io"]
resources: ["users"]
resourceNames: ["bob"]
verbs: ["impersonate:user-info"]
---
rules: # Role в demo: что можно делать от его имени
- apiGroups: [""]
resources: ["pods"]
verbs: ["impersonate-on:user-info:list"]
$ kubectl -n demo get pods --as=bob
NAME READY STATUS RESTARTS AGE
ci-runner-8695fbd774-jdh5k 1/1 Running 0 9m
hello-6b8ccbff4c-jjdqz 1/1 Running 0 9m
$ kubectl -n demo delete pod ci-runner-8695fbd774-jdh5k --as=bob
Error from server (Forbidden): pods "ci-runner-8695fbd774-jdh5k" is forbidden: User "alice" cannot impersonate-on:user-info:delete resource "pods" in API group "" in the namespace "demo"
$ kubectl -n demo get pods --as=carol
Error from server (Forbidden): users.authentication.k8s.io "carol" is forbidden: User "alice" cannot impersonate:user-info resource "users" in API group "authentication.k8s.io" at the cluster scope
Запрос при этом проходит обе проверки: собственные права bob и разрешение alice действовать от его имени. Для ServiceAccount и узлов есть свои глаголы (impersonate:serviceaccount и т. п.). Старый глагол impersonate продолжает работать и даёт имперсонацию без ограничений.
nodes/proxy. Прямой доступ к API kubelet на узлах через apiserver — а это exec в любой под узла, чтение логов и метрик. В 1.36 стала стабильной детальная авторизация kubelet: для метрик, статистики и логов есть отдельные подресурсы nodes/metrics, nodes/stats, nodes/log, и системам мониторинга теперь можно не давать nodes/proxy. Но сам nodes/proxy остаётся равным root на всех узлах.
create на serviceaccounts/token. Выпуск токенов от имени любого ServiceAccount неймспейса — то же, что владение ими. Встроенная роль edit это право даёт.
update/patch на validatingwebhookconfigurations, mutatingwebhookconfigurations. Можно зарегистрировать вебхук, который будет получать все создаваемые объекты, включая Secret, и подменять их.
approve на certificatesigningrequests вместе с правом создавать CSR — выпуск себе клиентского сертификата с любым CN и O.
Отсюда правило ревью: в Role и ClusterRole не должно быть * ни в verbs, ни в resources, ни в apiGroups — кроме cluster-admin.
Минимальные права для приложения
Каждый неймспейс получает ServiceAccount default, и каждый под без явного serviceAccountName запускается от него. Сам по себе default прав не имеет, но его токен монтируется в каждый под. Если кто-то однажды выдаст права default («чтобы заработало»), их получат все поды неймспейса. А токен, лежащий в контейнере, — первое, что ищет атакующий, получивший выполнение кода в приложении.
Правильно:
- Приложению, которому API не нужен (а это почти все:
hello, базы, веб-серверы), токен не монтировать:
apiVersion: v1
kind: ServiceAccount
metadata:
name: hello
namespace: demo
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
namespace: demo
spec:
template:
spec:
serviceAccountName: hello
automountServiceAccountToken: false
…
Поле есть и у ServiceAccount, и у пода; значение пода важнее. Ставить его в обоих местах — защита от того, что кто-то поменяет одно из них.
Приложению, которому API нужен (контроллер, оператор, CI) — отдельный ServiceAccount, Role вместо ClusterRole, если хватает одного неймспейса, только нужные глаголы и, где возможно,
resourceNames.Внешней системе (CI-серверу вне кластера) — короткоживущие токены через
kubectl create tokenс--durationи--audienceпо мере надобности, а лучше — федерация удостоверений: если CI умеет выдавать OIDC-токены своим заданиям, его можно прописать вAuthenticationConfigurationкак ещё одного провайдера, и в Kubernetes не будет храниться ни одного долгоживущего секрета.
Смотрим руками
Для опытов в неймспейсе demo нужны два ServiceAccount и два Deployment: hello из прошлых частей, переведённый на ServiceAccount hello без токена (как в разделе выше), и ci-runner — busybox от ServiceAccount ci с токеном по умолчанию:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci
namespace: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci-runner
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ci-runner
template:
metadata:
labels:
app.kubernetes.io/name: ci-runner
spec:
serviceAccountName: ci
containers:
- name: runner
image: busybox:1.37
command: ["sleep", "infinity"]
Кто я:
$ kubectl auth whoami
ATTRIBUTE VALUE
Username kubernetes-admin
Groups [kubeadm:cluster-admins system:authenticated]
Extra: authentication.kubernetes.io/credential-id [X509SHA256=6ff9adf123f0ab47fe9d5bb2f450928eefcf619e3f52b22f46d3d85b4a5e92d2]
Команда спрашивает apiserver (ресурс SelfSubjectReview) и показывает, каким он видит текущее удостоверение — полезно, когда непонятно, какой из пользователей kubeconfig сработал. В Extra apiserver кладёт отпечаток SHA-256 клиентского сертификата: по нему запросы конкретного сертификата находятся в журнале аудита.
Анонимный доступ — без kubeconfig, просто curl:
$ curl -sk https://k8s-api.example.com:6443/livez
ok
$ curl -sk https://k8s-api.example.com:6443/api
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/api\"",
"reason": "Forbidden",
"details": {},
"code": 403
}
/version тоже отвечает анонимно, а всё остальное — 403: имя system:anonymous apiserver присвоил, но RBAC ему ничего не дал.
Выдаём доступ alice через CSR:
$ kubectl apply -f alice-csr.yaml
certificatesigningrequest.certificates.k8s.io/alice created
$ kubectl get csr alice
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
alice 0s kubernetes.io/kube-apiserver-client kubernetes-admin 7d Pending
$ kubectl certificate approve alice
certificatesigningrequest.certificates.k8s.io/alice approved
$ kubectl get csr alice
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
alice 2s kubernetes.io/kube-apiserver-client kubernetes-admin 7d Approved,Issued
$ kubectl get csr alice -o jsonpath='{.status.certificate}' | base64 -d > alice.crt
$ openssl x509 -in alice.crt -noout -subject -issuer -dates
subject=O=platform-admins, CN=alice
issuer=CN=kubernetes
notBefore=Nov 1 19:54:51 2026 GMT
notAfter=Nov 8 19:54:51 2026 GMT
notBefore на пять минут раньше одобрения: подписчик сдвигает начало срока назад на случай расхождения часов. Добавляем пользователя и контекст в kubeconfig:
$ kubectl config set-credentials alice-cert --client-certificate=alice.crt --client-key=alice.key --embed-certs=true
User "alice-cert" set.
$ kubectl config set-context lab-alice-cert --cluster=kubernetes --user=alice-cert --namespace=demo
Context "lab-alice-cert" created.
$ kubectl --context lab-alice-cert get pods
Error from server (Forbidden): pods is forbidden: User "alice" cannot list resource "pods" in API group "" in the namespace "demo"
$ kubectl --context lab-alice-cert auth whoami
ATTRIBUTE VALUE
Username alice
Groups [platform-admins system:authenticated]
Extra: authentication.kubernetes.io/credential-id [X509SHA256=6c543d4596112bd6ce44b45ad6ec15db737eee77ecd758a851f3b57b34d7dab4]
Аутентификация прошла — apiserver знает её имя и группы, auth whoami разрешён любому аутентифицированному, — но прав на поды нет. Выдаём view в demo на имя пользователя и проверяем от имени администратора через имперсонацию:
$ kubectl -n demo create rolebinding alice-view --clusterrole=view --user=alice
rolebinding.rbac.authorization.k8s.io/alice-view created
$ kubectl -n demo auth can-i list pods --as=alice
yes
$ kubectl -n demo auth can-i get secrets --as=alice
no
$ kubectl -n demo auth can-i create pods/exec --as=alice
no
$ kubectl auth can-i list pods --as=alice -n kube-system
no
auth can-i использует API SubjectAccessReview: apiserver прогоняет запрос через авторизаторы, ничего не выполняя. Полный список прав субъекта в неймспейсе:
$ kubectl -n demo auth can-i --list --as=system:serviceaccount:demo:hello
Resources Non-Resource URLs Resource Names Verbs
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
globalnetworkpolicies.projectcalico.org [] [] [get list watch create update patch delete deletecollection]
networkpolicies.projectcalico.org [] [] [get list watch create update patch delete deletecollection]
[/.well-known/openid-configuration/] [] [get]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks/] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
У ServiceAccount hello только то, что есть у любого аутентифицированного: узнать о себе, прочитать список API и служебные URL. Пути повторяются, потому что их выдают две роли: system:public-info-viewer (всем, включая анонимов) и system:discovery (аутентифицированным). /.well-known/openid-configuration и /openid/v1/jwks — открытые ключи подписи токенов ServiceAccount: их всем ServiceAccount выдаёт роль system:service-account-issuer-discovery, чтобы внешние системы могли проверять токены подов.
Неожиданные строки — полные права на networkpolicies.projectcalico.org и globalnetworkpolicies.projectcalico.org. Их выдаёт всем аутентифицированным ClusterRoleBinding calico-tiered-policy-passthrough, а настоящую проверку делает сам Calico: политики Calico разложены по уровням (tiers), и его API-сервер требует ещё права на уровень. Без них отказ:
$ kubectl -n demo get networkpolicies.projectcalico.org --as=system:serviceaccount:demo:hello
Error from server (Forbidden): networkpolicies.projectcalico.org is forbidden: operation on Calico tiered policy is forbidden
Отсюда важное ограничение auth can-i: он показывает только решение RBAC в kube-apiserver. Агрегированные API (часть запросов kube-apiserver передаёт отдельным API-серверам, как calico-apiserver) могут проверять что-то ещё.
Токен ServiceAccount и его содержимое:
$ kubectl -n demo create token hello --duration=10m | cut -d. -f2 | base64 -d 2>/dev/null | jq
{
"aud": [
"https://kubernetes.default.svc.cluster.local"
],
"exp": 1793563801,
"iat": 1793563201,
"iss": "https://kubernetes.default.svc.cluster.local",
"jti": "2588cc28-1335-4888-9a21-e9dec8cca842",
"kubernetes.io": {
"namespace": "demo",
"serviceaccount": {
"name": "hello",
"uid": "fa0b64f7-c3a0-455d-b0c8-833e4c88b67e"
}
},
"nbf": 1793563201,
"sub": "system:serviceaccount:demo:hello"
}
Выпустить токен можно и ServiceAccount без автомонтирования: automountServiceAccountToken управляет только томом в поде. Короче 10 минут apiserver токен не выдаст:
$ kubectl -n demo create token hello --duration=5m
error: failed to create token: TokenRequest.authentication.k8s.io "" is invalid: spec.expirationSeconds: Invalid value: 300: may not specify a duration less than 10 minutes
А вот токен, который kubelet смонтировал в под ServiceAccount ci (у него автомонтирование не выключено), — с привязкой к поду и узлу:
$ kubectl -n demo exec deploy/ci-runner -- cat /var/run/secrets/kubernetes.io/serviceaccount/token \
| cut -d. -f2 | base64 -d 2>/dev/null | jq '{iat, exp, "kubernetes.io"}'
{
"iat": 1793563172,
"exp": 1825099172,
"kubernetes.io": {
"namespace": "demo",
"node": {
"name": "k8s-node-1",
"uid": "60c90f02-4a49-4cce-b306-dcefb70c0b9f"
},
"pod": {
"name": "ci-runner-8695fbd774-jdh5k",
"uid": "054e17af-58e3-419e-b294-f3d716236fd3"
},
"serviceaccount": {
"name": "ci",
"uid": "8758de5d-da1d-479a-9e3a-2581c6c25214"
},
"warnafter": 1793566779
}
}
exp ровно через год после iat (продление для старых клиентов), а warnafter — настоящий срок, 3607 секунд: если токен предъявлен после него, apiserver засчитает это в метрику устаревших токенов. Откуда взялся файл, видно в спецификации пода — это проецируемый том:
$ kubectl -n demo get pod -l app.kubernetes.io/name=ci-runner -o json | jq '.items[0].spec.volumes'
[
{
"name": "kube-api-access-q6ft5",
"projected": {
"defaultMode": 420,
"sources": [
{
"serviceAccountToken": {
"expirationSeconds": 3607,
"path": "token"
}
},
{
"configMap": {
"items": [
{
"key": "ca.crt",
"path": "ca.crt"
}
],
"name": "kube-root-ca.crt"
}
},
{
"downwardAPI": {
"items": [
{
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.namespace"
},
"path": "namespace"
}
]
}
}
]
}
}
]
У пода hello с automountServiceAccountToken: false такого тома нет вовсе — список томов пуст (а заглянуть внутрь контейнера не получится: в distroless-образе hello нет ни оболочки, ни ls):
$ kubectl -n demo get pod -l app.kubernetes.io/name=hello -o jsonpath='{.items[0].spec.volumes}'
И как устроена агрегация:
$ kubectl get clusterrole view -o jsonpath='{.aggregationRule}{"\n"}'
{"clusterRoleSelectors":[{"matchLabels":{"rbac.authorization.k8s.io/aggregate-to-view":"true"}}]}
$ kubectl get clusterrole view -o jsonpath='{range .rules[*]}{.apiGroups}{.resources}{"\n"}{end}' | grep demo
["demo.example.com"]["greetings"]
Правило из greetings-view появилось в view само — его дописал контроллер агрегации в kube-controller-manager. Самого CRD в кластере может и не быть: роли ссылаются на ресурс по имени.
Последнее: кто в кластере имеет опасные права. Встроенного отчёта нет, но перебор привязок к cluster-admin делается одной командой:
$ kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | "\(.metadata.name): \([.subjects[]? | "\(.kind)/\(.name)"] | join(", "))"'
cluster-admin: Group/system:masters
kubeadm:cluster-admins: Group/kubeadm:cluster-admins
Если в этом списке появится ServiceAccount какого-нибудь чарта — это повод разобраться до того, как разбираться придётся после инцидента.
Уборка: kubectl delete csr alice, kubectl delete clusterrole greetings-view greetings-edit, пользователь и контекст alice-cert — kubectl config delete-user alice-cert и kubectl config delete-context lab-alice-cert.
Аутентификация и RBAC определяют, кто и что может сделать с объектами API. Но то, что окажется внутри пода, — от чьего имени процесс работает на узле, какие у него capabilities, может ли он смонтировать каталог узла, — RBAC не регулирует: право create pods даёт всё или ничего.
Источники
- Authenticating — аутентификаторы, сертификаты X.509, токены, OIDC,
AuthenticationConfiguration, анонимный доступ,kubectl auth whoami. - kube-apiserver Configuration (v1) — поля
AuthenticationConfiguration. - Service Accounts и Managing Service Accounts — TokenRequest, проецируемые токены, продление срока, удаление старых Secret.
- Certificates and Certificate Signing Requests — CSR, подписчики,
expirationSeconds. - Organizing Cluster Access Using kubeconfig Files — устройство kubeconfig.
- Using RBAC Authorization — Role, ClusterRole, привязки, агрегация, встроенные роли, защита от повышения прав.
- Role Based Access Control Good Practices — опасные права.
- User Impersonation — имперсонация и ограниченная имперсонация.
- Using Node Authorization и Kubelet authentication/authorization — авторизатор Node, подресурсы
nodes/*. - Customizing components with the kubeadm API —
extraArgsиextraVolumesвClusterConfiguration. - OpenID Connect Core 1.0 и RFC 7519 — ID-токен и JWT.
- Configure RBAC for tiered policies — права на политики Calico по уровням.
Дальше — безопасность подов: securityContext, запуск не от root, capabilities, seccomp и AppArmor, user namespaces, стандарты Pod Security и их применение через Pod Security Admission, и собственные правила допуска на CEL.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.