Как легко настроить аутентификацию для нескольких доменов в Kubernetes: Deckhouse Kubernetes Platform

от автора

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

Читайте также в нашем блоге:


ссылка на оригинал статьи https://habr.com/ru/articles/891846/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *