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

Рассмотрим два кейса:
На данную тему уже были хорошие статьи на Хабре, вот примеры:
Плагиатить данные статьи не имею желания и не вижу смысла. Теорию по HA для keycloak вы можете взять из них.
В своей статье постараюсь описать, что же изменилось в настройке на период Q3–Q4 2023 года и как сейчас можно без «головной боли» настроить standalone-ha в k8s.
Изменения:
Напомню, что keycloak может быть развернут в следующих режимах: standalone, standalone-ha, domain cluster, DC replication.
Режим standalone-ha, у keycloak развернутого в k8s, включает в себя следующие элементы или наборы подов в нашем случае:
Как собрать кластер СУБД внутри 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 при аутентификации.
2) Headless Service. Нужен для определения количества подов кластера Infinispan. Так как headless-service не имеет собственного ip-адреса, то при использовании протокола обнаружения узлов кластера JGroups, такого как DNS_PING, он в ответе получит ip-адреса всех эндпоинтов keycloak+infinispan. Протоколы обнаружения JDBC_PING и KUBE_PING в режиме standalone-ha не используются.
3) StatefulSet. Нужен для развертывания реплик сервера приложений Quarkus со встроенным модулем Infinispam. Используется StatefulSet, а не Deployment, так как StatefulSet может использовать Headless Service для управления доменом своих подов. Поле
4) Ingress. Нужен для доступа веба извне, для пользователей, которые будут проходить аутентификацию через keycloak.
Также на нем выставляем привязку сессий (Sticky Sessions), чтобы все запросы пользователя в рамках одной сессии передавались на один под. Иначе мы усложним жизнь infinispan-y, которому придется передавать данные о сессиях пользователей между подами.
Деплоим это все в k8s и смотрим, собрался ли наш кластер infinispan:
Если в логах контейнеров такого вида строки (отображается два элемента keycloak: keycloak-1-XXXXX, keycloak-0-XXXXX), то значит, кластер infinispam собрался
Первый кейс решен, keycloak развернут в режиме standalone-ha в k8s!
P. S. Если не уверены, потянет ли развернутое вами количество подов keycloak-HA всех ваших пользователей, то можете использовать проект самого keycloak-a под названием keycloak-benchmark для проведения тестов производительности.
Если мы развернем ingress keycloak-a, как в первом кейсе, то пользователям будут доступны все пути, в том числе и админка, а этого делать не рекомендуется, так как создаются дополнительные векторы атаки на систему аутентификации. В официальной документации keycloak можно посмотреть, какие пути достаточны для потока аутентификации пользователей.
Обрезать пути будем на reverse-proxy. Для работы стандартного потока аутентификации достаточно пути /realms/, но для примера указаны все пути, которые могут пригодиться и которые рекомендует keycloak. Измененный ingress будет выглядеть так:
После данных изменений доступа извне к админке не будет ни у кого. Но администраторам же надо конфигурировать сам IAM, а постоянно возвращать путь "/" для этих целей в ingress-e не хотелось бы. Можно использовать k8s port-forwarding, но админка все равно будет редиректить на внешний адрес keycloak-a, и в итоге мы получим страницу с «вечной» загрузкой административной консоли. Мы решили данный кейс, добавив в StatefulSet keycloak-a переменную KC_HOSTNAME_ADMIN_URL (поменяв редирект админки на localhost:9999/):
После этого выполним k8s port-forwarding на 9999/tcp:
Далее заходим в браузере на 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.
Предыдущую статью нашей команды вы можете прочесть вот здесь: Keycloak. Админский фактор и запрет аутентификации
В этой части продолжим настраивать IAM с упором на отказоустойчивость и безопасность. Статья рассчитана на людей, которые ранее были знакомы с IAM и, в частности, с keycloak-ом. Поэтому в этой части не будет «базы» по SAML2, OAuth2/OIDC и в целом по IAM (на Хабре есть хорошие статьи на эту тему). Также для понимания данной статьи необходимы знания базовых абстракций kubernetes и умение читать его манифесты.

Рассмотрим два кейса:
- Как в свежей версии keycloak (v.22.0.3) настроить отказоустойчивость при развертывании в k8s в режиме standalone-ha.
- Как закрыть ненужные векторы атаки, ограничив пользователям доступ только до нужных путей, но оставив возможность админам заходить на консоль админки keycloak.
Первый кейс. Keycloak standalone-HA в k8s (v.22.0.3)
На данную тему уже были хорошие статьи на Хабре, вот примеры:
- Запускаем Keycloak в HA-режиме на Kubernetes (2020 год, Southbridge).
- Настраиваем отказоустойчивый Keycloak с Infinispan в Kubernetes (2021 год, Флант).
Плагиатить данные статьи не имею желания и не вижу смысла. Теорию по HA для keycloak вы можете взять из них.
В своей статье постараюсь описать, что же изменилось в настройке на период Q3–Q4 2023 года и как сейчас можно без «головной боли» настроить standalone-ha в k8s.
Изменения:
- Keycloak перешел с выделенного сервера приложений WildFly на Quarkus (на момент написания статьи версия 3.2.5.Final).
- Cменилось registry c jboss/keycloak на quay.io/keycloak/keycloak.
- Старые версии образов используют устаревшие переменные среды, которые в современных версиях не поддерживаются (работу с тегом -legacy не проверял).
- И самое главное: в статьях прошлых лет нет манифестов для развертывания keycloak в режиме standalone-ha в k8s, хотя многим компаниям/проектам этого режима достаточно для базовой отказоустойчивости инструмента аутентификации/авторизации (а при необходимости и идентификации) для производственной среды.
Напомню, что keycloak может быть развернут в следующих режимах: standalone, standalone-ha, domain cluster, DC replication.
Режим standalone-ha, у keycloak развернутого в k8s, включает в себя следующие элементы или наборы подов в нашем случае:
- Поды с выделенным сервером приложений Quarkus и встроенным модулем Infinispam, собранным в кластер (Key-value database, используется для хранения кэша, аналог redis-a).
- Поды распределенной СУБД, собранной в кластер, либо отдельно стоящий кластер СУБД.
- 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.
