Спецификация: RFC 8705
С mutual TLS клиент аутентифицируется на authorization server сертификатом X.509 вместо общего секрета, а authorization server привязывает выпущенный access token к этому сертификату — украденный токен без соответствующего приватного ключа бесполезен. Механизмы независимы: любой из них работает без второго.
Метаданные регистрации клиента
- Типы субъекта
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, которого нет
Терминирование 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 проверенный им сертификат — а единого заголовка и единой кодировки для этого у прокси нет.
Реализована именно эта часть: заголовок и кодировка сертификата настраиваются в Central для каждого тенанта.
Настройка
В админке Central, на странице тенанта Challenges & Security, в разделе 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-клиент, и привязан он к сертификату, который есть только уedge, — и отклоняет его в заголовкеAuthorization. Значит, mTLS-клиент не может напрямую обратиться к ресурсу заedgeсо своим токеном, привязанным к его сертификату.