Привет, Хаброжители! Продолжаем делиться с вами экспертизой отдела 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.
Комментарии (6)
panablack Автор
28.09.2023 04:18+1Да, это сработает, если по пути нет nat-ов) Идея интересная и применима не только в данном кейсе, спасибо)
toxella
Можно для админки на ingress сделать nginx.ingress.kubernetes.io/whitelist-source-range, чтобы ограничить доступ только с IP-адресов подсети админов, если сеть достаточно сегментирована