Продолжаем с делиться экспертизой отдела Security services infrastructure (департамент Security Services компании «Лаборатории Касперского»). В данном посте мы разберем, как легко настроить mTLS, обращаясь к ресурсам в k8s через ingress-контроллер, и подсоединить это все к keycloak. Пост будет полезен тем, кто в своей инфраструктуре использует PKI и, в частности, клиентские сертификаты.



Ни для кого не секрет, что для улучшения защиты доступа к веб-ресурсам многие компании используют или начинают использовать mTLS — когда помимо проверки серверного сертификата проверяется сертификат пользователя. В данной статье мы расскажем:
  1. Как настроить проверку клиентских сертификатов в k8s на ingress-контроллере.
  2. Как передать клиентский сертификат с ingress-контроллера в keycloak с мапингом сертификата к учетной записи Keycloak-a.
  3. Как и зачем настраивать перепроверку клиентского сертификата в keycloak.
  4. Как проверить отозванные клиентские сертификаты с помощью keycloak и CRL/OCSP.

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

В ресерче материалов для данного поста и реализации данной технологии на проде принимали участие еще несколько человек. Указать их соавторами на Хабре нет возможности, поэтому озвучу их тут: Ян Краснов, Иван Николаев, Максим Сушков, Иван Кодянов.


Настройка mTLS на ingress-контроллере


Mutual TLS (mTLS), он же — взаимный TLS, усиливает защиту входа на защищаемые сервисы посредством проверки клиентского сертификата помимо серверного. Данное расширение протокола TLS применимо в инфраструктурах с заранее известным количеством и составом пользователей, т. е. для внутреннего сегмента компании.

Большинство сервисов и веб-серверов, имеющих функционал терминации TLS-соединения из коробки, могут «базово» проверять клиентские сертификаты. Пример: nginx, traefik, caddy, HAproxy, envoy и т. п.

Мы терминируем TLS-соединения на ingress-контроллере кластера kubernetes. В качестве ingress-контроллера используем классический nginx.

Для дальнейших действий нам будет нужен сертификат CA, Intermediate-сертификат и клиентский сертификат, подписанный вышеуказанным Intermediate-сертификатом. С этой целью у нас развернут HashiCorp Vault, и мы используем его функционал PKI secrets engine. Для тестов читателям данной статьи необязательно разворачивать HC Vault, достаточно вручную создать пару ключей для CA, Intermediate и клиентский сертификат, используя openSSL. Приведем пример.

Выписка пользовательских сертификатов
При выписке пользовательского сертификата необходимо указывать поле email! Именно по этому полю будет происходить проверка валидности пользователя, если настраивать аутентификацию по данной инструкции.
RootCA
openssl req -x509 -sha256 -days 3650 -newkey rsa:2048 -keyout rootCA.key -out rootCA.crt

Host certificate
openssl req -new -newkey rsa:4096 -keyout server.key -out server.csr -nodes

Sign host csr with rootCA (see below for file localhost.ext):
openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in server.csr -out server.crt -days 365 -CAcreateserial -extfile server.ext

Client (user) certificate
openssl req -new -newkey rsa:2048 -nodes -keyout user1.key -out user1.csr

Sign client csr with rootCA:
openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in user1.csr -out user1.crt -days 365 -CAcreateserial

Import client key and crt in keystore to create the «certificate» to be used in the browser:
openssl pkcs12 -export -out user1.p12 -name "user1" -inkey user1.key -in user1.crt

.ext file
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
subjectAltName = @alt_names
[alt_names]
DNS.1 = server

Далее необходимо добавить .p12-сертификат на компьютер пользователя.


Для того чтобы проверить валидность сертификата, предоставляемого пользователем, необходимо куда-нибудь поместить CA, которым был подписан промежуточный (клиентский) сертификат. В нашем случае мы поместим его в Secret k8s. Имя файла сертификата в секрете обязательно должно быть ca.crt:
apiVersion: v1
kind: Secret
metadata:
  name: <ИМЯ>
  namespace: <ИМЯ НЕЙМСПЕЙСА>
type: Opaque
data:
  ca.crt: <PEM СЕРТИФИКАТ В BASE64>

Проверка сертификата осуществляется на каждом хосте отдельно. Поэтому для включения проверки сертификата на нужном сервисе необходимо приписать аннотации в ingress-манифест:
annotations:
  nginx.ingress.kubernetes.io/auth-tls-secret: default/ca-secret
  nginx.ingress.kubernetes.io/auth-tls-verify-client: 'on'

Полный манифест будет выглядеть примерно так:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/auth-tls-secret: default/ca-secret
    nginx.ingress.kubernetes.io/auth-tls-verify-client: 'on'
 
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - test-ingress.local
      secretName: web-tls
  rules:
    - host: test-ingress.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: test-ingress
                port:
                  number: 80

После добавления данных аннотаций ingress-контроллер не будет пропускать вас через себя пока вы не предъявите личный сертификат. Выглядеть это будет примерно так:

При предъявлении нужного сертификата и его успешной проверке вы перейдете на нужный вам сайт, а при неудачной попытке ответ будет такой:


Настройка mTLS на keycloak (мапинг данных сертификата и учетных записей Keycloak)


Если есть желание или необходимость настроить аутентификацию в keycloak по клиентским сертификатам, то требуется следующее.
1. Перейти в keycloak в настройке аутентификации (пункт меню Authentication), найти browser flow и нажать Duplicate:

2. Выбрать новое имя, например x509-auth, и удалить ненужные шаги аутентификации, оставив Cookies и, например, User-Pass-OTP, как альтернативный вариант аутентификации:

3. Нажать на Add Step и выбрать x509/Validate Username Form:

4. Передвинуть данный шаг повыше, сразу после Cookies, поставить его как Alternative и нажать на шестеренку для настройки. Здесь придумать Alias, указать User Identity Source (Subject's email) и User mapping method (Username or Email).

!!! В данном случае проводится проверка по пользовательскому email-адресу. Соответственно, у пользователя обязательно должен быть проставлен email. Сохраняем.

5. Теперь данную конфигурацию необходимо забиндить на browser flow. В настройках шагов аутентификации в правом верхнем углу нажмем Action — Bind flow. Выберем Browser flow:

6. В deployment/statefullset keycloak нужно прописать некоторые переменные, необходимые для считывания клиентского сертификата через ingress-контроллер:
env:
  - name: KC_SPI_X509CERT_LOOKUP_PROVIDER # какой reverse-proxy server (ingress-контроллер) используется
    value: nginx
  - name: KC_SPI_X509CERT_LOOKUP_NGINX_SSL_CLIENT_CERT # имя заголовка, содержащего клиентский сертификат
    value: ssl-client-cert
  - name: KEYCLOAK_X509CERT_LOOKUP_NGINX_SSL_CERT_CHAIN_PREFIX # префикс заголовков, содержащий дополнительные сертификаты в цепочке и используемый для извлечения отдельных сертификатов в соответствии с длиной цепочки
    value: USELESS
  - name: KEYCLOAK_X509CERT_LOOKUP_NGINX_SSL_CERTIFICATE_CHAIN_LENGTH # максимальная длина цепочки сертификатов
    value: '2'

7. В манифесте Ingress-а keycloak добавляем следующие аннотации для проверки клиентского сертификата и передачи его на сервис keycloak:
annotations:
  nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: 'true' # Указывает, следует ли передавать полученные сертификаты вышестоящему серверу в заголовке ssl-client-cert. Возможные значения: «true» или «false»
  nginx.ingress.kubernetes.io/auth-tls-secret: default/ca-secret # Путь к секрету в виде namespace/secret. Сертификат в секрете обязательно должен лежать в файле с именем ca.crt
  nginx.ingress.kubernetes.io/auth-tls-verify-client: on # Включает проверку пользовательского сертификата. Возможные опции: on - всегда включена. off - всегда выключена. optional - проверка происходит, но необязательна. optional_no_ca - проверка происходит, но не выводит ошибку, если сертификат не подписан указанным CA.
  nginx.ingress.kubernetes.io/auth-tls-verify-depth: '2' # Глубина проверки между предоставленным сертификатом клиента и цепочкой центра сертификации.
  nginx.ingress.kubernetes.io/proxy-ssl-secret: default/ca-secret # Используется для проверки сертификата проксируемого HTTPS-сервера.
  nginx.ingress.kubernetes.io/proxy-ssl-verify: 'off' # Включает или отключает проверку сертификата проксируемого HTTPS-сервера
  nginx.ingress.kubernetes.io/proxy-ssl-verify-depth: '2' # Глубина проверки между предоставленным сертификатом проксируемого HTTPS-сервера и цепочкой центра сертификации.

После выполнения данных действий при переходе на сервис c настроенной аутентификацией, используя keycloak, мы увидим при входе следующую картину:

Часть информации скрыта, но можно понять, что keycloak увидел клиентский сертификат, сопоставил email и понял, что они совпадают с пользователем sandzhiev, и предлагает выполнить вход под этой учетной записью.

Настройка перепроверки клиентского сертификата в keycloak


Если мы прочтем заметку ребят на GitHub
mTLS: When certificate authentication is done wrong (о ней я узнал из канала k8security) или посмотрим их выступление на blackhat USA 2023, то поймем, что надо:
  • обновить keycloak как минимум до 21.1.2 (так как только с этой версии он проверяет всю цепочку сертификатов);
  • настраивать проверку клиентских сертов не только на reverse-proxy, но и в самом iam (не доверять проверку клиентских сертификатов только ingress-контроллеру);
  • дополнительно перепроверять «сетевку» внутри куба, чтобы в keycloak не могли направить заголовок в обход reverse-proxy (ingress-контроллер).

Вот как это настроить.
1. Необходимо внутрь keycloak (хранилище доверенных сертификатов (truststore)) положить CA со всей цепочкой сертификатов. Для этого нужно необходимые нам сертификаты (CA и intermediate) преобразовать в .jks (Java KeyStore, актуально до версии keycloak 23.х, в версиях 24+ можно закидывать даже pem), положить его в secret k8s и примаунтить этот secret к deployment/statefullset keycloak — как показано ниже.

Создаем jks и кладем его в секрет k8s:
keytool -import -alias moi-ca -keystore truststore.jks -file ca.pem
keytool -list -v -keystore truststore.jks
kubectl create secret generic dss-truststore --from-file=./truststore.jks -n keycloak

Примаунтим secret к deployment/statefullset keycloak, а также зададим переменные, для того чтобы keycloak подцепил .jks:
apiVersion: apps/v1
kind: StatefulSet
spec:
...
  template:
    spec: 
      containers:
        - name: keycloak
...
          env:
...
            - name: KC_SPI_TRUSTSTORE_FILE_HOSTNAME_VERIFICATION_POLICY # depricated c версии 24.x (Политика проверки имени хоста TLS для исходящих запросов HTTPS и SMTP)
              value: "WILDCARD"
            - name: KC_SPI_TRUSTSTORE_FILE_FILE # depricated c версии 24.x (путь до хранилища доверенных сертификатов)
              value: "/opt/keycloak/spi-certs/truststore.jks"
            - name: KC_SPI_TRUSTSTORE_FILE_TYPE # depricated c версии 24.x (тип хранилища доверенных сертификатов, например jks, pkcs12 или bcfks)
              value: "jks"
            - name: KC_SPI_TRUSTSTORE_FILE_PASSWORD # depricated c версии 24.x
              valueFrom:
                secretKeyRef:
                  name: keycloak-spi-passwords
                  key: "spi-truststore-password"
            #- name: KC_TLS_HOSTNAME_VERIFIER # новый формат c версии 24.x (замена KC_SPI_TRUSTSTORE_FILE_HOSTNAME_VERIFICATION_POLICY)
            #  value: "DEFAULT"
            #- name: KC_TRUSTSTORE_PATHS # новый формат c версии 24.x (замена KC_SPI_TRUSTSTORE_FILE_FILE, теперь принимает pkcs12 (расширения файлов p12 или pfx), файлы PEM или каталоги, содержащие эти файлы)
            #  value: "/opt/keycloak/spi-certs/..."
            - name: KC_SPI_X509CERT_LOOKUP_PROVIDER
              value: nginx
            - name: KC_SPI_X509CERT_LOOKUP_NGINX_SSL_CLIENT_CERT
              value: ssl-client-cert
            - name: KEYCLOAK_X509CERT_LOOKUP_NGINX_SSL_CERT_CHAIN_PREFIX
              value: USELESS
            - name: KEYCLOAK_X509CERT_LOOKUP_NGINX_SSL_CERTIFICATE_CHAIN_LENGTH
              value: '2'
...
          volumeMounts:
            - name: spi-certificates
              mountPath: /opt/keycloak/spi-certs
              readOnly: true
      volumes:
        - name: spi-certificates
          secret:
            secretName: dss-truststore
            defaultMode: 420
...

2. Переходим в «админку» keycloak, в настройки x509/Validate Username Form созданного нами ранее Browser flow (Configure → Authentication → <имя нашего Browser flow> → x509/Validate Username Form → Setting)
и меняем значение следующих параметров на True:
  • Check certificate validity
  • Revalidate Client Certificate (Certificate Policy Validation Mode=All)


После вышеописанных действий в keycloak начинает работать следующий workflow.
  • Клиент отправляет запрос аутентификации по каналу SSL/TLS.
  • Во время установления связи SSL/TLS-сервер и клиент обмениваются сертификатами x.509/v3.
  • Контейнер (WildFly) проверяет путь PKIX сертификата и дату истечения срока действия сертификата.
  • Аутентификатор клиентского сертификата x.509 проверяет клиентский сертификат, используя следующие методы:
    • проверяет, соответствует ли ключ в сертификате ожидаемому ключу;
    • проверяет, соответствует ли расширенный ключ в сертификате ожидаемому расширенному ключу.
  • Если какая-нибудь из этих проверок не пройдена, аутентификация x.509 не пройдена. В противном случае аутентификатор извлекает идентификатор сертификата и сопоставляет его с существующим пользователем.


Проверка отозванных клиентские сертификатов средствами keycloak


В настроенном нами workflow не хватает проверки отозванных клиентских сертификатов. Keycloak умеет проверять отозванные сертификаты с помощью моделей online certificate status protocol (OCSP) и certificate revocation lists (CRLs). В данной статье не будет описано, как заполнять списки CRL, создавать CRL Distribution Points и как настраивать сервер OSCP, по этим вопросам достаточно информации в Интернете.
1. Настройка CRLs.
Переходим в админку keycloak, в настройки x509/Validate Username Form созданного нами ранее Browser flow (Configure → Authentication → <имя нашего Browser flow> → x509/Validate Username Form → Setting) и находим строки, связанные с CRLs:
  • CRL Checking Enabled — включить/выключить проверку по CRL;
  • Enable CRL Distribution Point to check certificate revocation status — включить/выключить поиск CRL-списков из точек распространения. Можно использовать, только если у вас в сертификате есть поле cRLDistributionPoints с URL до списков;
  • CRL Path — путь к файлу со списками CRL (может быть локальным либо можно указать URL).


2. Настройка OCSP.
Переходим в админку keycloak, в настройки x509/Validate Username Form созданного нами ранее Browser flow (Configure → Authentication → <имя нашего Browser flow> → x509/Validate Username Form → Setting) и находим строки, связанные с OCSP:
  • OCSP Checking Enabled — включить/выключить проверку OCSP;
  • OCSP Fail-Open Behavior — разрешить/запретить аутентификацию для клиентских сертификатов, у которых отсутствуют/недействительны/неопределены конечные точки OCSP. По умолчанию требуется успешный ответ OCSP;
  • OCSP Responder Uri — Uri ответчика OCSP для проверки статуса отзыва сертификата;
  • OCSP Responder Certificate — необязательный сертификат, используемый ответчиком для подписи ответов. Сертификат должен быть в формате PEM без тегов BEGIN и END. Он используется только в том случае, если установлен URI ответчика OCSP. По умолчанию сертификат ответчика OCSP — это сертификат эмитента проверяемого сертификата или сертификат с расширением OCSPSigning, выданный тем же центром сертификации. Этот параметр определяет сертификат ответчика OCSP, если значения по умолчанию не применяются.


После данных настроек keycloak добавит в свой workflow следующие пункты.
  • Проверка статуса отзыва сертификата с помощью CRL или точек распространения CRL.
  • Проверка статуса отзыва сертификата с помощью OCSP (Online Certificate Status Protocol).

При успешной проверке CRL и (или) OCSP вы зайдете на необходимый ресурс. При неправильной настройке или при попытке зайти с отозванным сертификатом будет получен примерно такой ответ:


Прошлые публикации


Предыдущие посты нашей команды вы можете прочесть по следующим ссылкам:
  1. Keycloak. Админский фактор и запрет аутентификации
  2. Keycloak. Standalone-HA в k8s и закрытие админки на ingress-e с переводом на localhost
  3. Что и зачем почитать DevSecOps-у: личный опыт

Комментарии (6)


  1. quarachelia
    09.10.2024 14:42

    Как раз вовремя, спасибо за статью!


  1. Caraul
    09.10.2024 14:42

    А вместо "открыть и нажать" есть ли автоматизированные возможности настройки Keycloak? Статья отличная, но количество ручной работы наводит грусть.


    1. panablack Автор
      09.10.2024 14:42

      Конечно есть. В нашем случае: terraform-provider + несколько наших самописных микросеовисов. Не стал их указывать в статье, т.к. это бы усложнило статью. Пытался простым языком донести без подробностей настроки pki в волте, автоматизации и т.п.


    1. panablack Автор
      09.10.2024 14:42

      Тот кому эта статья пригодится на проде, скорее всего уже настроил IaC на keycloak и ему не составит проблем данную статью автоматизировать.


      1. Caraul
        09.10.2024 14:42

        Статья, конечно, не об этом, но практика показывает, что все, что можно открыть и нажать - открывают и нажимают. Культуру IaC надо продвигать в массы, хотя бы на таких примерах.


        1. panablack Автор
          09.10.2024 14:42

          Я с Вами согласен. В следующих публикациях попробую приложить ещё варианты IaC-a (надеюсь, что сильно читателей это не оттолкнет из-за увеличения количества инструментов и технологий которых необходимо знать в рамках одной статьи). Спасибо.