Привет! Я Дмитрий Трофимов, инженер в команде поддержки продуктов 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.
Читайте также в нашем блоге: