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

Ни для кого не секрет, что для улучшения защиты доступа к веб-ресурсам многие компании используют или начинают использовать mTLS — когда помимо проверки серверного сертификата проверяется сертификат пользователя. В данной статье мы расскажем:
Статья рассчитана на людей, которые ранее были знакомы с IAM и, в частности, с keycloak-ом. Поэтому в этой части не будет «базы» по SAML2, OAuth2/OIDC и в целом по IAM (на Хабре есть хорошие статьи на эту тему). Также для понимания данной статьи необходимы знания базовых абстракций kubernetes и умение читать его манифесты.
В ресерче материалов для данного поста и реализации данной технологии на проде принимали участие еще несколько человек. Указать их соавторами на Хабре нет возможности, поэтому озвучу их тут: Ян Краснов, Иван Николаев, Максим Сушков, Иван Кодянов.
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. Приведем пример.
Для того чтобы проверить валидность сертификата, предоставляемого пользователем, необходимо куда-нибудь поместить CA, которым был подписан промежуточный (клиентский) сертификат. В нашем случае мы поместим его в Secret k8s. Имя файла сертификата в секрете обязательно должно быть ca.crt:
Проверка сертификата осуществляется на каждом хосте отдельно. Поэтому для включения проверки сертификата на нужном сервисе необходимо приписать аннотации в ingress-манифест:
Полный манифест будет выглядеть примерно так:
После добавления данных аннотаций 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-контроллер:
7. В манифесте Ingress-а keycloak добавляем следующие аннотации для проверки клиентского сертификата и передачи его на сервис keycloak:
После выполнения данных действий при переходе на сервис c настроенной аутентификацией, используя keycloak, мы увидим при входе следующую картину:
Часть информации скрыта, но можно понять, что keycloak увидел клиентский сертификат, сопоставил email и понял, что они совпадают с пользователем sandzhiev, и предлагает выполнить вход под этой учетной записью.
Если мы прочтем заметку ребят на GitHub
mTLS: When certificate authentication is done wrong (о ней я узнал из канала k8security) или посмотрим их выступление на blackhat USA 2023, то поймем, что надо:
Вот как это настроить.
1. Необходимо внутрь keycloak (хранилище доверенных сертификатов (truststore)) положить CA со всей цепочкой сертификатов. Для этого нужно необходимые нам сертификаты (CA и intermediate) преобразовать в .jks (Java KeyStore, актуально до версии keycloak 23.х, в версиях 24+ можно закидывать даже pem), положить его в secret k8s и примаунтить этот secret к deployment/statefullset keycloak — как показано ниже.
Создаем jks и кладем его в секрет k8s:
Примаунтим secret к deployment/statefullset keycloak, а также зададим переменные, для того чтобы keycloak подцепил .jks:
2. Переходим в «админку» keycloak, в настройки x509/Validate Username Form созданного нами ранее Browser flow (Configure → Authentication → <имя нашего Browser flow> → x509/Validate Username Form → Setting)
и меняем значение следующих параметров на True:
После вышеописанных действий в keycloak начинает работать следующий workflow.
В настроенном нами 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:
2. Настройка OCSP.
Переходим в админку keycloak, в настройки x509/Validate Username Form созданного нами ранее Browser flow (Configure → Authentication → <имя нашего Browser flow> → x509/Validate Username Form → Setting) и находим строки, связанные с OCSP:
После данных настроек keycloak добавит в свой workflow следующие пункты.
При успешной проверке CRL и (или) OCSP вы зайдете на необходимый ресурс. При неправильной настройке или при попытке зайти с отозванным сертификатом будет получен примерно такой ответ:
Предыдущие посты нашей команды вы можете прочесть по следующим ссылкам:

Ни для кого не секрет, что для улучшения защиты доступа к веб-ресурсам многие компании используют или начинают использовать mTLS — когда помимо проверки серверного сертификата проверяется сертификат пользователя. В данной статье мы расскажем:
- Как настроить проверку клиентских сертификатов в k8s на ingress-контроллере.
- Как передать клиентский сертификат с ingress-контроллера в keycloak с мапингом сертификата к учетной записи Keycloak-a.
- Как и зачем настраивать перепроверку клиентского сертификата в keycloak.
- Как проверить отозванные клиентские сертификаты с помощью 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
Host certificate
Sign host csr with rootCA (see below for file localhost.ext):
Client (user) certificate
Sign client csr with rootCA:
Import client key and crt in keystore to create the «certificate» to be used in the browser:
Далее необходимо добавить .p12-сертификат на компьютер пользователя.
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 вы зайдете на необходимый ресурс. При неправильной настройке или при попытке зайти с отозванным сертификатом будет получен примерно такой ответ:

Прошлые публикации
Предыдущие посты нашей команды вы можете прочесть по следующим ссылкам: