Перейти к содержанию

TLS

TLS (Transport Layer Security) - протокол поверх TCP, который шифрует соединение, защищает данные от подмены и подтверждает, что клиент говорит с настоящим сервером. Наследник SSL, поэтому TLS до сих пор часто зовут «SSL».

HTTPS - HTTP поверх TLS, порт по умолчанию 443. Сам HTTP не меняется (см. HTTP), меняется канал под ним.

TLS не знает, какой протокол идёт внутри, и шифрует любой поток байт. HTTPS - самый известный случай, так же работают SMTPS, IMAPS, LDAPS, подключения к Postgres и Kafka. Для UDP есть DTLS, а QUIC (основа HTTP/3) встраивает handshake TLS 1.3 в свой транспорт.


Какую задачу решает HTTPS

При обычном HTTP между клиентом и сервером три проблемы, и TLS закрывает каждую своим механизмом:

Проблема Что происходит Чем закрывает TLS
Подслушивание Любой узел по пути (Wi-Fi, провайдер) видит запросы и ответы Шифрование
Подмена данных Ответ можно незаметно изменить по дороге Проверка целостности (MAC, AEAD)
Подмена сервера Клиент не знает, говорит он с настоящим сервером или с самозванцем (MITM) Сертификат

Шифрование без проверки сервера почти бесполезно: можно надёжно зашифровать трафик прямо для злоумышленника. Поэтому сертификаты - главная часть TLS.

Версии:

  • SSL 2.0, SSL 3.0, TLS 1.0, TLS 1.1 - устарели и запрещены (TLS 1.0 и 1.1 официально выведены RFC 8996, 2021)
  • TLS 1.2 (RFC 5246, 2008) - ещё массово жив
  • TLS 1.3 (RFC 8446, 2018) - текущий: быстрее handshake, только стойкие шифры, обмен ключом всегда с forward secrecy

Как заставить ходить только по HTTPS:

  • не слушать HTTP вовсе: порт закрыт, ответить некому
  • редирект с 80 на 443 плюс заголовок Strict-Transport-Security (HSTS): браузер запоминает, что сайт только HTTPS, и сам не идёт по HTTP

Асимметричная криптография

Пара ключей - сервер генерирует приватный и публичный ключ, связанные математически.

  • Приватный ключ знает только сервер. Он никогда не покидает сервер.
  • Публичный ключ раздают всем.

Асимметричные ключи дают две операции:

  • Подпись. Сервер подписывает данные приватным ключом. Любой, у кого есть публичный ключ, проверит, что подпись сделал владелец приватного ключа и данные не менялись.
  • Обмен ключом. Две стороны договариваются об общем секрете, которого не видит никто на линии. В современном TLS для этого используется ECDHE на временных парах ключей. Ключ из сертификата в обмене не участвует, он только подписывает.

Асимметричная криптография медленная. Поэтому ей только устанавливают соединение и договариваются о ключе, а сам трафик шифруют быстрым симметричным шифром.

Задача Алгоритмы
Подпись сертификата и handshake RSA, ECDSA, Ed25519
Обмен ключом ECDHE (X25519, P-256). В TLS 1.2 ещё был обмен через RSA
Шифрование трафика AES-GCM, ChaCha20-Poly1305 (AEAD: шифрование и целостность сразу)
Хэш SHA-256, SHA-384

Forward secrecy - пары ECDHE одноразовые, на каждое соединение новые. Если приватный ключ сервера когда-нибудь утечёт, записанный раньше трафик им всё равно не расшифровать. При старом обмене через RSA это было возможно: клиент шифровал общий секрет ключом из сертификата. Поэтому TLS 1.3 такой обмен убрал.

Остаётся вопрос: откуда клиент знает, что публичный ключ принадлежит именно localhost:8181, а не злоумышленнику? На него отвечает сертификат.


Сертификат

Сертификат X.509 - публичный ключ плюс данные о владельце, заверенные чьей-то подписью.

Пример: сертификат movie-service из лабы.

Subject:    CN=localhost, OU=Payara, O=Payara Foundation, ...   <- кому выдан
SAN:        DNS:localhost, IP:127.0.0.1                         <- для каких адресов действует
Issuer:     CN=localhost, ...                                   <- кто подписал
Validity:   2026-10-22 ... 2036-10-19                           <- срок действия
Public Key: RSA 2048 ...                                        <- публичный ключ сервера
Extensions: Key Usage, Extended Key Usage, Basic Constraints    <- для чего ключ, CA или нет
Signature:  ...                                                 <- подпись Issuer над всем выше
  • Сертификат не секретный: сервер отдаёт его каждому клиенту. Секретный только приватный ключ, который лежит рядом.
  • SAN (Subject Alternative Name) - расширение со списком DNS-имён и IP, для которых сертификат действует. Браузеры смотрят только SAN, поле CN они не проверяют (Chrome - с 2017 года).
  • Java и CN. Java сверяет CN, только если в SAN нет ни одного DNS-имени. IP она ищет только в SAN.
  • Wildcard *.example.com покрывает один уровень: api.example.com подходит, а a.b.example.com и сам example.com нет.
  • Extended Key Usage - serverAuth для сервера, clientAuth для клиента в mTLS.
  • Basic Constraints - CA:true у сертификата, которым можно подписывать другие, CA:false у конечного.

Команды:

openssl x509 -in cert.pem -noout -text        # все поля сертификата из файла
keytool -printcert -file movie-service.cer    # то же через keytool

Удостоверяющий центр

Удостоверяющий центр (CA, Certificate Authority) - организация, которая проверяет, что домен ваш, и подписывает сертификат своим ключом: Let's Encrypt, DigiCert, Sectigo, GlobalSign.

Уровни проверки:

  • DV (Domain Validation) - проверяется только владение доменом: файл по HTTP (HTTP-01) или запись в DNS (DNS-01). Выдаётся автоматически за минуты.
  • OV (Organization Validation) - плюс проверка организации.
  • EV (Extended Validation) - расширенная проверка документов. Браузеры давно не выделяют EV в адресной строке, поэтому смысла в нём почти не осталось.

Let's Encrypt - бесплатный CA, выдаёт DV-сертификаты автоматически по протоколу ACME (RFC 8555), обычно через certbot (см. Nginx). Сертификат живёт 90 дней, с февраля 2027 года 64 дня, с февраля 2028 года 45 дней. Продление автоматизируют.

Note

Срок жизни публичных сертификатов сокращают решением CA/Browser Forum (2025): не больше 200 дней с марта 2026 года, 100 дней с марта 2027 года, 47 дней с марта 2029 года. Без автоматического продления дальше не обойтись.

Отзыв сертификата. Если ключ утёк, сертификат отзывают до истечения срока:

  • CRL - список отозванных сертификатов, который CA публикует и клиент скачивает
  • OCSP - клиент спрашивает CA про конкретный сертификат
  • OCSP stapling - сервер сам прикладывает свежий ответ OCSP к handshake, клиенту не надо ходить в CA

Let's Encrypt отключил OCSP в августе 2025 года и оставил только CRL: запрос OCSP раскрывает CA, какие сайты открывает пользователь.

Certificate Transparency - CA обязан записать каждый выпущенный сертификат в публичные журналы, браузеры без этого сертификат не принимают. Так владелец домена видит, если кто-то выпустил сертификат на его имя.


Цепочка доверия

Конечный сертификат обычно подписывает не корневой CA, а промежуточный:

Root CA  --подписал-->  Intermediate CA  --подписал-->  сертификат example.com
   ^
   +-- заранее лежит в ОС, браузере, JDK как доверенный

Клиент поднимается по цепочке, пока не дойдёт до корня, которому уже доверяет. Если дошёл и все подписи сходятся, сертификат принят. Корневой сертификат подписан сам собой, его подлинность держится только на том, что он заранее лежит в доверенном хранилище.

  • Хранилища корней: у ОС (Windows, связка ключей macOS, /etc/ssl/certs в Linux), у Firefox (Mozilla NSS), у Java (cacerts)
  • Сервер отдаёт цепочку: свой сертификат и промежуточные, корень не нужен, он у клиента уже есть

Warning

Забыли промежуточный сертификат - браузер часто всё равно откроет сайт: он сам докачивает недостающие звенья. Java и curl этого не делают и падают с PKIX path building failed. Проверяй цепочку через openssl s_client -showcerts, а не браузером.


Самоподписанный сертификат

Самоподписанный сертификат - сертификат, у которого Issuer совпадает с Subject: сервер подписал сам себя своим же приватным ключом. Цепочка из одного звена, и этого звена нет ни в одном доверенном хранилище. Поэтому:

  • браузер показывает «Подключение не защищено»
  • Java выбрасывает PKIX path building failed
  • curl без -k отказывается подключаться

Шифрование при этом работает так же, как с сертификатом от CA. Разница только в доверии: его настраивают вручную, кладут сертификат клиенту в truststore (см. Передать доверие другой стороне). Так в лабе oscar-service доверяет movie-service.

Для локальной разработки удобнее свой маленький CA: mkcert создаёт локальный корневой CA, ставит его в хранилища ОС и браузеров (и в cacerts Java, если задан JAVA_HOME) и выпускает от него сертификаты для localhost. Тогда предупреждений нет без отключения проверки.


TLS-соединение

Упрощённый handshake TLS 1.3 на примере лабы, где oscar-service вызывает movie-service:

Клиент (oscar-service)      Сервер (movie-service :8181)
  │── ClientHello: версии, шифры, ключ ECDHE, SNI ────►│
  │◄─ ServerHello: выбранный шифр, ключ ECDHE ─────────│
  │  дальше всё зашифровано                            │
  │◄─ EncryptedExtensions: ALPN ───────────────────────│
  │◄─ Certificate: s1as (CN=localhost) ────────────────│
  │◄─ CertificateVerify: подпись приватным ключом ─────│
  │◄─ Finished ────────────────────────────────────────│
  │                                                    │
  │  клиент проверяет:                                 │
  │  1) цепочка ведёт к сертификату из truststore      │
  │  2) срок действия не истёк                         │
  │  3) имя хоста из URL есть в SAN                    │
  │  4) подпись CertificateVerify верна                │
  │                                                    │
  │── Finished ───────────────────────────────────────►│
  │═════ дальше HTTP, зашифрованный общим ключом ══════│

Все четыре проверки делает клиент, и соединение падает на любой из них.

Имя хоста (проверка 3). Поэтому в лабе пришлось перевыпустить сертификат oscar-service: он был выписан на CN=macbook, а к сервису обращаются по localhost. Строгий клиент упадёт с No subject alternative DNS name matching localhost found.

Подпись CertificateVerify (проверка 4). Сертификат может скопировать кто угодно, но подписать CertificateVerify может только владелец приватного ключа. Украденный сертификат без ключа ничего не даёт.

Что ещё в ClientHello:

  • SNI (Server Name Indication) - имя хоста, к которому идёт клиент. На одном IP живёт много сайтов, и по SNI сервер выбирает, какой сертификат отдать.
  • SNI виден всем по пути. ClientHello не зашифрован, спрятать имя хоста можно только через ECH (Encrypted Client Hello).
  • ALPN - какой протокол пойдёт поверх TLS: h2 (HTTP/2) или http/1.1.

Чем TLS 1.3 отличается от 1.2:

  • Handshake за один round-trip вместо двух.
  • Всё после ServerHello шифруется, сертификат больше не идёт открытым текстом.
  • Обмена ключом через RSA нет: полный handshake всегда идёт через (EC)DHE и даёт forward secrecy.
  • Остались только AEAD-шифры, на практике три набора: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256.
  • Повторное соединение можно возобновить по session ticket, а с 0-RTT отправить данные сразу. Данные 0-RTT можно переиграть (replay), поэтому так шлют только идемпотентные запросы.

mTLS (mutual TLS) - сервер в handshake тоже просит сертификат (CertificateRequest), и клиент отвечает своими Certificate и CertificateVerify. Обе стороны знают, кто на другом конце. Так сервисы подтверждают друг друга внутри кластера (см. Безопасность микросервисов).

Команды:

openssl s_client -connect localhost:8181 -servername localhost -showcerts   # цепочка, версия TLS, шифр
openssl s_client -connect localhost:8181 -tls1_3                            # проверить, что сервер умеет TLS 1.3
curl -v https://localhost:8181/                                             # handshake и проверка сертификата

Форматы файлов

Формат Расширения Что внутри
PEM .pem, .crt, .cer, .key Base64 между строками -----BEGIN CERTIFICATE----- и -----END ...-----. Сертификат, ключ или цепочка подряд
DER .der, .cer Тот же сертификат в бинарном виде
PKCS#12 .p12, .pfx Контейнер под паролем: приватный ключ и цепочка сертификатов
JKS .jks Старый формат хранилища Java
CSR .csr Запрос на подпись сертификата для CA

Расширение .cer и .crt ничего не гарантирует: внутри может быть и PEM, и DER. PEM - текст, его видно через cat.

openssl x509 -inform der -in cert.der -out cert.pem           # DER -> PEM
openssl pkcs12 -in keystore.p12 -nokeys -out chain.pem        # достать сертификаты из .p12 без ключа
openssl pkcs12 -in keystore.p12 -nocerts -nodes -out key.pem  # достать ключ без пароля, например для nginx

Java: KeyStore и truststore

KeyStore - файл-контейнер java.security.KeyStore, защищённый паролем. Внутри записи, у каждой есть имя (alias).

Тип записи Что содержит Пример
PrivateKeyEntry Приватный ключ и цепочка сертификатов к нему s1as - личность сервера
TrustedCertificateEntry Только чужой сертификат, без ключа movie-service в cacerts oscar-service
SecretKeyEntry Симметричный ключ В TLS не используется

Форматы:

  • PKCS12 (.p12, .pfx) - стандартный формат, по умолчанию с Java 9. Его понимают openssl и браузеры. Nginx читает только PEM, из .p12 его достают через openssl pkcs12.
  • JKS (.jks) - старый формат, специфичный для Java. Встречается в основном в легаси.

Keystore и truststore: один формат, разные роли

  • Keystore отвечает на вопрос «кто я?». В нём мой приватный ключ и мой сертификат, которые я предъявляю собеседнику. Из него читает KeyManager.
  • Truststore отвечает на вопрос «кому я доверяю?». В нём чужие сертификаты (корневые CA или конкретные самоподписанные), без приватных ключей. Из него читает TrustManager.

Технически это одинаковые файлы KeyStore, различаются они только тем, как их используют.

                SSLContext
               +-------------------------------+
  keystore --> | KeyManager    предъявить себя |
truststore --> | TrustManager  проверить чужой |
               +-------------------------------+
                               |
       SSLSocket, HttpClient, RestClient, листенер Grizzly
Сторона Keystore Truststore
Сервер Обязательно: предъявить сертификат Только при mTLS: проверить клиента
Клиент Только при mTLS: предъявить себя Обязательно: проверить сервер

Итого: keystore - кто я, truststore - кому я доверяю. Сервер предъявляет сертификат из keystore, клиент проверяет его по своему truststore: цепочку, срок, имя хоста и подпись handshake.

Хранилища по умолчанию

В JDK есть встроенный truststore $JAVA_HOME/lib/security/cacerts (пароль changeit) с корневыми CA. Поэтому Java из коробки доверяет, например, https://google.com. Keystore по умолчанию пустой, клиенту без mTLS он и не нужен.

Хранилища по умолчанию переопределяются свойствами JVM. Тогда любой HTTPS-клиент в этой JVM, который не настраивал SSL сам, использует эти файлы:

-Djavax.net.ssl.keyStore=...      -Djavax.net.ssl.keyStorePassword=...
-Djavax.net.ssl.trustStore=...    -Djavax.net.ssl.trustStorePassword=...
-Djavax.net.debug=ssl:handshake   # печатать handshake в лог, когда непонятно, почему не подключается

Свойства JVM меняют хранилища для всего процесса. В Spring Boot отдельному клиенту или серверу дают свои хранилища через SSL bundles: файлы описывают в spring.ssl.bundle.jks.<имя>.*, сервер подключает bundle через server.ssl.bundle=<имя>, клиент - в своих настройках.

Команды:

keytool -list -v -keystore keystore.p12 -storepass changeit   # что лежит в хранилище
keytool -list -cacerts -storepass changeit                    # корневые CA в JDK

Как выпускать сертификаты

Самоподписанный через keytool

# -alias:     имя записи в хранилище
# -keyalg:    алгоритм и длина ключа
# -validity:  срок в днях
# -dname:     Subject
# -ext san:   адреса, для которых действует сертификат
keytool -genkeypair \
  -alias s1as \
  -keyalg RSA -keysize 2048 \
  -validity 3650 \
  -dname "CN=localhost, OU=Payara, C=UK" \
  -ext san=dns:localhost,ip:127.0.0.1 \
  -keystore keystore.p12 -storepass changeit

Команда создаёт пару ключей и сертификат, подписанный своим же ключом, и кладёт всё в хранилище как PrivateKeyEntry. Так в лабе перевыпускали s1as у oscar-service.

Передать доверие другой стороне

Из keystore сервера извлекается только сертификат, без ключа, и кладётся в truststore клиента как доверенный. Приватный ключ при этом никуда не копируется.

keytool -exportcert -alias s1as -keystore movie/keystore.p12 -file movie-service.cer
keytool -importcert -alias movie-service -file movie-service.cer -keystore oscar/cacerts.p12

Сертификат от CA, как на проде

  1. keytool -genkeypair создаёт пару ключей.
  2. keytool -certreq создаёт запрос на подпись (CSR): публичный ключ и Subject, подписанные вашим ключом. SAN в CSR передают тем же -ext san=....
  3. CSR отправляется в CA. CA проверяет, что домен ваш, и возвращает подписанный сертификат с цепочкой.
  4. keytool -importcert -trustcacerts кладёт ответ CA под тот же alias, и сертификат «прирастает» к приватному ключу.

Клиентам ничего настраивать не надо: корневой CA уже есть в их стандартном truststore.

То же через openssl

# ключ и самоподписанный сертификат одной командой
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -keyout key.pem -out cert.pem \
  -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

# упаковать ключ и сертификат в .p12 для Java
openssl pkcs12 -export -in cert.pem -inkey key.pem -name s1as -out keystore.p12

Пример из лабы: Payara

В каждом домене Payara в payara7/glassfish/domains/<домен>/config/ лежат два хранилища.

keystore.p12 - keystore домена:

  • s1as - сертификат сервера, который предъявляется на HTTPS-порту
  • glassfish-instance - служебная запись для кластеров, в лабе не нужна

create-domain сам генерирует s1as с CN=<имя машины>. Поэтому у oscar-service и оказался CN=macbook.

cacerts.p12 - truststore домена: копия cacerts из JDK с корневыми CA, плюс собственный s1as домена, плюс в oscar-service импортированный сертификат movie-service.

В domain.xml они подключаются так:

<!-- HTTPS-листенер (http-listener-2, порт 8181 и 10081) предъявляет запись s1as -->
<ssl classname="com.sun.enterprise.security.ssl.GlassfishSSLImpl" cert-nickname="s1as"/>

        <!-- JVM домена использует эти файлы как хранилища по умолчанию -->
<jvm-options>-Djavax.net.ssl.keyStore=${com.sun.aas.instanceRoot}/config/keystore.p12</jvm-options>
<jvm-options>-Djavax.net.ssl.trustStore=${com.sun.aas.instanceRoot}/config/cacerts.p12</jvm-options>
браузер --https--> Vite :5173 --https, secure: false--> movie-service :8181   keystore: s1as, CN=localhost
                       |
                       +------https, secure: false--> oscar-service :10081  keystore: s1as, CN=localhost
                                                            |
                                                            | RestClient без своих SSL-настроек
                                                            | берёт javax.net.ssl.trustStore = cacerts.p12
                                                            v
                                               https://localhost:8181 (movie-service)
                                               сертификат принят: в cacerts.p12 oscar-service
                                               есть запись movie-service
  • Почему oscar-service доверяет movie-service без единой строки SSL-кода. Spring RestClient использует SSL-контекст JVM по умолчанию, а Payara направила его на cacerts.p12. Если удалить оттуда запись movie-service, вызовы упадут с PKIX path building failed, и oscar-service ответит 503.
  • Почему Vite не ругается. secure: false в прокси отключает проверку сертификата. Это допустимо только для локальной разработки.
  • Почему HTTP недоступен. Выключен http-listener-1 (порты 8080 и 10080). Сокет не открыт, на обычный HTTP отвечать некому.
  • Почему после замены сертификата нужен рестарт. Grizzly читает keystore при старте листенера и дальше держит ключ в памяти.
# выключить HTTP-листенер домена
asadmin set configs.config.server-config.network-config.network-listeners.network-listener.http-listener-1.enabled=false

# что лежит в keystore домена
keytool -list -v -keystore payara7/glassfish/domains/oscar-service/config/keystore.p12 -storepass changeit

Частые ошибки

Ошибка Причина
PKIX path building failed: unable to find valid certification path Сертификата сервера или его CA нет в truststore клиента, или сервер не отдал промежуточный сертификат
No subject alternative DNS name matching <host> found Имени хоста из URL нет в SAN
No name matching <host> found В SAN нет DNS-имён, а CN не совпал с хостом
No subject alternative names matching IP address <ip> found Обращение по IP, а в SAN нет записи IP:. По CN Java IP не сверяет
CertificateExpiredException Истёк срок validity
keystore password was incorrect Неверный -storepass
Received fatal alert: handshake_failure Стороны не договорились о версии TLS или шифре, либо при mTLS клиент не предъявил сертификат
Unrecognized SSL message, plaintext connection? Клиент говорит TLS, а на порту обычный HTTP
curl: (60) SSL certificate problem: self-signed certificate Самоподписанный сертификат, которому curl не доверяет

Диагностика:

keytool -list -v -keystore keystore.p12 -storepass changeit       # что лежит в хранилище
openssl s_client -connect localhost:8181 -showcerts               # какой сертификат реально отдаёт сервер