Ошибка TLS-рукопожатия: причины и способы исправить

9 минут чтения
Средний рейтинг статьи — 4.8

Ошибка TLS-рукопожатия появляется, когда клиент и сервер не смогли договориться о безопасном HTTPS-соединении. Для пользователя это выглядит как «сайт не открывается», ERR_SSL_PROTOCOL_ERROR, SSL_ERROR_HANDSHAKE_FAILURE_ALERT, TLS handshake failed или похожее сообщение. Для администратора — как сбой на уровне, который происходит ещё до HTTP-запроса: до GET /, до ответа 200, 301 или 500.

Главная сложность в том, что ошибка TLS-рукопожатия не указывает на одну конкретную причину. Это может быть просроченный сертификат, неполная цепочка, неправильный server_name в Nginx, отсутствие SNI, несовместимые версии TLS, проблемы на CDN, балансировщике, фаерволе или даже расхождение системного времени на клиенте.

Разберём, как работает рукопожатие TLS, где оно чаще всего ломается и как быстро сузить круг причин. Материал рассчитан на владельцев сайтов, разработчиков и DevOps-инженеров: с примерами команд, логов и практическим чеклистом исправления.

Что означает ошибка TLS-рукопожатия

TLS-рукопожатие — это начальная фаза HTTPS-соединения. Клиент и сервер ещё не передают обычные HTTP-данные, а договариваются о параметрах защищённого канала.

Упрощённо процесс выглядит так:

1) Клиент отправляет ClientHello — сообщает поддерживаемые версии TLS, наборы шифров, расширения, имя домена через SNI, поддерживаемые протоколы через ALPN, например h2 или http/1.1.

2) Сервер отвечает ServerHello — выбирает версию TLS, шифр, отдаёт сертификат и дополнительные данные для обмена ключами.

3) Клиент проверяет сертификат — смотрит срок действия, доменное имя, цепочку доверия, корневой центр сертификации, иногда статус отзыва.

4) Стороны согласуют ключи — после этого появляется защищённый канал, и только затем начинается обычный HTTP-обмен.

Если сбой происходит на любом из этих этапов, клиент сообщает об ошибке TLS handshake. Подробный технический разбор пакетов есть в статье TLS handshake под микроскопом: разбор по шагам с tcpdump, а базовую картину HTTPS, SSL, TLS и сертификатов можно освежить в материале Как работает HTTPS. Разбираемся в SSL, TLS и сертификатах.

Отдельный момент: TLS-ошибка — это не HTTP-ошибка. Сервер может быть жив, порт 443 открыт, Nginx запущен, но браузер всё равно не покажет страницу, потому что защищённое соединение не собрано.

Как выглядит ошибка в браузере, curl и логах

Формулировки зависят от клиента, библиотеки и ОС. Один и тот же сбой может называться по-разному.

1) В браузерах — Chrome часто показывает ERR_SSL_PROTOCOL_ERROR, ERR_CERT_COMMON_NAME_INVALID, ERR_CERT_DATE_INVALID, ERR_SSL_VERSION_OR_CIPHER_MISMATCH. Firefox — SSL_ERROR_HANDSHAKE_FAILURE_ALERT, SEC_ERROR_EXPIRED_CERTIFICATE, SSL_ERROR_UNSUPPORTED_VERSION. Safari может ограничиться сообщением о невозможности установить безопасное соединение.

2) В curl — ошибки обычно полезнее, потому что содержат код библиотеки TLS:

curl -Iv https://example.com

Типичные варианты:

  • SSL certificate problem: certificate has expired;
  • SSL: no alternative certificate subject name matches target host name;
  • OpenSSL SSL_connect: SSL_ERROR_SYSCALL;
  • error:0A00010B:SSL routines::wrong version number;
  • gnutls_handshake() failed;
  • tlsv1 alert protocol version.

Если вы редко используете этот инструмент, пригодится отдельный разбор: Что такое curl и как им пользоваться.

3) В логах Nginx или балансировщика — можно увидеть SSL_do_handshake() failed, no shared cipher, unsupported protocol, client sent plain HTTP request to HTTPS port, peer closed connection in SSL handshake.

4) В приложениях — Node.js может писать EPROTO, Java — javax.net.ssl.SSLHandshakeException, Go — remote error: tls: handshake failure, Python — ssl.SSLError.

Формулировка важна, но не стоит диагностировать только по ней. Например, handshake failure может быть вызван и шифрами, и сертификатом, и тем, что на 443 фактически отвечает не TLS-сервис.

Быстрая диагностика: с чего начать

Сначала нужно понять, где именно ломается соединение: DNS, TCP, TLS, сертификат, прокси или конкретный клиент.

1) Проверьте, что домен резолвится туда, куда нужно — особенно после переезда, смены CDN или балансировщика:

dig example.com A
dig example.com AAAA
dig www.example.com A

Если IPv4 и IPv6 ведут на разные инфраструктуры, ошибка может проявляться только у части пользователей. Частый сценарий: A-запись указывает на новый сервер с корректным сертификатом, а AAAA — на старый сервер с просроченным.

2) Проверьте TCP-доступность порта 443 — TLS не начнётся, если соединение не устанавливается:

nc -vz example.com 443

Если TCP не проходит, причина не в сертификате, а в маршрутизации, фаерволе, security group, балансировщике или недоступном сервере.

3) Посмотрите сертификат и цепочку через OpenSSL:

openssl s_client -connect example.com:443 -servername example.com -showcerts

Ключевой параметр здесь — -servername. Он включает SNI. Без него сервер может отдать сертификат по умолчанию, и диагностика будет ложной.

Смотрите на строки:

  • Verify return code: 0 (ok) — цепочка корректна;
  • certificate has expired — сертификат истёк;
  • unable to get local issuer certificate — неполная цепочка;
  • self signed certificate — клиент не доверяет сертификату;
  • no peer certificate available — сервер не отдал сертификат.

4) Сравните проверку с разных клиентов — браузер, curl, серверный curl, мобильная сеть, внешний мониторинг. Если ошибка есть только из одной сети, вероятны DPI, корпоративный прокси, фаервол или IPv6-маршрут.

5) Проверьте логи в момент ошибки — у Nginx это обычно /var/log/nginx/error.log, у HAProxy — системный журнал или отдельный лог, у Kubernetes ingress — логи pod’ов ingress-контроллера.

Если сайт важен для бизнеса, такие сбои лучше замечать раньше, чем от пользователей начнут поступать жалобы. Мониторинг с HTTPS-проверкой, например Statuser, регулярно опрашивает сайт и уведомляет, если рукопожатие начинает падать.

Сертификат: срок, домен, цепочка и доверие

Ошибки сертификата — самая понятная, но не единственная причина TLS-сбоев.

1) Истёк срок действия сертификата — браузеры сразу блокируют соединение. Проверяется командой:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates

Если используете Let’s Encrypt, проверьте автообновление, certbot renew, таймеры systemd и перезагрузку Nginx после выпуска нового сертификата. Подробная инструкция есть в статье Как настроить автоматическое обновление сертификатов Let’s Encrypt с помощью certbot.

2) Сертификат выпущен не на тот домен — например, сертификат покрывает example.com, но не покрывает www.example.com, api.example.com или кириллический домен в punycode. Проверяйте Subject Alternative Name, а не только CN:

openssl x509 -in cert.pem -noout -text | grep -A1 "Subject Alternative Name"

Современные клиенты ориентируются именно на SAN.

3) Неполная цепочка сертификатов — сервер отдаёт только leaf-сертификат, но не intermediate. У части клиентов всё может работать за счёт кэша промежуточных сертификатов, у других — падать. В Nginx обычно нужно указывать fullchain.pem, а не cert.pem.

Пример корректной настройки:

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

4) Самоподписанный сертификат в продакшене — допустим для локальной разработки, внутренних стендов или закрытых контуров, но публичные браузеры ему не доверяют. Если самоподписанный сертификат нужен осознанно, клиентам придётся добавить ваш CA в доверенные.

5) Неверное системное время — если часы на клиенте или сервере сильно отличаются, сертификат может считаться ещё не действующим или уже истёкшим. На серверах проверьте timedatectl, chrony, systemd-timesyncd.

Для публичных сайтов полезно отдельно следить за сроками сертификатов. Почему это стоит вынести в мониторинг, разобрано в статье Почему важен мониторинг SSL-сертификатов.

SNI, ALPN и виртуальные хосты

На одном IP-адресе часто живут десятки HTTPS-сайтов. Чтобы сервер понял, какой сертификат отдать, клиент передаёт имя домена через расширение SNI.

1) Клиент не отправляет SNI — старые клиенты, старые библиотеки, некоторые встроенные устройства и неправильно написанные health checks могут подключаться к IP:443 без имени домена. Сервер в таком случае отдаёт сертификат виртуального хоста по умолчанию. Для пользователя это выглядит как ошибка имени сертификата.

Проверка с SNI:

openssl s_client -connect 203.0.113.10:443 -servername example.com

Проверка без SNI:

openssl s_client -connect 203.0.113.10:443

Если результаты отличаются, проблема связана с выбором виртуального хоста.

2) Неправильный server_name в Nginx — например, настроен только example.com, а пользователи заходят на www.example.com. В таком случае запрос может попадать в дефолтный серверный блок.

Минимальный пример:

server {
    listen 443 ssl;
    http2 on;
    server_name example.com www.example.com;
 
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

Базовая настройка HTTPS в Nginx подробно разобрана здесь: Как настроить HTTPS и бесплатный SSL-сертификат в Nginx.

3) Ошибки ALPN — через ALPN клиент и сервер договариваются, использовать HTTP/2, HTTP/1.1 или другой протокол. Сам по себе неверный ALPN редко ломает TLS полностью, но может вызывать проблемы на связке CDN → балансировщик → backend, особенно если один слой ожидает h2, а другой работает только с http/1.1.

4) Балансировщик терминирует TLS не там, где вы думаете — сертификат может быть корректным на Nginx, но трафик до него не доходит: TLS завершается на CDN, ingress, HAProxy, cloud load balancer или API Gateway. Проверять нужно тот слой, который реально принимает HTTPS от клиента.

Если хотите глубже разобраться, как браузер выбирает сертификат и протокол, см. статью SNI и ALPN: как браузер выбирает сертификат и протокол.

Версии TLS и наборы шифров

TLS-рукопожатие требует совместимости. Клиент предлагает версии и шифры, сервер выбирает подходящие. Если пересечения нет, соединение падает.

1) Сервер поддерживает только старые версии TLSTLS 1.0 и TLS 1.1 давно считаются устаревшими. Современные браузеры и многие API-клиенты их блокируют. Если на старом сервере включён только TLS 1.0, пользователь увидит ошибку протокола.

2) Сервер отключил всё, кроме слишком нового набора — обратная ситуация: включили только TLS 1.3, а часть клиентов работает на старых ОС, Java, OpenSSL или embedded-устройствах. Для публичного сайта обычно разумный минимум — TLS 1.2 и TLS 1.3.

Пример для Nginx:

ssl_protocols TLSv1.2 TLSv1.3;

3) Нет общих cipher suites — в логах Nginx это часто выглядит как no shared cipher. Причина может быть в слишком жёсткой настройке ssl_ciphers или старом клиенте.

Проверить конкретную версию TLS можно так:

openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -tls1_3

4) Старые клиенты не поддерживают ECDSA-сертификаты — если сайт использует только ECDSA-сертификат, очень старые клиенты могут не подключиться. Для массовой совместимости иногда используют RSA + ECDSA одновременно, если это поддерживает сервер и инфраструктура.

5) FIPS, корпоративные политики и кастомные trust store — в enterprise-средах клиенты могут запрещать часть алгоритмов или доверять только внутренним CA. Тогда сайт работает у обычных пользователей, но падает у клиентов из корпоративной сети.

Не стоит «чинить» это включением всего подряд. Возвращать TLS 1.0, слабые шифры и небезопасные алгоритмы ради одного клиента — плохой компромисс. Лучше выяснить, какой клиент подключается, можно ли обновить его библиотеку TLS и какие требования у вашей аудитории.

Сетевые и инфраструктурные причины

Иногда сертификат и TLS-настройки корректны, но рукопожатие всё равно обрывается. Тогда нужно смотреть сеть и промежуточные слои.

1) На порту 443 отвечает не HTTPS — классика после неправильного деплоя или изменения port mapping в Docker. Клиент начинает TLS, а сервер отвечает обычным HTTP или вообще другим протоколом. В curl может появиться wrong version number.

Проверьте, кто слушает порт:

ss -ltnp | grep ':443'

2) Фаервол или security group пропускает TCP, но режет часть соединений — соединение может устанавливаться, но сбрасываться во время обмена. Ищите RST, дропы, правила DPI, лимиты state table.

3) Проблемы MTU и фрагментации — TLS-пакеты с сертификатом могут быть крупнее, чем первые TCP-пакеты. Если где-то по пути сломана Path MTU Discovery, маленькие проверки проходят, а рукопожатие зависает или обрывается. Это чаще проявляется через VPN, GRE/IPsec-туннели, облачные сети, Kubernetes overlay.

4) Перегружен балансировщик или Nginx — при нехватке worker’ов, файловых дескрипторов, CPU или переполнении очередей соединения могут закрываться до завершения TLS. В логах это выглядит не как «красивый» отказ сертификата, а как таймауты, connection reset, SSL_ERROR_SYSCALL.

5) CDN или WAF меняет поведение — у CDN обычно есть два TLS-сегмента: клиент → CDN и CDN → origin. Ошибка может быть на любом из них. Например, браузер успешно подключается к CDN, но CDN не может установить TLS к вашему origin из-за просроченного сертификата или несовпадения имени.

6) IPv6 настроен хуже IPv4 — пользователи с IPv6 получают один результат, остальные — другой. Проверяйте отдельно:

curl -4 -Iv https://example.com
curl -6 -Iv https://example.com

Если -4 работает, а -6 нет, ищите проблему в AAAA-записи, IPv6-маршруте, security group или сертификате на другом endpoint.

Как исправить ошибку TLS-рукопожатия: практический чеклист

Лучше идти от простого к сложному и фиксировать результат каждой проверки.

1) Убедитесь, что проверяете правильный доменexample.com, www.example.com, api.example.com и wildcard-сертификаты — разные вещи. Проверьте также редиректы: пользователь может начинать с одного имени, а после 301 попадать на другое.

2) Проверьте DNS и IP-адреса — сравните A, AAAA, записи CDN и балансировщика. Если недавно был переезд, часть пользователей может ходить на старый адрес из-за DNS-кэша.

3) Проверьте сертификат через openssl s_client с -servername — это быстро покажет срок, цепочку и имя сертификата. Если Verify return code не 0, начните с сертификатов.

4) Исправьте цепочку — для Nginx используйте fullchain.pem, для HAProxy — PEM-файл, где сертификат и цепочка собраны корректно. После замены перезагрузите сервис:

nginx -t && systemctl reload nginx

5) Проверьте виртуальные хосты и SNI — убедитесь, что нужный домен есть в server_name, а дефолтный server не отдаёт чужой сертификат. Для нескольких сайтов на одном IP это критично.

6) Настройте совместимые версии TLS — включите TLS 1.2 и TLS 1.3, если нет специальных ограничений. Не включайте устаревшие протоколы без крайней необходимости.

7) Проверьте CDN, WAF и балансировщик — где именно терминируется TLS? Какой сертификат установлен на публичном endpoint? Как CDN проверяет origin: по IP, по hostname, с каким SNI?

8) Сравните IPv4 и IPv6 — отдельная проверка curl -4 и curl -6 часто экономит часы.

9) Посмотрите логи в момент проверки — если клиент видит handshake failure, а на сервере нет записей, запрос может не доходить до нужного сервиса. Если записи есть, они обычно подскажут: no shared cipher, bad key share, unknown ca, unsupported protocol.

10) Проверьте после исправления из внешней сети — локальная проверка с самого сервера не заменяет проверку снаружи. Используйте другой провайдер, мобильный интернет или внешний мониторинг вроде Statuser, чтобы убедиться, что HTTPS-проверка проходит и у реальных пользователей.

Как предотвратить повторение проблемы

TLS-сбои часто появляются не из-за сложной криптографии, а из-за операционных мелочей: забыли обновить сертификат, поменяли ingress, добавили AAAA, включили CDN, не проверили www.

1) Автоматизируйте выпуск и обновление сертификатов — ручное продление рано или поздно приводит к простою. Для Let’s Encrypt проверьте, что автообновление не только выпускает сертификат, но и перезагружает сервис, который его использует.

2) Держите TLS-настройки в конфигурации и проверяйте измененияssl_protocols, ssl_ciphers, server_name, конфиги ingress и балансировщиков должны проходить review так же, как код приложения.

3) Мониторьте не только HTTP-статус, но и срок сертификата — отдельная проверка сертификата и TLS-доступности сигнализирует о проблеме раньше, чем обычный GET /.

4) Проверяйте все публичные имена — основной домен, www, API, админку, статические поддомены, wildcard и не-wildcard варианты. Ошибка часто живёт не на главной странице, а на вторичном поддомене.

5) Не забывайте про IPv6 — если публикуете AAAA-запись, она должна вести на такой же рабочий endpoint, как IPv4: с тем же сертификатом, firewall и маршрутизацией.

6) Тестируйте изменения снаружи перед выкладкой — особенно при замене CDN, ingress-контроллера, HAProxy, Nginx-конфигов или сертификатов. curl localhost не проверяет реальный путь клиент → интернет → CDN → балансировщик → origin.

TLS — часть доступности сайта, а не только безопасности. Если рукопожатие не проходит, пользователь не увидит даже страницу ошибки приложения. Поэтому TLS-проверки стоит включать в общий контур мониторинга вместе с HTTP-статусами, временем ответа и алертами.

FAQ

Что значит ошибка TLS-рукопожатия простыми словами?

Клиент и сервер не смогли договориться о защищённом HTTPS-соединении. Причина может быть в сертификате, версиях TLS, шифрах, SNI, прокси, CDN или сети.

Почему сайт открывается у меня, но не открывается у других?

Частые причины: разные DNS-кэши, IPv4/IPv6, корпоративный прокси, старый браузер, старый trust store, CDN с региональными endpoint’ами или неполная цепочка сертификатов.

Можно ли исправить ошибку, просто перевыпустив сертификат?

Иногда да, если сертификат истёк, выпущен не на тот домен или собран без intermediate-цепочки. Но если проблема в SNI, шифрах, TLS-версии или балансировщике, перевыпуск не поможет.

Почему openssl s_client показывает один сертификат, а браузер — другой?

Скорее всего, проверка выполнялась без SNI или через другой IP-адрес. Используйте -servername example.com и отдельно проверяйте A и AAAA-записи.

Опубликовано 10 августа 20269 минут чтенияДенис Коршунов
Средний рейтинг статьи — 4.8

Настроить мониторинг за 30 секунд

Надежные оповещения о даунтаймах. Без ложных срабатываний