Спецификация: RFC 8705
С mutual TLS клиент аутентифицируется на authorization server сертификатом X.509 вместо общего секрета, а authorization server привязывает выпущенный access token к этому сертификату — украденный токен без соответствующего приватного ключа бесполезен. Механизмы независимы: любой из них работает без второго.
Статус
Реализован. Отмечено пунктов: 17 из 17.
Метаданные регистрации клиента
- Типы субъекта
tls_client_auth—subject_dn,san_dns,san_uri,san_ip,san_email(§2.1.2) — хранятся у клиента одной парой «тип субъекта + значение», потому что сертификат, совпавший хотя бы с одним из них, клиента все равно аутентифицирует - Флаг намерения клиента
tls_client_certificate_bound_access_tokens(§3.4) -
self_signed_tls_client_auth(§2.2) — сертификат аутентифицирует клиента, если его открытый ключ есть в зарегистрированномjwksклиента — том же столбце, что читаетprivate_key_jwtиз RFC 7523. Ключи сравниваются по закодированному ключевому материалу, поэтому зарегистрированный ключ с другими необязательными полями (kid,use) все равно совпадает. Безjwksрегистрация с этим методом отклоняется, а зарегистрированные ключи аутентифицируют только сертификат и никогда — client assertion - Любой из двух mTLS-методов отклоняется при регистрации, если прокси тенанта не терминирует mutual TLS (см. Терминирование TLS): такой клиент никогда бы не аутентифицировался
Аутентификация клиента
-
tls_client_authиself_signed_tls_client_authна token endpoint (§2) во всех трех grant type: клиент, зарегистрированный для любого из методов, аутентифицируется подходящим сертификатом — и только им. Присланный рядом секрет не проверяется, а отсутствующий или несовпавший сертификат даетinvalid_client - Оба метода на introspection, revocation и pushed authorization request endpoint — правило то же, поэтому клиент без секрета может получить токены, а затем читать, отзывать и отправлять pushed request с ними. На introspection дополнительно отклоняется вызов, в котором предъявлен только
client_id: RFC 7662 требует аутентифицировать вызывающего, а идентификатор публичного клиента — не секрет
Access token с привязкой к сертификату
- Claim
cnfс членомx5t#S256в выпускаемых access token (§3.1) — SHA-256 от DER-представления сертификата в base64url; проставляется клиенту, который аутентифицировался сертификатом или зарегистрировал флаг §3.4 - Refresh token, привязанные к сертификату (§4, §7.1): привязка сохраняется за grant, обновить его можно только тем же сертификатом, и токен продлевается на месте вместо ротации — так же, как токен с привязкой DPoP
- Обе привязки сразу: токен с привязкой к сертификату, у которого есть еще и
jktпо RFC 9449, сохраняет обе, а не ту из них, которая слабее -
x5t#S256в ответах token introspection (§3.2) —/introspectвозвращаетcnfтокена целиком, поэтому resource server, который проверяет токены через introspection, видит отпечаток сертификата вместе сjktот DPoP, если он есть - Проверка привязки на
/userinfo(§3) — токен с привязкой к сертификату принимается, только если запрос пришел с тем сертификатом, к которому он привязан; отсутствующий, нечитаемый или чужой сертификат даетinvalid_token. Проверка следует привязке в самом токене, а не текущей регистрации клиента, поэтому клиент, снявший флаг §3.4, не сможет воспользоваться старым привязанным токеном без сертификата - Метаданные authorization server
tls_client_certificate_bound_access_tokens(§3.3), а такжеtls_client_auth/self_signed_tls_client_authвtoken_endpoint_auth_methods_supported— публикуются, если оператор задал их в хранимом документе метаданных: документ передает их без изменений. Автоматически они не выводятся: документ один на всю инсталляцию, а примет ли она сертификат, зависит от прокси, который настроил каждый тенант -
mtls_endpoint_aliases(§5) — для тенанта с настроенным mTLS-listener’ом документ дискавери переиздает пути token, introspection, revocation, pushed-authorization-request и userinfo endpoint под собственным внешним адресом listener’а, чтобы клиент мог узнать адрес, который действительно терминирует его сертификат. Без такой настройки поле вообще не добавляется — сервис не обещает listener, которого нет
Listener mutual TLS
- Отдельный listener
auth(по умолчанию порт8083), который требует клиентский сертификат при handshake; соединение без сертификата отклоняется (MutualTlsListenerSpec) - Listener обслуживает только endpoint’ы, для которых важен сертификат:
/token,/introspect,/revoke,/parи/userinfo - Включается блоком
mutual-tlsв конфигурацииauthс полямиcertificate,private-key,trusted-certificatesиexternal-url.trusted-certificatesобязателен, возврата к trust store JDK нет - DPoP proof, отправленный на listener, сравнивается с адресом самого listener, а proof, сделанный для основного адреса, там отклоняется (
MutualTlsListenerSpec)
Терминирование TLS (§6.5)
Спецификация выносит это за скобки: «How the client certificate metadata is securely communicated between the intermediary and the application server… is out of scope of this specification». Сервис auth в Versola всегда стоит за обратным прокси, который терминирует TLS, поэтому прежде чем заработает что-либо из перечисленного выше, прокси должен передать в auth проверенный им сертификат — а единого заголовка и единой кодировки для этого у прокси нет.
Это второй способ получить сертификат рядом с listener’ом выше: заголовок и кодировка сертификата настраиваются в Central для каждого тенанта.
Настройка
В админке Central, на странице тенанта Security, в карточке Tokens & keys, в разделе Mutual TLS:
- Отметьте «This tenant’s reverse proxy terminates mutual TLS».
- Выберите пресет под свой обратный прокси либо задайте заголовок и кодировку вручную.
| Пресет | Заголовок | Кодировка |
|---|---|---|
| nginx / ingress-nginx | ssl-client-cert | PEM в URL-кодировке |
| Traefik | X-Forwarded-Tls-Client-Cert | DER в base64 |
| Custom | любое имя заголовка | PEM в URL-кодировке или DER в base64 |
Пустая настройка означает, что прокси тенанта mTLS не терминирует, — состояние, в котором находится каждый тенант до того, как ее задали.
Заголовок читается только для клиента, на которого сертификат влияет: который аутентифицируется сертификатом, а на /token — еще и чьи токены к сертификату привязываются. Для остальных он игнорируется, поэтому тенант, чей прокси пересылает сертификат на каждом соединении, не привязывает токены, которые никто не просил привязывать, и не ломает запросы, прекрасно аутентифицирующиеся секретом. Заголовок, который пришел, но не читается, отклоняется как invalid_client, а не считается отсутствующим сертификатом: причина описывает прокси инсталляции, поэтому она пишется в лог, а не возвращается клиенту.
Примечания
-
Токены с привязкой к сертификату на
edge. Обычный портedgeне терминирует клиентский TLS и не читает заголовок с сертификатом, поэтому сверить сертификат вызывающего сcnf.x5t#S256(§3) он не может. Там он принимает токен, привязанный к сертификату, только из собственной cookieEDGE_SESSION(токен, которыйedgeполучил как mTLS-клиент) и отклоняет такой токен в заголовкеAuthorization(certificate_bound_token_outside_session). Вызывающий со своим токеном, привязанным к сертификату, обращается к API на необязательный mutual-TLS listeneredge:edgeчитает сертификат из handshake и принимает токен, только если SHA-256-отпечаток сертификата равенcnf.x5t#S256. -
Продленные сертификаты и токены. Токен несет отпечаток сертификата, на который выпущен. После того как клиент продлил сертификат (например, через EST), токен, привязанный к старому сертификату, с новым не работает: клиент продолжает предъявлять старый сертификат со старыми токенами, пока они не истекут, или запрашивает новые токены с новым сертификатом.
Куда смотреть
Токены с привязкой к отправителю объясняют привязку к сертификату, а клиентские сертификаты Edge — сертификаты, которые предъявляет edge.