# Keycloak in k8s

<https://habr.com/ru/companies/kaspersky/articles/763790/>

Привет, Хаброжители! Продолжаем делиться с вами экспертизой отдела Security services infrastructure ([департамент Security Services компании «Лаборатории Касперского»](https://kas.pr/habr-sandzhiev-keycloak2-sec-serv)).

Предыдущую статью нашей команды вы можете прочесть вот здесь: [Keycloak. Админский фактор и запрет аутентификации](https://habr.com/ru/companies/kaspersky/articles/756812/)

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

![](https://gitlab.com/johnmkane/tech-recipe-book/-/blob/main/Book/Architect/Kubernetes/Auth/Keycloak%20in%20k8s/Untitled)

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

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

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

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

Плагиатить данные статьи не имею желания и не вижу смысла. Теорию по 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 [не используются](https://www.keycloak.org/server/caching).

```
---
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.

Также на нем выставляем привязку сессий (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](https://github.com/keycloak/keycloak-benchmark) для проведения тестов производительности.

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

Если мы развернем ingress keycloak-a, как в первом кейсе, то пользователям будут доступны все пути, в том числе и админка, а этого делать не рекомендуется, так как создаются дополнительные векторы атаки на систему аутентификации. [В официальной документации](https://www.keycloak.org/server/reverseproxy) 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](http://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](http://localhost/):9999 и с welcome-страницы переходим в админку.

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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://book.konstantinsecurity.com/readme/architect/kubernetes/auth/keycloak-in-k8s.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
