Сети и протоколы
Защита 0-RTT, ECH, гибридный PQ и продакшн TLS
FinTech-сервис, обрабатывающий 50 тысяч TLS-рукопожатий в секунду, не может позволить себе наивно включённый 0-RTT, статический ключ сессионного тикета или чисто классический обмен ключами, который будущий квантовый компьютер способен ретроспективно взломать. Этот урок — о том, как продакшн-развёртывания защищаются от повтора, ротируют STEK, скрывают SNI через ECH, добавляют постквантовую защиту и наблюдают за TLS в масштабе.
Защита от повтора 0-RTT в продакшне (RFC 8446 §8 + RFC 8470)
Три ортогональных защиты применяются в совокупности:
1. Валидация возраста тикета. Клиент отправляет obfuscated_ticket_age; сервер вычитает ticket_age_add, учитывает ожидаемый RTT и отклоняет early_data, если результат выходит за пределы узкого окна (типично: 10 с). Злоумышленник, воспроизводящий данные часы спустя, получает немедленный отказ.
2. Однократное использование тикета. Кэш в памяти с ключом (ticket_nonce, age_bucket) отклоняет любую вторую попытку расшифровки. Cloudflare ведёт кэши на каждой точке присутствия плюс best-effort кросс-PoP синхронизацию, чтобы повторы между регионами также перехватывались в пределах окна.
3. Идемпотентность на уровне приложения. Только безопасные методы проходят в early_data. Фреймворки (Spring, Express, Caddy) проверяют заголовок запроса Early-Data: 1 и возвращают 425 Too Early для любого обработчика, изменяющего состояние.
Ротация ключа шифрования сессионных тикетов (STEK)
Без прямой секретности при возобновлении утечка STEK позволяет злоумышленнику расшифровать все 0-RTT-данные из тикетов, выданных этим ключом. Правила для продакшна:
- Ротируйте не реже раза в час (Cloudflare ротирует ежечасно; операторы Nginx должны перезагружаться с новым
ssl_session_ticket_keyв начале очереди). - Держите кольцевой буфер из прежних ключей только для расшифровки (обычно 24 ключа), чтобы тикеты в пути продолжали возобновляться.
- Никогда не сохраняйте STEK на диск. Хранение только в памяти — обязательное требование.
- Распространяйте новые ключи на все бэкенды через аутентифицированное распределение до истечения старого ключа, чтобы кросс-узловые тикеты оставались возобновляемыми.
Режимы отказа: ключи, сохранённые на диск и восстановленные после компрометации узла (инцидент GoDaddy 2023); статический STEK, настроенный на год, превращающий возобновление в монокультуру одного ключа.
Encrypted ClientHello (ECH, RFC 9849)
Обычный ClientHello в TLS 1.3 раскрывает SNI, ALPN, поддерживаемые группы и алгоритмы подписи — достаточно для идентификации целевого имени хоста и стека клиента. ECH (RFC 9849, опубликован в марте 2026) определяет:
- Внешний ClientHello, отправляемый на публичное имя хоста фронтенда.
- Внутренний ClientHello, зашифрованный через HPKE с использованием опубликованного ECH-ключа источника (полученного из DNS-записи
HTTPS/SVCB, RFC 9460).
Клиент получает ECH-ключ до TCP-соединения. Если он недоступен, клиент может использовать GREASE (отправить фиктивный ECH) или откатиться к обычному SNI. Chrome поставляет ECH по умолчанию начиная с Chrome 117+; Firefox включил его с версии 119; поддержка Safari всё ещё в разработке по состоянию на май 2026 года.
Операторы должны регулярно ротировать публичный ECH-ключ (TTL DNS — верхняя граница компрометации ключа) и принять, что внешний SNI по-прежнему называет общий фронтенд.
Гибридный постквантовый обмен ключами
draft-ietf-tls-ecdhe-mlkem (февраль 2026) стандартизирует X25519MLKEM768 (кодовая точка именованной группы 0x11EC). ML-KEM — это NIST FIPS 203 (стандартизирован в августе 2024). Гибридный общий секрет:
combined_secret = concat(ecdhe_secret, mlkem_secret)Это поступает в HKDF-Extract там, где раньше находился классический секрет ECDHE — расписание ключей ниже не изменяется. Гибрид безопасен, если держится хотя бы один примитив: квантовый компьютер, взломавший ECDHE, не сможет восстановить сессию, если ML-KEM устоит.
Стоимость на уровне протокола: публичные ключи ML-KEM-768 занимают 1184 байта, шифртексты — 1088 байт, что вынуждает ClientHello превысить 1500 байт и требует TCP-сегментации. Chrome 131+ (ноябрь 2024) поставляет гибридный PQ по умолчанию. По состоянию на Q1 2026 более трети трафика Cloudflare использует гибридные PQ-рукопожатия.
- Публичный ключ X25519
- 32 байта
- Публичный ключ ML-KEM-768
- 1 184 байта
- Шифртекст ML-KEM-768
- 1 088 байт
- Размер гибридного ClientHello
- >1 500 байт (TCP-сегментация)
- Chrome по умолчанию с
- Chrome 131 (ноябрь 2024)
- Трафик Cloudflare на гибридном PQ (Q1 2026)
- >33%
Ядерный TLS (kTLS) — выгрузка в ядро
setsockopt(fd, SOL_TLS, TLS_TX, &crypto_info) передаёт симметричный уровень записи ядру после завершения рукопожатия в пользовательском пространстве (Linux 4.13+, OpenSSL 3.2+). Современные ядра плюс поддерживаемый сетевой адаптер могут строить зашифрованные записи прямо на проводе с нулевой CPU-стоимостью на запись.
Netflix и Cloudflare приписывают экономию 8–29% CPU при обслуживании статических файлов использованию sendfile() поверх kTLS: файл перемещается из кэша страниц на сетевой адаптер, не попадая в пользовательское пространство. Поддерживаемые алгоритмы: AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 — только TLS 1.3.
Вмешательство промежуточных узлов и GREASE (RFC 8701)
TLS 1.3 помещает 0x0303 (TLS 1.2) в поле legacy_version и отправляет фиктивную запись ChangeCipherSpec, поскольку слишком много промежуточных узлов прерывают соединения, байты которых не соответствуют шаблонам TLS 1.2. GREASE (RFC 8701) рассыпает зарезервированные кодовые точки (0x0A0A, 0x1A1A, …, 0xFAFA) по спискам расширений и наборам шифров. Корректный партнёр игнорирует неизвестные значения; хрупкий промежуточный узел ломается немедленно, обнажая ошибку перед поставщиком вместо того, чтобы дать ей окаменеть в протоколе.
Наблюдаемость в продакшне
Минимально жизнеспособный набор метрик Prometheus для TLS-сервиса:
tls_handshake_duration_seconds_bucket— гистограмма; p95 выше 200 мс на CDN-источнике — это регрессия.tls_resumption_total{kind="psk|ticket|none"}— резкое падение доли возобновлений обычно означает ротацию STEK без окна перекрытия.tls_version_total{version="1.2|1.3"}— помечайте любой трафик на 1.2.tls_cipher_suite_total— выявляйте нежелательные наборы шифров.tls_early_data_total{outcome="accepted|rejected"}— всплеск отклонений early_data нередко предшествует сообщениям клиентов о дублирующихся POST.tls_ocsp_staple_total{outcome="ok|expired|missing"}— отсутствие прикрепления на сертификате с must-staple вызовет отказы соединений.
Реальные производственные инциденты
- Истечение цепочки Let’s Encrypt 2021: срок действия DST Root CA X3 истёк; Android < 7.1.1 не прошёл верификацию, сломав длинный хвост устройств.
- Утечка STEK GoDaddy 2023: утечка STEK в неправильно настроенной резервной копии клиента позволила злоумышленнику расшифровать месяцы кэшированных данных 0-RTT — именно тот режим отказа, о котором предупреждает RFC 8446 §2.3.
- Heartbleed (CVE-2014-0160): отсутствующая проверка длины в расширении heartbeat OpenSSL позволяла злоумышленникам читать 64 КБ памяти процесса, включая приватные ключи. Постмортем изменил экосистему (BoringSSL, rustls) и является причиной, по которой kTLS ограничивает поверхность ядра только шифрованием уровня записи.
Гибридное рукопожатие ML-KEM-768 + x25519 (Chrome 131+)
Ошибка рукопожатия OpenSSL — определите причину.
% openssl s_client -connect api.example.internal:443 -tls1_3 -showcerts
CONNECTED(00000005)
depth=0 CN = api.example.internal
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = api.example.internal
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
0 s:CN = api.example.internal
i:CN = Internal Corp Issuing CA
---
SSL handshake has read 1845 bytes and written 308 bytes
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server certificate
subject=CN = api.example.internal
issuer=CN = Internal Corp Issuing CA
---
SSL-Session:
Protocol : TLSv1.3
Verify return code: 21 (unable to verify the first certificate) Рукопожатие завершается, но verify return code = 21. В чём неправильная конфигурация и что сервер должен отправлять для исправления?
Почему утечка STEK позволяет расшифровывать данные 0-RTT, но не обычные рукопожатия TLS 1.3?
Почему это работает
mTLS в масштабах сервисной сетки. Istio + SPIFFE/SPIRE выдают свежие листовые сертификаты на каждую рабочую нагрузку (TTL нередко менее 24 часов), ротируя через API рабочей нагрузки SPIRE. При 10 000 подов с 24-часовой ротацией это примерно 7000 выдач сертификатов в минуту. CA уровня управления становится узким местом; иерархическая конструкция CA (корневой → промежуточный на кластер) держит подписание листов внутри кластера и делает его быстрым. Плоскость данных (Envoy) завершает и переинициирует TLS на каждом переходе — ваш бюджет сквозной задержки оплачивает рукопожатие TLS 1.3 на каждой границе сервиса, если только пулы соединений не амортизируют стоимость на тысячи запросов.
- 01Назовите три ортогональные защиты от повтора 0-RTT и что каждая из них перехватывает.
- 02Почему гибридный PQ конкатенирует секреты ECDHE и ML-KEM, а не использует только один из них?
- 03Какая продакшн-метрика наиболее прямо сигнализирует об ошибке ротации STEK?
Безопасность 0-RTT в продакшне требует трёх послойных защит: окно повтора по возрасту тикета, кэш одноразовых nonce на точку присутствия и 425 Too Early на уровне приложения для неидемпотентных маршрутов. Ключи шифрования сессионных тикетов должны ротироваться не реже раза в час, никогда не касаться диска и распределяться на все бэкенды до истечения старого ключа. Encrypted ClientHello (RFC 9849) скрывает целевой SNI от наблюдателей на пути; гибридный PQ (X25519MLKEM768) защищает от квантовых атак «записывай сейчас — расшифровывай потом» ценой увеличения ClientHello. Ядерный TLS выгружает симметричный уровень записи на сетевой адаптер, давая 8–29% экономии CPU при обслуживании статических файлов. Минимально жизнеспособный набор метрик отслеживает p95 продолжительности рукопожатия, долю возобновлений, исходы early_data и распределение наборов шифров — падение доли возобновлений является самым быстрым сигналом об ошибке ротации STEK.
встречается в47
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Hidden classes, деревья переходов и расположение в памятиmiddle
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Что такое воркеры и зачем они нужныjunior
- Механика web workers: dedicated, shared и OffscreenCanvasmiddle
- Structured clone и transferablesmiddle
- SharedArrayBuffer, Atomics и cross-origin isolationsenior
- Пулы воркеров, Comlink и наблюдаемость в продакшенеsenior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- JSONB, массивы и когда side table побеждаетmiddle
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Где происходит data fetching — и почему это решает LCPjunior
- React Server Components и Suspense streamingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Что такое OpenTelemetry: API, SDK, Collector, OTLPjunior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- Что такое trace propagation и почему сломанная propagation хуже отсутствия трейсовjunior
- traceparent и tracestate: полный формат W3C-заголовкаmiddle
- Baggage и async-границы: перенос контекста через очереди и callback''''иmiddle
- Async context на разных языках, service mesh, миграция B3 и безопасностьsenior
- Production-сбои propagation, span links и платформенный дизайнsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior