Keycloak in k8s
https://habr.com/ru/companies/kaspersky/articles/763790/
Привет, Хаброжители! Продолжаем делиться с вами экспертизой отдела Security services infrastructure (департамент Security Services компании «Лаборатории Касперского»).
Предыдущую статью нашей команды вы можете прочесть вот здесь: 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)
На данную тему уже были хорошие статьи на Хабре, вот примеры:
Плагиатить данные статьи не имею желания и не вижу смысла. Теорию по 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, а это может внести путаницу.
Необходимые нам манифесты:
Service. Нужен для балансировки внешних запросов (запросов аутентификации) от пользователей. То есть чтобы пользователя забрасывало на разные вебки keycloak при аутентификации.
---
apiVersion: v1
kind: Service
metadata:
name: keycloak-http
spec:
type: ClusterIP
ports:
- name: http
port: 8080
protocol: TCP
selector:
app: keycloak-ha
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
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
Ingress. Нужен для доступа веба извне, для пользователей, которые будут проходить аутентификацию через keycloak.
Также на нем выставляем привязку сессий (README/README.md)), чтобы все запросы пользователя в рамках одной сессии передавались на один под. Иначе мы усложним жизнь 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 для повышения привилегий.
Last updated
Was this helpful?