Как стать автором
Обновить
Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

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

Уровень сложностиПростой
Время на прочтение13 мин
Количество просмотров489

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

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

Теги:
Хабы:
+22
Комментарии0

Публикации

Информация

Сайт
flant.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Александр Лукьянов