Привет! Я Дмитрий Трофимов, инженер в команде поддержки продуктов Deckhouse. Как-то мы насобирали запросы от пользователей Deckhouse Kubernetes Platform (DKP), связанные с проблемой динамического развёртывания стендов разработки. Каждый стенд требовал деплоя в кластер отдельного аутентификатора, что приводило к созданию множества объектов в кластере. Например, было более 100 стендов, каждый со своим доменом, соответственно, это создавало сотни объектов аутентификации.
В итоге это вызывало несколько проблем: использование значительных ресурсов (CPU и RAM), усложнение управления настройками безопасности и доступом для нескольких стендов одновременно. Стало понятно, что подход под названием «один домен — один аутентификатор» неудобен. Поэтому мы решили создать многодоменный аутентификатор.

Мы реализовали многодоменный DexAuthenticator, который сократил 100 объектов в пространстве имён до одного и позволил управлять аутентификацией для всех стендов через единый конфиг.
В статье рассмотрим, как DKP обеспечивает безопасный доступ к API кластера, веб-интерфейсам и приложениям в контексте динамически развёртываемых стендов разработки. Вы узнаете, как многодоменный DexAuthenticator упрощает настройку аутентификации, позволяет экономить ресурсы и уменьшает количество объектов в Kubernetes.
Один домен — один аутентификатор
Представим кластер, где каждый стенд разработки развёртывается автоматически, например как часть процесса непрерывной интеграции и доставки (CI/CD), или его запрашивают разработчики и тестировщики. Каждый стенд требует своего аутентификатора для входа в приложение или между его частями. В результате у нас может быть более 100 таких аутентификаторов — по одному на каждый домен, например feature-123.product.com, bugfix-456.product.com.
Такая ситуация приводила к следующим проблемам:
Отсутствие централизованного управления при работе с большим количеством объектов. 100 аутентификаторов в режиме высокой доступности создавали более 300 объектов (Deployment, Service, Ingress). Это усложняло внесение изменений, таких как настройка времени жизни сессии по запросу службы безопасности или добавление IP-адреса нового разработчика. Каждый раз приходилось искать нужный аутентификатор, что было неэффективно. Нам требовалось решение, позволяющее управлять всеми настройками в одном месте для упрощения конфигурации и обслуживания.
Ресурсы впустую. Каждый аутентификатор в среднем использовал 10 мCPU и 10 МБ RAM. В сумме это составляло около 1 ядра CPU и 1 ГБ RAM, которые можно было использовать для более важных задач.
Получается, схема «один домен — один аутентификатор» приводила к избыточност��, увеличивала потребление ресурсов и усложняла управление, так как нужно было следить за всеми аутентификаторами. Не очень эффективно, прямо скажем.
Для наглядности: ресурс DexAuthenticator создаёт deployment <dexauthenticator_name>-dex-authenticator c OAuth2-Proxy под капотом. Показатели по его потреблению (10 мCPU и 10 МБ RAM) также зависят от того, как много новых аутентификаций он обрабатывает, а также от того, включён ли режим высокой доступности (HighAvailability) для модуля user-authn. Кстати, возможность настраивать High Availability для каждого DexAuthenticator появится в ближайшем обновлении DKP (релиз 1.68).
Как работает OAuth2-Proxy
OAuth2-Proxy позволяет добавлять аутентификацию через OAuth2/OIDC (например, Google, GitHub, Keycloak) к приложениям и сервисам, которые изначально не поддерживают встроенную аутентификацию. Он действует как посредник между пользователем и backend-приложением: перехватывает запросы, перенаправляет пользователя на страницу входа выбранного провайдера, проверяет полученные токены доступа и только после успешной аутентификации пропускает трафик к защищаемому ресурсу.
В Deckhouse мы используем Dex как основной OIDC-провайдер. Dex позволяет настраивать подключения к различным внешним провайдерам аутентификации. Связка OAuth2-Proxy + Dex работает следующим образом:
1. Отправляем запрос к защищённому аутентификацией приложению, например https://example.local/. В его Ingress-ресурсе добавлены аннотации, указывающие необходимые для приложения хедеры и редирект на инстанс OAuth2-Proxy.
nginx.ingress.kubernetes.io/auth-response-headers: X-Auth-Request-User,X-Auth-Request-Email nginx.ingress.kubernetes.io/auth-signin: https://$host/oauth2/sign_in nginx.ingress.kubernetes.io/auth-url: https://example-oauth2.example.svc.cluster.local/oauth2/auth
2. Контроллер Ingress проверяет запрос, чтобы определить, требуется ли аутентификация. Он проверяет наличие cookie и заголовков, указанных в аннотации. Если запрос не содержит аутентификационной информации, Ingress определяет, что клиенту необходимо пройти аутентификацию. Он пересылает запрос на OAuth2-Proxy.
3. OAuth2-Proxy запускает процесс аутентификации, используя Dex. Dex пересылает запрос во внешний провайдер аутентификации (например, GitLab, GitHub, Keycloak) для ввода учётных данных.
4. Мы вводим учётные данные, а затем Dex проводит аутентификацию и отправляет ответ с заголовками обратно на OAuth2-Proxy.
5. OAuth2-Proxy сохраняет данные в экземпляре Redis и перенаправляет на Ingress с заголовками ответа.
6. Ingress перенаправляет нас в защищённое приложение с заголовками ответа. Приложение может выполнять дополнительные проверки авторизации, используя информацию из заголовков, и отвечает запрошенным ресурсом.
Это реализует схему OAuth2-Proxy as a standalone reverse-proxy:

Несколько доменов — один аутентификатор
Чтобы упростить настройку аутентификации и повысить эффективность, мы добавили возможность указывать несколько доменов для одного экземпляра DexAuthenticator. Теперь один компонент аутентификации может обслуживать сразу несколько приложений, представленных Ingress-ресурсами.
Решение было на поверхности: использовать один экземпляр DexAuthenticator (а следовательно и один deployment) для нескольких приложений в рамках одного пространства имён. Для этого в параметр --whitelist-domain в OAuth2-Proxy передаются все домены (additionalApplications.domain), которые используются для авторизации.
Обычно при использовании OAuth2-Proxy для каждого приложения нужно создавать и поддерживать целый набор объектов Kubernetes:
Ingress с аннотациями для аутентификации;
Deployment и Service для прокси;
Secret для TLS-сертификатов;
OAuth2Client в Dex для регистрации приложения;
Redis для хранения сессий.
С DexAuthenticator в Deckhouse всё это делает контроллер в паре со встроенным шаблонизатором Helm, который использует values, полученные внутренним дискавери через хуки (подробнее в документации по созданию своего модуля в Deckhouse). Вот что происходит «под капотом», когда создаётся ресурс DexAuthenticator:
1. Создание OAuth2Client в Dex. Контроллер в паре с шаблонизатором генерирует объект OAuth2Client с разрешёнными redirectURIs для всех доменов, указанных в spec.applicationDomain и spec.additionalApplications.
Параметры вроде allowedGroups, allowedEmails или keepUsersLoggedInFor могут обеспечить централизованную настройку безопасности, поскольку они применяются ко всем доменам аутентификатора и конфигурируются в одном deployment DexAuthenticator. Изменения в политиках обновляются атомарно — не нужно править каждый конфиг отдельно:
{{- $context := . }} {{- range $crd := $context.Values.userAuthn.internal.dexAuthenticatorCRDs }} # Каждый модуль (в данном случае userAuthn) содержит свои values, которые хранит в себе Deckhouse-контроллер, тут мы используем цикл для создания рендера шаблонов всех ресурсов по каждому dexAuthenticator. --- apiVersion: dex.coreos.com/v1 kind: OAuth2Client metadata: name: {{ $crd.encodedName }} namespace: d8-{{ $context.Chart.Name }} {{- include "helm_lib_module_labels" (list $context (dict "app" "dex")) | nindent 2 }} id: {{ $crd.name }}-{{ $crd.namespace }}-dex-authenticator name: {{ $crd.name }}-{{ $crd.namespace }}-dex-authenticator secret: {{ $crd.credentials.appDexSecret }} {{- if $crd.spec.allowedEmails }} allowedEmails: {{- range $email := $crd.spec.allowedEmails }} # Подобные конструкции могут быть знакомы по Helm, тут используются те же логика и синтаксис для заполнения шаблона по values. - {{ $email }} {{- end }} {{- end }} {{- if $crd.spec.allowedGroups }} allowedGroups: {{- range $group := $crd.spec.allowedGroups }} - {{ $group }} {{- end }} {{- end }} redirectURIs: {{- range $app := $crd.spec.applications }} # Список приложений, из которого мы достаём доменные имена, преобразован в общий массив с помощью conversion webhook для удобства работы с ним как одним списком. - https://{{ $app.domain }}/dex-authenticator/callback {{- end }} {{- end }}
2. Генерация Ingress-ресурсов. Для каждого домена создаётся два Ingress:
основной (
/dex-authenticator) — для аутентификации;дополнительный (например,
/logout) — для выхода из сессии (он конфигурируется параметромsignOutURL).
Например, добавляется аннотация в зависимости от указанной whitelistSourceRanges в DexAuthenticator, а также некоторые из параметров ingressClass и tls в создаваемом Ingress:
{{- $context := . }} {{- range $crd := $context.Values.userAuthn.internal.dexAuthenticatorCRDs }} {{- range $idx, $app := $crd.spec.applications }} {{- $hashedDomain := sha256sum $app.domain | trunc 8 }} # Поскольку имя у ресурса DexAuthenticator может быть одно на несколько additionalApplications, используется хеш каждого доменного имени для генерации Ingress. {{- $nameSuffix := "" }} {{- if ne $idx 0 }} {{- $nameSuffix = printf "-%s" $hashedDomain }} {{- end }} --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: nginx.ingress.kubernetes.io/backend-protocol: HTTPS {{- if $crd.spec.sendAuthorizationHeader }} nginx.ingress.kubernetes.io/proxy-buffer-size: 32k {{- end }} {{- if $app.whitelistSourceRanges }} nginx.ingress.kubernetes.io/whitelist-source-range: {{ $app.whitelistSourceRanges | join "," }} {{- end }} name: {{ $crd.name }}{{ $nameSuffix }}-dex-authenticator namespace: {{ $crd.namespace }} {{- include "helm_lib_module_labels" (list $context (dict "app" "dex-authenticator")) | nindent 2 }} spec: ingressClassName: {{ $app.ingressClassName }} # В настройках модуля можно переопределить ingressClass, который будет использоваться по умолчанию во всех DexAuthenticator, если не указать его для additionalApplications. rules: - host: {{ $app.domain }} http: paths: - backend: service: name: {{ $crd.name }}-dex-authenticator port: number: 443 path: /dex-authenticator pathType: ImplementationSpecific {{- if (include "helm_lib_module_https_ingress_tls_enabled" $context ) }} {{- if $app.ingressSecretName }} tls: - hosts: - {{ $app.domain }} secretName: {{ $app.ingressSecretName }} {{- end }} {{- end }} {{- if $app.signOutURL }} --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: nginx.ingress.kubernetes.io/backend-protocol: HTTPS nginx.ingress.kubernetes.io/rewrite-target: /dex-authenticator/sign_out name: {{ $crd.name }}{{ $nameSuffix }}-dex-authenticator-sign-out namespace: {{ $crd.namespace }} {{- include "helm_lib_module_labels" (list $context (dict "app" "dex-authenticator")) | nindent 2 }} spec: ingressClassName: {{ $app.ingressClassName }} rules: - host: {{ $app.domain }} http: paths: - backend: service: name: {{ $crd.name }}-dex-authenticator port: number: 443 path: {{ $app.signOutURL }} pathType: ImplementationSpecific {{- if (include "helm_lib_module_https_ingress_tls_enabled" $context ) }} {{- if $app.ingressSecretName }} tls: - hosts: - {{ $app.domain }} secretName: {{ $app.ingressSecretName }} {{- end }} {{- end }} {{- end }} {{- end }} {{- end }} spec: ingressClassName: {{ $app.ingressClassName }} rules: - host: {{ $app.domain }} http: paths: - backend: service: name: {{ $crd.name }}-dex-authenticator port: number: 443 path: {{ $app.signOutURL }} pathType: ImplementationSpecific {{- if (include "helm_lib_module_https_ingress_tls_enabled" $context ) }} {{- if $app.ingressSecretName }} tls: - hosts: - {{ $app.domain }} secretName: {{ $app.ingressSecretName }} {{- end }} {{- end }} {{- end }} {{- end }} {{- end }}
include helm_lib_module_https_ingress_tls_enabled и helm_lib_module_labels — это сниппеты библиотеки helm_lib, которую мы используем во всех модулях Deckhouse для возвращения значений в конечный рендер манифестов на основе заложенной в них логики.
3. Развёртывание Redis для сессий. В под DexAuthenticator автоматически добавляется контейнер Redis. Нет необходимости отдельно выкатывать и настраивать хранилище сессий.
4. Настройка Deployment (а дополняют его VerticalPodAutoscaler и PodDisruptionBudget) и выкат связанного Service происходит по такой же логике, что и Ingress с OAuth2Client:
... containers: - name: dex-authenticator {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 8 }} image: {{ include "helm_lib_module_image" (list $context "dexAuthenticator") }} args: - --provider=oidc - --client-id={{ $crd.name }}-{{ $crd.namespace }}-dex-authenticator # Имя ресурса, свзяанного с этим dex-authenticator ресурса OAuth2Client {{- if ne (include "helm_lib_module_uri_scheme" $context ) "https" }} - --cookie-secure=false {{- end }} - --redirect-url=/dex-authenticator/callback # Ранее тут был полный URI, а не URN, что не подходило для нескольких additionalApplications. - --oidc-issuer-url=https://{{ include "helm_lib_module_public_domain" (list $context "dex") }}/ - --skip-oidc-discovery - --redeem-url=https://dex.d8-user-authn/token - --login-url=https://{{ include "helm_lib_module_public_domain" (list $context "dex") }}/auth - --oidc-jwks-url=https://dex.d8-user-authn/keys {{- if $crd.spec.sendAuthorizationHeader }} - --set-authorization-header=true {{- end }} - --set-xauthrequest - --scope=groups email openid profile offline_access{{- if $crd.allowAccessToKubernetes }} audience:server:client_id:kubernetes{{- end }} - --ssl-insecure-skip-verify=true - --proxy-prefix=/dex-authenticator # Корневой путь для OAuth2-Proxy, в примере авторизации выше (Как работает oauth2-proxy) он был /oauth2. - --email-domain=* {{- range $app := $crd.spec.applications }} - --whitelist-domain={{ $app.domain }} # Пользуемся тем, что в whitelist-domain у OAuth2-Proxy можно указывать несколько URL. {{- end }} - --upstream=file:///dev/null - --https-address=0.0.0.0:8443 - --tls-cert-file=/opt/dex-authenticator/tls/tls.crt - --tls-key-file=/opt/dex-authenticator/tls/tls.key - --skip-provider-button - --silence-ping-logging - --session-store-type=redis - --redis-connection-url=redis://127.0.0.1/ {{- $idTokenTTL := $context.Values.userAuthn.idTokenTTL | default "10m" }} {{- $keepUsersLoggedInFor := $crd.spec.keepUsersLoggedInFor | default "168h" }} # По умолчанию сессия будет храниться 7 дней. {{- $delta := now }} - --cookie-refresh={{ $idTokenTTL }} {{- if gt ($delta | mustDateModify $keepUsersLoggedInFor | unixEpoch) ($delta | mustDateModify $idTokenTTL | unixEpoch) }} - --cookie-expire={{ $keepUsersLoggedInFor }} {{- else }} - --cookie-expire={{ duration (add (sub ($delta | mustDateModify $idTokenTTL | unixEpoch) ($delta | unixEpoch)) 1) }} {{- end }} - --insecure-oidc-allow-unverified-email=true - --approval-prompt=basic - --reverse-proxy ...
Получается, что такой подход лучше, так как он экономит ресурсы, поскольку вместо нескольких Dex deployment'ов создаётся один. А ещё управление аутентификацией для множества приложений в таком случае становится централизованным. При этом количество объектов уменьшается, что облегчает мониторинг и обслуживание кластера.
Конфигурация
Чтобы можно было указывать несколько доменов, в ресурс DexAuthenticator добавили секцию spec.additionalApplications. Она будет содержать список дополнительных приложений, для которых необходима аутентификация.
spec.additionalApplications
Разберём объекты, каждый из которых описывает дополнительное приложение. Они имеют следующие поля:
domain(обязательное) — домен приложения, который будет использоваться в Ingress-ресурсе. Запросы на этот домен будут перенаправлены в Dex для аутентификации. Важно: домен не может содержать HTTP-схему. Должен соответствовать регулярному выражению:^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$;ingressClassName(обязательное) — название Ingress-класса для использования в Ingress-ресурсе. Должно совпадать с названием Ingress-класса для домена приложения. Соответствует регулярному выражению:^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$;ingressSecretName— имя секрета с TLS-сертификатом для домена приложения. Используется в Ingress-ресурсе приложения. Секрет должен находиться в том же пространстве имён, что и DexAuthenticator;signOutURL— URL для завершения сеанса аутентификации. Используется в приложении для направления запросов на «выход». Для этого URL будет создан отдельный Ingress-ресурс, перенаправляющий запросы вdex-authenticator;whitelistSourceRanges— список IP-адресов в формате CIDR, которым разрешено проходить аутентификацию. Если он не указан, аутентификация разрешена без ограничения по IP-адресу. Пример:whitelistSourceRanges: - 192.168.42.0/24. Каждый элемент должен соответствовать регулярному выражению:^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$.
Пример конфигурации
Разберём конфигурацию параметра spec.additionalApplications в ресурсе DexAuthenticator:
apiVersion: deckhouse.io/v1 kind: DexAuthenticator metadata: name: app-name namespace: app-namespace spec: applicationDomain: app-name.kube.my-domain.com sendAuthorizationHeader: false applicationIngressCertificateSecretName: ingress-tls applicationIngressClassName: nginx keepUsersLoggedInFor: 720h allowedGroups: - everyone - admins whitelistSourceRanges: - 1.1.1.1/32 - 192.168.0.0/24 additionalApplications: - domain: additional-app-name.kube.my-domain.com ingressSecretName: ingress-tls ingressClassName: nginx signOutURL: "/logout" whitelistSourceRanges: - 2.2.2.2/32
В этом примере:
основное приложение для аутентификации определяется полем
spec.applicationDomain: app-name.kube.my-domain.com;дополнительное приложение добавляется через секцию
spec.additionalApplications. В данном случае для примера оно одно;domain: additional-app-name.kube.my-domain.comуказывает домен дополнительного приложения. Схема (HTTP/HTTPS) не указывается;ingressSecretName: ingress-tlsзадаёт имя секрета, содержащего TLS-сертификат для Ingress-ресурса дополнительного приложения. Секрет должен находиться в том же пространстве имён, что и DexAuthenticator;ingressClassName: nginxопределяет, какой Ingress-класс использовать для Ingress-ресурса. Он должен соответствовать Ingress-классу, который обслуживает домен приложения;signOutURL: "/logout"задаёт URL, на который приложение будет перенаправлять пользователя для выхода из сессии. Для этого URL будет создан отдельный Ingress, перенаправляющий наdex-authenticator;whitelistSourceRangesограничивает доступ к приложению только с указанных IP-адресов. В примере указан2.2.2.2/32.
При этом остальные параметры — keepUsersLoggedInFor, allowedGroups, sendAuthorizationHeader — будут распространяться на все приложения, указанные в этом DexAuthenticator. А основное приложение app-name.kube.my-domain.com и дополнительное additional-app-name.kube.my-domain.com будут использовать один экземпляр DexAuthenticator для аутентификации.
Вместо заключения
Возможность указывать несколько доменов для DexAuthenticator появилась в Deckhouse Kubernetes Platform версии 1.66. Благодаря этому нововведению можно сократить количество объектов в кластере. Например, вместо тех же ста аутентификаторов теперь в кластере будет только один. А ещё эта возможность позволяет сэкономить ресурсы: ранее каждый DexAuthenticator потреблял около 10 мCPU и 10 МБ RAM, теперь эти ресурсы высвобождены для более полезных задач.
Помимо экономии ресурсов, упростилась конфигурация и централизовалось управление параметрами безопасности. А это значит, что обслуживание и мониторинг кластера стали проще, что, безусловно, является большим плюсом для всех пользователей DKP.
P. S.
Читайте также в нашем блоге:
