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, как на проде¶
keytool -genkeypairсоздаёт пару ключей.keytool -certreqсоздаёт запрос на подпись (CSR): публичный ключ и Subject, подписанные вашим ключом. SAN в CSR передают тем же-ext san=....- CSR отправляется в CA. CA проверяет, что домен ваш, и возвращает подписанный сертификат с цепочкой.
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 не доверяет |
Диагностика: