Keycloak. Standalone-HA в k8s и закрытие админки на ingress-e с переводом на localhost

от автора

Привет, Хаброжители! Продолжаем делиться с вами экспертизой отдела Security services infrastructure (департамент Security Services компании «Лаборатории Касперского»).
Предыдущую статью нашей команды вы можете прочесть вот здесь: Keycloak. Админский фактор и запрет аутентификации

В этой части продолжим настраивать IAM с упором на отказоустойчивость и безопасность. Статья рассчитана на людей, которые ранее были знакомы с IAM и, в частности, с keycloak-ом. Поэтому в этой части не будет «базы» по SAML2, OAuth2/OIDC и в целом по IAM (на Хабре есть хорошие статьи на эту тему). Также для понимания данной статьи необходимы знания базовых абстракций kubernetes и умение читать его манифесты.

Рассмотрим два кейса:

  1. Как в свежей версии keycloak (v.22.0.3) настроить отказоустойчивость при развертывании в k8s в режиме standalone-ha.
  2. Как закрыть ненужные векторы атаки, ограничив пользователям доступ только до нужных путей, но оставив возможность админам заходить на консоль админки keycloak.

Первый кейс. Keycloak standalone-HA в k8s (v.22.0.3)

На данную тему уже были хорошие статьи на Хабре, вот примеры:

  1. Запускаем Keycloak в HA-режиме на Kubernetes (2020 год, Southbridge).
  2. Настраиваем отказоустойчивый Keycloak с Infinispan в Kubernetes (2021 год, Флант).

Плагиатить данные статьи не имею желания и не вижу смысла. Теорию по HA для keycloak вы можете взять из них.

В своей статье постараюсь описать, что же изменилось в настройке на период Q3–Q4 2023 года и как сейчас можно без «головной боли» настроить standalone-ha в k8s.

Изменения:

  1. Keycloak перешел с выделенного сервера приложений WildFly на Quarkus (на момент написания статьи версия 3.2.5.Final).
  2. Cменилось registry c jboss/keycloak на quay.io/keycloak/keycloak.
  3. Старые версии образов используют устаревшие переменные среды, которые в современных версиях не поддерживаются (работу с тегом -legacy не проверял).
  4. И самое главное: в статьях прошлых лет нет манифестов для развертывания keycloak в режиме standalone-ha в k8s, хотя многим компаниям/проектам этого режима достаточно для базовой отказоустойчивости инструмента аутентификации/авторизации (а при необходимости и идентификации) для производственной среды.

Напомню, что keycloak может быть развернут в следующих режимах: standalone, standalone-ha, domain cluster, DC replication.

Режим standalone-ha, у keycloak развернутого в k8s, включает в себя следующие элементы или наборы подов в нашем случае:

  1. Поды с выделенным сервером приложений Quarkus и встроенным модулем Infinispam, собранным в кластер (Key-value database, используется для хранения кэша, аналог redis-a).
  2. Поды распределенной СУБД, собранной в кластер, либо отдельно стоящий кластер СУБД.
  3. Reverse-proxy (в нашем случае ingress-controller), чтобы балансировать нагрузку.

Как собрать кластер СУБД внутри k8s, в данной статье описывать не будем, для базового примера с Postgresql можете развернуть Хельм-чарт от Bitnami: PostgreSQL или PostgreSQL-ha. Либо посмотреть в сторону k8s-операторов СУБД (у Фланта есть хорошие статьи на эту тему на Хабре). Как развертывать ingress-controller в k8s, в данной статье опустим (это популярный кейс и легко гуглится).

Берем за исходные данные то, что у нас перед развертыванием keycloak задеплоены PosgreSQL и ingress-controller (Nginx).

Что касаемо самого keycloak, для удобства развертывания вы можете использовать чарт от Bitnami: keycloak, но для понимания мы развернем standalone-ha keycloak, используя манифесты куба, + Bitnami любят менять название переменных в своих образах, которые отличаются от официальной документации keycloak-a, а это может внести путаницу.

Необходимые нам манифесты:

1) Service. Нужен для балансировки внешних запросов (запросов аутентификации) от пользователей. То есть чтобы пользователя забрасывало на разные вебки keycloak при аутентификации.

--- apiVersion: v1 kind: Service metadata:   name: keycloak-http spec:   type: ClusterIP   ports:     - name: http       port: 8080       protocol: TCP   selector:     app: keycloak-ha 

2) Headless Service. Нужен для определения количества подов кластера Infinispan. Так как headless-service не имеет собственного ip-адреса, то при использовании протокола обнаружения узлов кластера JGroups, такого как DNS_PING, он в ответе получит ip-адреса всех эндпоинтов keycloak+infinispan. Протоколы обнаружения JDBC_PING и KUBE_PING в режиме standalone-ha не используются.

--- apiVersion: v1 kind: Service metadata:   name: keycloak-headless spec:   type: ClusterIP   clusterIP: None   selector:     app: keycloak-ha 

3) StatefulSet. Нужен для развертывания реплик сервера приложений Quarkus со встроенным модулем Infinispam. Используется StatefulSet, а не Deployment, так как StatefulSet может использовать Headless Service для управления доменом своих подов. Поле serviceName в StatefulSet как раз для этого и необходимо.

--- apiVersion: apps/v1 kind: StatefulSet metadata:   name: keycloak   labels:     app: keycloak-ha spec:   selector:     matchLabels:       app: keycloak-ha   replicas: 2   serviceName: keycloak-headless   podManagementPolicy: Parallel   updateStrategy:     type: RollingUpdate   template:     metadata:       labels:         app: keycloak-ha     spec:       restartPolicy: Always       securityContext:         fsGroup: 1000 #      priorityClassName: high       containers:         - name: keycloak           image: quay.io/keycloak/keycloak:22.0.3           imagePullPolicy: Always           resources:             limits:               memory: 1500Mi             requests:               memory: 500Mi               cpu: 100m           securityContext:             runAsNonRoot: true             runAsUser: 1000             capabilities:               drop:                 - ALL                 - CAP_NET_RAW             readOnlyRootFilesystem: false # Quarkus не запускается если данное поле securityContext-a выставить в "true"             allowPrivilegeEscalation: false           args:             - start           env:             - name: KC_METRICS_ENABLED               value: "true"             - name: KC_LOG_LEVEL               value: "info"                       - name: KC_CACHE # тут мы указываем что кеш будем хранить в infinispan               value: "ispn"             - name: KC_CACHE_STACK # тут мы указываем, какую конфигурацию нужно выбрать infinispan-у что б он работал в кубе с протоколом обнаружения DNS_PING               value: "kubernetes"             - name: KC_PROXY               value: "edge"             - name: KEYCLOAK_ADMIN               valueFrom:                 secretKeyRef:                   name: ...                   key: "..."             - name: KEYCLOAK_ADMIN_PASSWORD               valueFrom:                 secretKeyRef:                   name: ...                   key: "..."              - name: KC_DB               value: "postgres"             - name: KC_DB_URL_HOST               value: "postgresql-keycloak"             - name: KC_DB_URL_PORT               value: "5432"             - name: KC_DB_USERNAME               valueFrom:                 secretKeyRef:                   name: ...                   key: "..."              - name: KC_DB_PASSWORD               valueFrom:                 secretKeyRef:                   name: ...                   key: "..."              - name: KC_DB_URL_DATABASE               valueFrom:                 secretKeyRef:                   name: ...                   key: "..."              - name: KC_FEATURES               value: "docker"             - name: KC_HOSTNAME               value: "keycloak.example.ru"             - name: JAVA_OPTS_APPEND # обязательное поле необходимое для работы DNS_PING, указываем наш Headless Service               value: "-Djgroups.dns.query=keycloak-headless.keycloak.svc.cluster.local"           ports:             - name: http               containerPort: 8080               protocol: TCP           livenessProbe:             httpGet:               path: /               port: http             initialDelaySeconds: 120             timeoutSeconds: 5           readinessProbe:             httpGet:               path: /realms/master               port: http             initialDelaySeconds: 60             timeoutSeconds: 1       terminationGracePeriodSeconds: 60 

4) Ingress. Нужен для доступа веба извне, для пользователей, которые будут проходить аутентификацию через keycloak.

Также на нем выставляем привязку сессий (Sticky Sessions), чтобы все запросы пользователя в рамках одной сессии передавались на один под. Иначе мы усложним жизнь infinispan-y, которому придется передавать данные о сессиях пользователей между подами.

--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata:   name: keycloak   annotations:     # следующие 4 строки аннотаций настраивают Sticky Sessions на ingress-e     nginx.ingress.kubernetes.io/affinity: "cookie"     nginx.ingress.kubernetes.io/session-cookie-expires: "86400"     nginx.ingress.kubernetes.io/session-cookie-max-age: "86400"     nginx.ingress.kubernetes.io/session-cookie-name: "keycloak-cookie" spec:   ingressClassName: nginx   tls:     - hosts:       - keycloak.examlple.ru       secretName: web-tls   rules:   - host: keycloak.examlple.ru     http:       paths:       - path: /         pathType: Prefix         backend:           service:             name: keycloak-http             port:               name: http       #путь "/" слишком избыточен для пользователей и создает дополнительные векторы атаки,       #тут он представлен только для примера, во 2-м кейсе этой статьи пофиксим =) 

Деплоим это все в k8s и смотрим, собрался ли наш кластер infinispan:

kubectl apply -f <folder> 

Если в логах контейнеров такого вида строки (отображается два элемента keycloak: keycloak-1-XXXXX, keycloak-0-XXXXX), то значит, кластер infinispam собрался

2023-09-08 13:48:20,514 INFO  [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000078: Starting JGroups channel `ISPN` with stack `kubernetes` 2023-09-08 13:48:20,518 INFO  [org.jgroups.JChannel] (keycloak-cache-init) local_addr: b4d6190d-e9cb-4a3c-8d01-c611129f2a3b, name: keycloak-0-11230 2023-09-08 13:48:20,530 INFO  [org.jgroups.protocols.FD_SOCK2] (keycloak-cache-init) server listening on *.57800 2023-09-08 13:48:20,672 INFO  [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000094: Received new cluster view for channel ISPN: [keycloak-1-40291|15] (2) [keycloak-1-40291, keycloak-0-11230] 2023-09-08 13:48:20,783 INFO  [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000079: Channel `ISPN` local address is `keycloak-0-11230` 2023-09-08 13:48:21,223 INFO  [org.infinispan.LIFECYCLE] (jgroups-6,keycloak-0-11230) [Context=org.infinispan.CONFIG] ISPN100002: Starting rebalance with members [keycloak-1-40291, keycloak-0-11230], phase READ_OLD_WRITE_ALL, topology id 42 2023-09-08 13:48:21,260 INFO  [org.infinispan.LIFECYCLE] (non-blocking-thread--p2-t5) [Context=org.infinispan.CONFIG] ISPN100010: Finished rebalance with members [keycloak-1-40291, keycloak-0-11230], topology id 42 

Первый кейс решен, keycloak развернут в режиме standalone-ha в k8s!

P. S. Если не уверены, потянет ли развернутое вами количество подов keycloak-HA всех ваших пользователей, то можете использовать проект самого keycloak-a под названием keycloak-benchmark для проведения тестов производительности.

Второй кейс. Админка на localhost и закрытие всех излишних путей для пользователей

Если мы развернем ingress keycloak-a, как в первом кейсе, то пользователям будут доступны все пути, в том числе и админка, а этого делать не рекомендуется, так как создаются дополнительные векторы атаки на систему аутентификации. В официальной документации keycloak можно посмотреть, какие пути достаточны для потока аутентификации пользователей.

Обрезать пути будем на reverse-proxy. Для работы стандартного потока аутентификации достаточно пути /realms/, но для примера указаны все пути, которые могут пригодиться и которые рекомендует keycloak. Измененный ingress будет выглядеть так:

--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata:   name: keycloak   annotations:     # следующие 4 строки аннотаций настраивают Sticky Sessions на ingress-e     nginx.ingress.kubernetes.io/affinity: "cookie"     nginx.ingress.kubernetes.io/session-cookie-expires: "86400"     nginx.ingress.kubernetes.io/session-cookie-max-age: "86400"     nginx.ingress.kubernetes.io/session-cookie-name: "keycloak-cookie" spec:   ingressClassName: nginx   tls:     - hosts:       - keycloak.examlple.ru       secretName: web-tls   rules:   - host: keycloak.examlple.ru     http:       paths:             - path: /realms/         pathType: Prefix         backend:           service:             name: keycloak-http             port:               name: http       - path: /resources/         pathType: Prefix         backend:           service:             name: keycloak-http             port:               name: http       - path: /robots.txt         pathType: ImplementationSpecific         backend:           service:             name: keycloak-http             port:               name: http       - path: /js/         pathType: Prefix         backend:           service:             name: keycloak-http             port:               name: http 

После данных изменений доступа извне к админке не будет ни у кого. Но администраторам же надо конфигурировать сам IAM, а постоянно возвращать путь «/» для этих целей в ingress-e не хотелось бы. Можно использовать k8s port-forwarding, но админка все равно будет редиректить на внешний адрес keycloak-a, и в итоге мы получим страницу с «вечной» загрузкой административной консоли. Мы решили данный кейс, добавив в StatefulSet keycloak-a переменную KC_HOSTNAME_ADMIN_URL (поменяв редирект админки на localhost:9999/):

--- apiVersion: apps/v1 kind: StatefulSet metadata:   name: keycloak   labels:     app: keycloak-ha spec:   ...   template:     ...     spec:       ...       containers:           ...           args:             - start           env:                         - name: KC_HOSTNAME_ADMIN_URL               value: "http://localhost:9999/"             ...       ...       

После этого выполним k8s port-forwarding на 9999/tcp:

kubectl port-forward -n keycloak services/keycloak-http 9999:8080 

Далее заходим в браузере на localhost:9999 и с welcome-страницы переходим в админку.

Второй кейс решен, относительно безопасный доступ к админке только для админов открыт!

P.S. Я понимаю, что у читателей могут возникнуть замечания типа: администратор k8s и администратор IAM могут быть разные люди и администратору IAM придется давать права на port-forward. Но на практике во многих компаниях эту роль выполняют одни и те же люди, + настраивайте правильно RBAC в k8s и используйте impersonate для повышения привилегий.

P.P.S. Напоминаю, что у коллег по команде Security Services открыты вакансии пентестера (Penetration Testing Specialist) и аппсекера (Application Security Specialist), и попасть к нам можно всего за одно техническое собеседование!

А вот тут, в нашей игре про умный город, можно проверить свои знания по offensive и defensive.


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


Комментарии

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

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