Keycloak. Мапинг учеток через mTLS c двойной проверкой в kubernetes
Продолжаем с делиться экспертизой отдела Security services infrastructure (департамент Security Services компании «Лаборатории Касперского»). В данном посте мы разберем, как легко настроить mTLS, обращаясь к ресурсам в k8s через ingress-контроллер, и подсоединить это все к keycloak. Пост будет полезен тем, кто в своей инфраструктуре использует PKI и, в частности, клиентские сертификаты.
Ни для кого не секрет, что для улучшения защиты доступа к веб-ресурсам многие компании используют или начинают использовать 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. Приведем пример.
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:
Проверка сертификата осуществляется на каждом хосте отдельно. Поэтому для включения проверки сертификата на нужном сервисе необходимо приписать аннотации в 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), то поймем, что надо:
- обновить 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 → → 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 → → 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 → → 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 вы зайдете на необходимый ресурс. При неправильной настройке или при попытке зайти с отозванным сертификатом будет получен примерно такой ответ:
Прошлые публикации
Предыдущие посты нашей команды вы можете прочесть по следующим ссылкам:
- Keycloak. Админский фактор и запрет аутентификации
- Keycloak. Standalone-HA в k8s и закрытие админки на ingress-e с переводом на localhost
- Что и зачем почитать DevSecOps-у: личный опыт