Почему HTTPS «дорогой» только в первый раз: сессии и возобновление
Если вы смотрели профиль запроса в браузере и видели, что первое соединение к серверу занимает заметно больше времени, чем последующие — это не баг и не сеть барахлит. Полный TLS-хендшейк — дорогая операция с асимметричной криптографией, и сервер с браузером специально придумали способ его пропускать при повторных подключениях. Разберёмся, что именно там дорого, как работает возобновление сессии и как проверить, что оно у вас реально включено, а не просто числится в конфиге.
Содержание
Что именно дорого в полном хендшейке
Когда клиент впервые подключается к серверу по HTTPS, происходит TLS handshake — обмен сообщениями, в результате которого обе стороны получают общий симметричный ключ для шифрования трафика. Дорогая часть — не сам факт шифрования, а то, как этот общий ключ согласовывается.
Асимметричная криптография (RSA или, чаще сейчас, ECDHE) используется на этапе согласования ключа именно потому, что у клиента и сервера изначально нет общего секрета — только сертификат сервера с публичным ключом. Операции с приватными/публичными ключами вычислительно тяжелее операций с симметричным ключом на порядки — это устроено математически: модульное возведение в степень (RSA) или скалярное умножение на эллиптической кривой (ECDHE) требует значительно больше CPU-тактов, чем один раунд AES.
Кроме вычислительной цены есть ещё сетевая — round-trip'ы. Полный TLS 1.2 хендшейк — это минимум два круга (ClientHello → ServerHello+Certificate+ServerKeyExchange → ClientKeyExchange+Finished → Finished), TLS 1.3 сократил это до одного круга для полного хендшейка, но всё равно требует обмена и вычисления ключа с нуля. Если сервер физически далеко от клиента (высокий RTT), каждый лишний круг — это ощутимая добавка ко времени до первого байта.
Так что «дорого» — это сумма двух вещей: CPU на асимметричную операцию (умножается на число одновременных соединений на сервере) и лишние round-trip'ы на согласование. Session resumption решает и то, и другое одновременно.
Идея resumption: пропустить асимметрику, взять готовый секрет
Смысл возобновления сессии простой: если клиент и сервер уже когда-то договорились об общем секрете, зачем заново гонять дорогую асимметричную математику — можно взять производный от старого секрета симметричный ключ и подтвердить обеим сторонам, что они всё ещё его помнят. Это на порядок дешевле по CPU и требует меньше round-trip'ов.
Есть два классических механизма, которые решают эту задачу по-разному.
Session ID — сервер после первого хендшейка выдаёт клиенту идентификатор сессии и хранит у себя состояние сессии (мастер-секрет и параметры) в серверном кэше. При повторном подключении клиент присылает этот ID в ClientHello, сервер ищет его в своём кэше — если находит, оба пересчитывают ключи трафика из старого мастер-секрета без нового согласования асимметричным способом. Минус — серверу нужно где-то хранить состояние каждой сессии, и если у вас несколько бэкендов за балансировщиком без sticky-сессий или общего кэша, resumption по Session ID может не срабатывать: запрос попадёт на другой сервер, у которого этой сессии в памяти нет.
Session tickets (RFC 5077) — сервер вообще не хранит состояние сессии у себя. Вместо этого он шифрует всё нужное для восстановления сессии (мастер-секрет, параметры) собственным ключом и отдаёт клиенту как «тикет» — непрозрачный блоб. Клиент хранит тикет у себя и присылает его при следующем подключении в расширении ClientHello. Сервер расшифровывает тикет своим ключом, восстанавливает секрет — и снова обходится без полного пересчёта асимметрики. Это масштабируется лучше: серверу не важно, на какой инстанс из пула попадёт клиент, лишь бы у всех инстансов был один и тот же ключ шифрования тикетов (это отдельный момент для конфигурации, о нём — ниже).
В TLS 1.3 оба механизма объединены под общим термином PSK (pre-shared key) resumption — но суть та же: обе стороны предъявляют доказательство, что помнят общий секрет из прошлой сессии, и на основе него быстро выводят новые ключи трафика без ECDHE-обмена с нуля (если только сервер явно не потребует ECDHE поверх PSK для forward secrecy — это тоже настраиваемый режим).
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSTLS 1.3 0-RTT: ещё быстрее, но с ценой
TLS 1.3 пошёл дальше и добавил режим 0-RTT (early data) поверх resumption. Идея — если у клиента уже есть PSK от предыдущей сессии, он может отправить прикладные данные (например, HTTP-запрос) сразу в первом пакете, вообще не дожидаясь ответа сервера на ClientHello. Это убирает последний round-trip ожидания — для клиента с плохим RTT до сервера разница заметна на глаз.
Но у 0-RTT есть фундаментальная проблема: у него нет защиты от replay-атак. Обычный TLS-хендшейк защищён от повторного воспроизведения, потому что сервер участвует в согласовании свежего nonce на каждое соединение. В 0-RTT данные первого пакета зашифрованы ключом, который выводится только из PSK и известных заранее параметров — если атакующий перехватил этот пакет, он может отправить его серверу ещё раз, и сервер (если не защищаться отдельно) обработает его как второй легитимный запрос.
Для GET-запроса, который не меняет состояние, replay обычно не критичен — прочитали страницу дважды, ничего не сломалось. Но если в 0-RTT-данных прилетает что-то с побочным эффектом (списание, создание заказа, любой запрос, не идемпотентный по своей природе) — повторное воспроизведение может привести к двойному выполнению действия. Поэтому:
- 0-RTT в принципе не стоит доверять для запросов с побочными эффектами — приложение должно либо явно запрещать early data для таких путей, либо самостоятельно обеспечивать идемпотентность на уровне бизнес-логики.
- nginx по умолчанию поддерживает 0-RTT только если вы явно включили
ssl_early_data on;— по умолчанию выключено именно из-за этого риска. - Даже с включённым 0-RTT нужно понимать: это оптимизация для конкретных сценариев (статика, идемпотентные API), а не универсальный ускоритель для всего трафика.
Мой совет — не включать 0-RTT бездумно «потому что быстрее». Session resumption без 0-RTT уже даёт основной выигрыш (пропуск асимметрики), а 0-RTT добавляет к нему ещё один сэкономленный round-trip ценой реального риска, который надо явно продумать на уровне приложения.
Настройка resumption в nginx
На стороне сервера nginx поддерживает оба механизма — Session ID через ssl_session_cache и session tickets через ssl_session_tickets. Базовая конфигурация в server или http блоке:
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets on;
shared:SSL:10m — общий кэш сессий размером 10 МБ, доступный всем worker-процессам nginx (без shared каждый воркер держал бы свой кэш, и клиент с шансом 50/50 попадал бы на воркер без своей сессии). Ориентировочно 10 МБ кэша хватает на несколько десятков тысяч сессий — точное число зависит от размера сессионных данных, поэтому если у вас высоконагруженный сервер, стоит смотреть на реальную статистику попаданий, а не полагаться на прикидку.
ssl_session_timeout задаёт, сколько сервер готов помнить сессию для resumption. Здесь стоит взвесить trade-off: чем дольше живёт сессия, тем больше шанс, что вернувшийся клиент возобновит её без полного хендшейка — но тем дольше живёт и потенциально скомпрометированный секрет, если он утёк.
Если у вас несколько бэкендов nginx за балансировщиком (несколько инстансов, HA-пара), ssl_session_cache shared:SSL:10m не шарится между разными машинами сам по себе — это разделяемая память в пределах одного процесса nginx на одном хосте. Для resumption между разными инстансами нужны либо sticky-сессии на балансировщике (клиент всегда попадает на тот же бэкенд), либо переход на session tickets с одним и тем же ключом шифрования тикетов на всех инстансах — по умолчанию nginx генерирует ключ тикетов случайно при старте, и он разный на каждой машине, так что тикет, выданный одним инстансом, не расшифруется на другом. Ключ можно задать явно и синхронизировать между инстансами:
ssl_session_ticket_key /etc/nginx/ssl/ticket.key;
Файл ключа нужно сгенерировать и аккуратно скопировать на все бэкенды, а также ротировать по расписанию — это не входит в тему одной статьи, но важно понимать, что «просто включить tickets» на пуле серверов без общего ключа не даст эффекта.
Если вы разворачивали HTTPS через Let's Encrypt, стоит свериться с базовой настройкой SSL на VPS — resumption настраивается поверх уже работающего сертификата, а не вместо него.
Как проверить, что resumption реально работает
Конфиг конфигом, но нагляднее — проверить руками, что второе соединение действительно короче первого и переиспользует сессию. Самый простой инструмент — openssl s_client с флагом -reconnect:
openssl s_client -connect example.com:443 -reconnect
Эта команда устанавливает соединение несколько раз подряд в рамках одного вызова и в конце выводит, какие из подключений были resumed. Ищите в выводе строки вида:
---
Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
---
Слово Reused (в некоторых версиях openssl — New для первого подключения и Reused для последующих) в блоке SSL-Session говорит о том, что сессия была возобновлена, а не согласована с нуля. Если во всех последующих подключениях стоит New — resumption не срабатывает, и стоит проверить конфиг ssl_session_cache/ssl_session_tickets и, если у вас балансировщик — распределение по бэкендам.
Более прицельная проверка — вручную сохранить сессию и переиспользовать её отдельной командой:
openssl s_client -connect example.com:443 -sess_out session.pem
openssl s_client -connect example.com:443 -sess_in session.pem
Во втором вызове ищите ту же пометку Reused в выводе. Это удобно, когда нужно явно развести во времени первое и второе подключение — например, проверить, не истекает ли сессия раньше, чем указано в ssl_session_timeout.
Если нужно посмотреть на уровне браузера — в Chrome DevTools на вкладке Security можно увидеть параметры соединения для конкретного запроса, а в вкладке Network время, потраченное на этап SSL, обычно заметно меньше при повторном заходе на тот же домен в рамках короткого времени (единицы минут), пока браузер держит сессию в своём кэше. Здесь я намеренно не привожу конкретные цифры в миллисекундах — разница сильно зависит от RTT до сервера, выбранного шифра и загрузки CPU, и надёжнее смотреть у себя в конкретных условиях, чем ориентироваться на чужие замеры.
Где resumption не поможет и на что смотреть отдельно
Resumption экономит именно на криптографии и round-trip'ах TLS-уровня — он не решает более общих проблем производительности. Несколько нюансов, которые стоит держать в голове:
- Если клиент заходит на сайт впервые за долгое время (сессия истекла по
ssl_session_timeoutили тикет истёк), resumption не сработает — будет полный хендшейк заново. Это нормально, не повод увеличивать timeout до бесконечности (это снижает forward secrecy — компрометация одного секрета даёт доступ к более длинному окну трафика). - Resumption работает на уровне TLS-соединения, а не на уровне HTTP-запроса. Если вы используете HTTP/1.1 без keep-alive и открываете новое TCP+TLS соединение на каждый запрос, resumption всё равно спасает от полного хендшейка на каждом из них — но само по себе множество отдельных соединений на клиента остаётся неэффективным паттерном, который стоит решать через keep-alive или переход на HTTP/2 или HTTP/3 и QUIC, где мультиплексирование снимает эту проблему на другом уровне.
- Если у сервера включён HSTS и клиент уже видел заголовок, браузер вообще не будет пытаться подключиться по HTTP и сразу пойдёт на HTTPS — это отдельная оптимизация, не связанная с resumption напрямую, но часто обсуждается в той же теме «первое подключение дороже».
- Если после смены конфигурации
ssl_session_cacheсайт вдруг перестал открываться или сертификат выглядит некорректно — прежде чем разбираться с resumption, стоит проверить общий статус SSL на сервере, для этого пригодится разбор частых ошибок, когда nginx не применяет SSL.
Отдельно: resumption — это оптимизация именно клиент-серверного соединения. Если у вас nginx стоит как reverse proxy перед бэкендом, соединение nginx → бэкенд — это отдельный TLS-контекст (если оно вообще шифруется), и настройки resumption для клиентского соединения на него не распространяются автоматически. Если интересна эта тема шире — как строится проксирование в целом, есть отдельный разбор nginx как reverse proxy на VPS.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Resumption снижает безопасность?
Не принципиально, если ключи тикетов ротируются и timeout сессии разумный (часы, не недели). Риск смещается в сторону: если ключ шифрования тикетов утёк, все сессии, зашифрованные им, можно расшифровать задним числом — поэтому ротация ключа важна не меньше, чем сама настройка resumption.
Почему в выводе openssl s_client -reconnect иногда все подключения показывают New, хотя конфиг вроде правильный?
Частая причина — несколько worker-процессов nginx без shared в ssl_session_cache, либо балансировщик раскидывает подключения по разным бэкендам без общего ключа тикетов. Проверьте оба пункта конфига построчно.
0-RTT нужно включать всем сайтам ради скорости?
Нет — только там, где вы явно продумали идемпотентность запросов, которые могут прийти в early data. Для большинства сайтов достаточно обычного resumption без 0-RTT — основной выигрыш по CPU и round-trip'ам уже получен, а риск replay не добавляется.
Sessions ID или session tickets — что выбрать?
Session tickets обычно удобнее для нескольких бэкендов, потому что не требуют серверного хранилища состояния — только общий ключ шифрования. Session ID проще для одиночного сервера, где нет проблемы синхронизации между инстансами.
Как понять, что resumption вообще стоит настраивать именно у меня?
Если у вас много повторных визитов одних и тех же клиентов за короткое время (API с частыми запросами, SPA с поллингом, мобильное приложение) — выигрыш будет ощутимым. Для сайта с редкими одноразовыми посещениями эффект от resumption на общую картину заметно меньше.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →