Мы уже писали в блоге о Structured Authentication Config — крупнейшем изменении системы аутентификации в K8s за последние годы, которое появилось в версии 1.29. В этой же статье я подробнее расскажу о предпосылках появления KEP 3331 и сценариях, в которых полезен новый аутентификатор.
Меня зовут Максим Набоких. Я руковожу разработкой Deckhouse Kubernetes Platform, а ещё вхожу в состав Kubernetes sig-auth, вношу свой вклад в развитие оркестратора в целом и являюсь основным мейнтейнером Dex. Я один из тех, кто участвовал в разработке Structured Authentication Config, и хочу, чтобы сообщество переходило на него со старых аутентификаторов.
Как устроена аутентификация в Kubernetes
Если вы хорошо знакомы с работой аутентификации, можете перейти сразу к разделу о её проблемах на примере OpenID Connect. Остальным читателям предлагаю вместе быстро вспомнить, как устроена аутентификация в Kubernetes.
Итак, аутентификация позволяет понять, кто хочет выполнить задачу, и убедиться, что этот «кто-то» и вправду тот, за кого себя выдаёт. В мире Kubernetes нет пользователей (user). Вместо них оркестратор опирается на концепцию субъектов (subjects). Субъект — это минималистичный стандартный интерфейс для отображения тех атрибутов, что необходимы для предоставления прав доступа. Сущность, которая будет выполнять действия в кластере.
Субъекты физически не существуют в Kubernetes. Их атрибуты вычисляются на лету. Поэтому неважно, кто вы — пользователь или машина. При аутентификации в Kubernetes вы будете для оркестратора просто субъектом.
Существуют различные варианты (flows), с помощью которых можно аутентифицироваться в Kubernetes. Но структура атрибутов для субъекта всегда одинакова:
uid : "..."
name : "..."
groups : [...]
extra : {...}
Между вами как пользователем и субъектом в мире Kubernetes стоят аутентификаторы. Они встроены в код kube-apiserver и отвечают за извлечение атрибутов субъекта из данных аутентификации.
Существуют две группы аутентификаторов — внутренние и внешние. Внутренние для аутентификации полагаются только на данные Kubernetes — ключи и сертификаты. Внешние полагаются на сторонние приложения, например прокси для аутентификации или провайдер OpenID Connect. Примеры внутренних аутентификаторов — это Service Account, Bootstrap Token и сертификаты x509. Внешних — Token Webhook, OpenID Connect и AuthProxy.
Аутентификаторы извлекают атрибуты субъекта из данных аутентификации по-разному. Приведу три примера.
Внутренний аутентификатор x509 проверяет, выпущен ли сертификат клиента Certificate Authority (CA) Kubernetes. Если да, то общим именем сертификата (CN) будет username, а атрибутом ORG будут группы этого субъекта.
CN = username
ORG = group-1
ORG = group-2
Второй пример — Service Account. Когда приходит запрос с токеном учётной записи сервиса, этот аутентификатор проверяет, что токен подписан Kubernetes, а также что указанный сервисный аккаунт существует в кластере. Если всё в порядке, атрибуты субъекта будут следующими:
system:serviceaccount:<ns>:<name> = username
[system:serviceaccount, system:serviceaccount:<ns>} = groups
И третий пример — аутентификатор OpenID Сonnect. Вы можете подключить к своему кластеру внешнего провайдера OpenID. В таком случае аутентификатор проверит, что токен подписан этим провайдером. Затем он убедится, что токен действителен, проверит его срок действия, проведёт проверку клиента и слушателей. Если всё в порядке, Kubernetes извлечёт атрибуты субъекта из объектов claim. А claim можно настраивать, и они могут быть разными.
name claim = username
group claim = groups
Для аутентификации в Kubernetes используются конфигурационные файлы kubeconfig. В этих файлах три секции: секция пользователя, кластера и контекста.
users : [...]
clusters : [...]
context : [{user,cluster}]
В секции user
хранятся данные аутентификации. Это ваш паспорт в мире Kubernetes. В качестве такого паспорта можно использовать:
клиентские сертификаты x509;
базовые учётные данные аутентификации;
токен Bearer;
сторонний exec-плагин, который вернёт либо сертификат клиента, либо токен Bearer.
Секция clusters
хранит адрес кластера, в который будут направляться запросы.
Секция context
показывает, какой пользователь к какому кластеру обращается.
Итак, у нас есть файл kubeconfig
. Как узнать, какой субъект в нём представлен? Раньше это было невозможно, отлаживать аутентификацию Kubernetes было сложно. Но в 2023 году мы добавили эту возможность в kubectl. Так что теперь вы можете легко узнать, кто вы. Просто введите в терминал команду $ kubectl auth whoami
и получите вывод атрибутов вашего субъекта со значениями:
ATTRIBUTE VALUE
Username john.doe@example.com
Groups [system:masters developers:mainteiners system:aithenticated]
Проблемы аутентификации на примере OpenID Connect
Давайте погрузимся немного глубже и поговорим о проблемах аутентификации на примере OpenID Сonnect. Это безопасный и самый популярный внешний аутентификатор. Википедия говорит, что в 2016 году стандарт OpenID поддерживало больше 1,1 млн сайтов, полагаю, сейчас их гораздо больше. Многие компании используют провайдер OpenID Сonnect. Например, если вы храните свой код на GitHub или GitLab, то имеете дело именно с ним.
Но популярность не означает, что с аутентификатором OpenID Connect нет проблем.
Первая из них заключается в том, что настроить аутентификатор можно только с помощью аргументов CLI. Вот пример аргументов, которые обычно используются для настройки:
--oidc-issuer= ...
--oidc-groups-claim= ...
--oidc-ca= ...
Это негибкий подход: задать сложные настройки не получится. Менять значения тоже неудобно, ведь в том случае, если вы захотите перенастроить аутентификацию, придётся перезапустить Kube API. А это плохо, поскольку в числе прочего нужно будет отключить всех клиентов, что может привести к сбою в работе.
Ещё одна сложность состоит в том, что вы можете подключить к Kubernetes только одного провайдера. Я не знаю, почему появилось это ненужное ограничение, но именно из-за него существуют такие решения, как Dex. Его можно использовать в качестве сплиттера OpenID Connect для подключения многих провайдеров к Kubernetes.
Третья проблема — аутентификатор OpenID Connect был разработан ещё в 2015 году и у него плохое тестовое покрытие. Добавлять новые фичи в этот аутентификатор просто страшно.
И последняя сложность заключается в том, что проверка токенов для этого аутентификатора довольно странная. Она во многом полагается на формат токенов OpenID, и вы не можете проверить подлинность токенов в других форматах, например токенов SPIFFE. Можно разработать свой вебхук для таких токенов, но это приведёт к усложнению инфраструктуры, потому что нужно будет развернуть вебхук и обеспечить его высокую доступность.
В сумме эти проблемы приводят к:
неудовлетворённым пользователям;
низкому уровню принятия фич в аутентификацию в Kubernetes;
обходным путям для внедрения фич, которые хотели получить пользователи.
Мотивация SIG Auth для изменений аутентификации
SIG Auth — это группа людей, которые занимаются фичами аутентификации в Kubernetes. Я был одним из участников обсуждения существующих проблем и хочу рассказать вам о двух основных причинах для изменений, которыми мы руководствовались.
Во-первых, хотелось сделать что-то расширяемое — добавить больше возможностей для аутентификации. Во-вторых, хотелось покрыть открытые issues — на GitHub их накопилось прилично. Разные пользователи просили свои варианты использования аутентификации, и мы хотели охватить их все.
Результатом обсуждения стала разработка нового аутентификатора — Structured Authentication Configuration.
Новый аутентификатор: Structured Authentication Config
Главное отличие нового аутентификатора — структурированная конфигурация. Ему нужен единственный CLI-аргумент, который указывает путь к файлу конфигурации с определённой структурой:
--authentication-config = <path-to-config-file>
Теперь при изменениях настроек Kubernetes API-сервер будет сам перезагружать конфигурацию. Вам больше не понадобится перезапускать свой API-сервер. То есть одна из описанных выше проблем уже решена.
Сама структура файла конфигурации выглядит так:
apiVersion: apiserver.config.k8s.io/v1alpha1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://example.com
clientIDs:
- my-app
claimValidationRules: [...]
claimMappings: {...}
userInfoValidationRules: [...]
Единственный верхний параметр jwt
(JSON Web Token) — это массив аутентификаторов. Благодаря нему вы можете указать множество аутентификаторов в конфигурации. Внутри параметра находятся ключи — правила валидации, сопоставления claim’ов из токена с атрибутами субъекта и валидации информации о пользователе. Я расскажу о них позже в статье.
Structured Authentication Config заменяет OpenID Сonnect. Поэтому вы можете использовать либо только новый аутентификатор, либо только старый. Использовать их одновременно не получится.
superseds OIDC: either --oidc-*
or --authentication-config
Этапы аутентификации
Этапы аутентификации со Structured Authentication Config выглядят следующим образом:
Сначала аутентификатор получает запрос с токеном. Затем выполняются несколько базовых проверок токена: проверка подписи, клиента, срока действия. Если базовые проверки пройдены, мы переходим к проверке утверждения (claim validation). Как раз на этом этапе вы можете предоставить свои собственные проверки. Для этого используется Common Expression Language (CEL).
Если с токеном всё в порядке, мы переходим к этапу сопоставления утверждений (claim mapping). Здесь утверждения токена соотносятся с атрибутами субъекта. Это тоже происходит с помощью CEL. И на последнем этапе аутентификации, когда мы уже извлекли атрибуты субъекта, можно применить к ним дополнительные проверки. Например, провалидировать политики аутентификации.
Common Expression Language
Пару слов о том, почему мы выбрали CEL, и его преимуществах. Это язык выражений c Apache License, разработанный компанией Google. Он очень быстрый, во многих случаях быстрее компиляции. Его легко расширять: вы можете написать собственную библиотеку и подключить её к среде выполнения CEL.
В СEL можно извлекать значения, потому что это язык запросов. Также он позволяет проверять возвращаемый тип: мы можем проверить, является ли возвращаемый тип булевым, массивом или строкой. И эта проверка отлично подходит для валидации, потому что уже используется в этих целях во многих местах в Kubernetes: например, в CustomResourceDefinitions или как отдельная вещь для проверки политики допуска (validating admission policy).
Какие проблемы решает Structured Authentication Config
Итак, в Kubernetes появился гибкий и расширяемый аутентификатор. Но какие именно задачи он позволяет решать? Рассмотрим пять примеров.
Использование разных URI для обнаружения и проверки эмитента
У всех провайдеров OpenID Connect есть адрес, например https:oidc.example.com
. Он используется для двух целей:
Для доступа к эндпоинту
/.well-known/openid-configuration
, чтобы получить ключи провайдера для валидации.Для подтверждения принадлежности токена эмитенту OpenID Connect.
Когда провайдер OpenID Connect находится за пределами кластера Kubernetes, всё в порядке. Мы используем внешний адрес для доступа к нему, и всё отлично работает даже в прежней версии аутентификатора.
Но что произойдёт, если провайдер OpenID Connect находится в том же кластере, что и kube-apiserver?
Не хочется, чтобы внешний адрес ходил во внешнюю сеть, а затем через ингресс возвращался в кластер. Такое переключение сетевого контекста не очень хорошо с точки зрения производительности и безопасности, потому что трафик уходит за пределы вашего кластера.
Structured Authentication Config позволяет иметь отдельные URI для обнаружения и проверки эмитента. В конфигурации это выглядит довольно просто: первый URL — это базовый эмитент. Второй, если он есть, используется для discovery-вызовов. Больше никаких запросов за пределы кластера:
jwt:
- issuer:
url: https://oidc.example.com./
discoveryURL: https//oidc.ns.svc.cluster.local
Валидация на основе кастомных запросов
Structured Authentication Config позволяет проводить валидацию токена на основе пользовательских запросов. Раньше это было невозможно.
Представим, что у нас есть следующие данные о пользователе:
{
"phone_number": +7 929 _",
"phone _number_verified": true,
"picture": "https://gravatar.com/path/to/pic"
}
Теперь на основе этих данных мы можем проводить дополнительные проверки во время аутентификации. Например, добавить валидацию по номеру телефона с помощью простого CEL-выражения:
- expression: !claims.phone_number.startsWith("+7")
message: Only Russian phone numbers are allowed
Здесь claims
— это объект, в котором хранятся все утверждения. Мы получаем из него номер телефона, а затем проверяем, что тот начинается с телефонного кода «+7». И если это не так, выводим сообщение, что аутентифицироваться в этом кластере могут только владельцы российского номера.
При этом использовать CEL не обязательно. Да, он быстрый, но затраты на вычисление выражений всё равно не нулевые. Поэтому в новой аутентификации осталась простая валидация для случаев, когда нужно проверить, что определённое утверждение имеет конкретное значение:
- claim: phone_number_verifie
requiredValue: true
Приведу также сложный пример использования CEL. Он немного странный, но всё же:
- expression: isURL(claims.picture)
&& URL(claims.picture).getHostname() == "gravatar.com"
message: Only gravatar images can be used as profile pictures
В Kubernetes есть библиотека для URL-адресов, и вы можете манипулировать ими. В примере выше мы проверяем, что картинка профиля соответствует URL. И если да, то извлекаем имя хоста и проверяем, что оно равно gravatar.com
. И если снова да, то мы разрешаем пользователю попасть в кластер, а если нет, то говорим, что нужно изменить картинку профиля.
Плюсы Structured Authentication Config здесь в том, что он по-настоящему гибкий. А ещё вы можете писать кастомные ошибки, что улучшает пользовательский опыт.
Конвертация утверждений в атрибуты субъекта с помощью преобразований
Представим, что у нас есть следующие данные:
{
"name": "jane.doe",
"sub": "550e8400-e29b-41d4",
"roles": "admin,user"
}
С помощью CEL мы можем забрать claim "name"
и при помощи префикса указать, что пользователь из GitLab.
username:
expression: "gitlab:" + claims.name
Результат будет таким:
Username: "gitlab:jane.doe"
Опять же, использовать CEL не обязательно. Можно извлечь значение верхнего утверждения в качестве атрибута пользователя. Например, использовать ключ токена sub
для ID:
uid:
claim: sub
Результат выполнения:
UID: "550e8400-e29b-41d4"
И более сложный пример с CEL. В приведённых данных утверждение роли — это не массив, а разделённая запятой строка "roles": "admin,user".
Раньше использовать такое утверждение в Kubernetes было невозможно, нужно было как-то преобразовать его в массив строк. Но теперь благодаря CEL можно обойтись без лишних преобразований.
Мы берём утверждение роли, разделяем его запятой с помощью функции claims.roles.split
и добавляем префикс gitlab к получившимся группам, как и в первом примере:
groups:
expression: claims.roles.split(",")
.map(g, "gitlab:"+g)
В итоге получаем следующие атрибуты пользователя:
Groups: ["gitlab:admin", "gitlab:user"]
Structured Authentication Config даёт проникнуть глубже в токен, и нам больше не нужно полагаться только на утверждения первого уровня. Мы можем изменять claim values по своему усмотрению, а ещё преобразовывать типы данных.
Подключение нескольких провайдеров к кластеру
Я уже говорил о том, теперь что jwt: [...]
в файле конфигурации — это массив. Поэтому мы можем определить больше чем один аутентификатор. Это можно использовать:
для миграций;
для аутсорсинга работы;
при приобретении или слиянии с другими компаниями;
для использования одного провайдера для разных проверок.
Миграция. Вы разрабатывали свой код на GitLab и использовали его в качестве провайдера. Но ваша компания выросла, поэтому вы хотите перейти на self-hosted-инсталляцию Keycloak. Теперь во время миграции вы можете использовать обоих провайдеров для доступа к Kubernetes-кластеру.
Аутсорсинг. Вы хотите передать часть работы другой компании. У вас есть Keycloak, и у них есть Keycloak. Вы можете подключить оба провайдера к своему кластеру, но не забудьте защитить их правилами UserInfoValidatonRules
.
Покупка компании. Вы работаете в большой корпорации, которая использует Keycloak. И ваша компания решает приобрести небольшой стартап, где в качестве провайдера аутентификации используется GitLab. В таком случае вы можете подключить к кластеру и Keycloak, и GitHub.
Один провайдер для разных проверок. У вас есть Keycloak, и вы хотите подключить к кластеру два разных приложения с разными политиками аутентификации или разными проверками утверждений. Так тоже можно, кто-то из пользователей просил о такой возможности.
Политика аутентификации пользователей
Новые возможности Structured Authentication Config из этого примера будут наиболее полезны провайдерам managed-услуг.
Представим, что у нас уже есть атрибуты пользователей и нужно проверить, что они не содержат системный префикс system
, потому что при его использовании можно случайно дать пользователю лишние права:
UserInfoValidationRules:
- expression: !username.startsWith("system:")
message: '"system:" prefix in command'
- expression: groups.all(g, !g.startsWith("system:"))
message: '"system:" prefix in command'
При такой проверке пользователь со следующими атрибутами в кластер не попадёт, потому что его группы содержат system:masters
:
{
"name": "jane.doe",
"groups": ["system:masters". "users"]
}
Эти нововведения позволяют защитить кластер от утечек.
Резюме
Structured Authentication Config — крупнейшее обновление аутентификации за долгое время. Оно разработано с учётом потребностей инженерного сообщества.
Common Expression Language — быстрый, надёжный и гибкий движок для аутентификации. Он позволяет делать много замечательных вещей.
Structured Authentication Config может быть расширен в будущем. Возможно, мы добавим в эту конфигурацию настройки других аутентификаторов, не только OpenID Connect.
Вы уже можете попробовать Structured Authentication Config. Он вошёл в бета-фичи Kubernetes 1.30 и, без сомнения, будет глобально доступен (GA) в ближайшем будущем. А попробовать создавать свои CEL-политики можно в CEL Playground.
P. S.
Читайте также в нашем блоге:
D1abloRUS
Ура, я смогу выкинуть свой супер патч, который три года отлично выполнял свою работу, пока технологии смогли достичь нужного уровня
nabokihms Автор
Давайте вместе этому порадуемся)