Предыдущую статью нашей команды вы можете прочесть вот здесь: 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.
ссылка на оригинал статьи https://habr.com/ru/articles/763790/
Добавить комментарий