Боль, которую решаем: ты копируешь чужой ssl-блок из интернета, он "работает", но половина директив либо устарела, либо вообще не применяется к твоему трафику. Разберёмся, что на самом деле делает каждая строчка.
Что такое терминация TLS и как nginx ssl вообще работает
Терминация TLS - это когда nginx сам расшифровывает входящее TLS-соединение и дальше к бэкенду ходит уже по обычному http (или по отдельному внутреннему TLS). Клиент устанавливает шифрованный канал именно до nginx. Это и есть классическая роль reverse-proxy: один узел держит сертификаты, терминирует криптографию и снимает эту нагрузку с приложений.
Чтобы nginx https заработал, нужны три кита: слушающий сокет на 443 с признаком ssl, сертификат и приватный ключ. Минимум выглядит так:
Код: Выделить всё
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name cyberlake.ru;
ssl_certificate /etc/nginx/ssl/cyberlake.ru/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/cyberlake.ru/privkey.pem;
location / {
proxy_pass http://backend_upstream;
}
}

Цепочка сертификата: главная ловушка nginx сертификат
Самая частая ошибка новичка - подсунуть в ssl_certificate только сам листовой сертификат домена. В Chrome всё зелёное (у него свой кэш промежуточных), а на curl, мобильном приложении или старом Android вылезает "unable to get local issuer certificate".
Причина: клиент должен построить цепочку доверия от твоего сертификата до корневого CA. Промежуточные сертификаты CA в эту цепочку должен подложить сервер. Поэтому в ssl_certificate всегда подаётся fullchain - листовой сертификат плюс промежуточные, склеенные в один PEM-файл, именно в таком порядке (свой - первым, корневой не нужен):
Код: Выделить всё
ssl_certificate /etc/nginx/ssl/cyberlake.ru/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/cyberlake.ru/privkey.pem;
Код: Выделить всё
openssl s_client -connect cyberlake.ru:443 -servername cyberlake.ru < /dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
nginx ssl_protocols: только TLS 1.2 и TLS 1.3
В 2026 году живых протокола ровно два. TLS 1.0 и 1.1 признаны небезопасными давно (RC4, слабый MAC, отсутствие AEAD), а в nginx они по умолчанию выключены с версии 1.27. Но не полагайся на дефолт - пиши явно, чтобы конфиг был самодокументированным:
Код: Выделить всё
ssl_protocols TLSv1.2 TLSv1.3;
Шифры в 2026: почему ssl_ciphers НЕ применяется к TLS 1.3
Вот фундаментальный момент, который ломает интуицию. Директива ssl_ciphers управляет только шифрами TLS 1.2 и ниже. К TLS 1.3 она не применяется вообще - ни одной буквы из неё TLS 1.3 не читает.
Почему так. В TLS 1.3 набор шифров жёстко зафиксирован стандартом - их всего пять, и все они AEAD: TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256 и пара с CCM. Договариваться о "плохих" шифрах там просто не из чего, поэтому отдельная гибкая строка как в TLS 1.2 не нужна. Технически nginx для ssl_ciphers зовёт openssl-функцию для TLS 1.2 (set_cipher_list), а ciphersuites TLS 1.3 настраиваются другой функцией.
Что это значит на практике. Твоя строка ssl_ciphers с длинным списком ECDHE-ECDSA-AES256-GCM... работает ровно для TLS 1.2. Для TLS 1.3 список фиксирован, и если ты хочешь, например, исключить CHACHA20 или поменять приоритет - это делается другой директивой:
Код: Выделить всё
# TLS 1.2 - сюда смотрит ssl_ciphers
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# TLS 1.3 - сюда ssl_ciphers НЕ смотрит, нужен ssl_conf_command
ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
ssl_prefer_server_ciphers и кривые ssl_ecdh_curve
ssl_prefer_server_ciphers on; в эпоху TLS 1.2 заставляло сервер навязывать свой порядок шифров клиенту - это защищало от клиента, который предлагает слабый шифр первым. В TLS 1.3 это снова неактуально (там сервер и так выбирает), но для TLS 1.2 директива остаётся полезной и стоит в global_ssl эталона.
Код: Выделить всё
ssl_prefer_server_ciphers on;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
Пост-квантовый обмен ключами - реальность 2026. Большие квантовые компьютеры угрожают именно обмену ключами: запиши шифрованный трафик сегодня, расшифруй через 10 лет, когда появится квантовый компьютер ("harvest now, decrypt later"). Ответ - гибридный обмен X25519MLKEM768: классический X25519 плюс пост-квантовый ML-KEM-768, оба дают вклад в общий секрет. Если квантовый компьютер сломает один - второй держит.
Код: Выделить всё
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
OCSP stapling - в 2026 это legacy для Let's Encrypt
Раньше всех учили: включай ssl_stapling, чтобы сервер сам прикладывал свежий ответ OCSP (статус отзыва сертификата) и клиенту не надо было ходить к CA. Звучало правильно.
В 2026 картина изменилась. Let's Encrypt убрал OCSP-URL из своих сертификатов 7 мая 2025 года и перешёл на CRL-схему. Для LE-сертификата ssl_stapling теперь сшивать нечего - в сертификате просто нет ссылки на OCSP-ответчик.
Код: Выделить всё
# Legacy: для Let's Encrypt в 2026 БЕСПОЛЕЗНО (OCSP-URL убран).
# Оставлять только если CA реально выдаёт OCSP (некоторые коммерческие).
# ssl_stapling on;
# ssl_stapling_verify on;
# resolver 1.1.1.1 8.8.8.8 valid=300s;
Сессии: переиспользование рукопожатия
TLS-рукопожатие стоит дорого (асимметричная криптография). Чтобы повторные соединения не платили эту цену, есть кэш сессий:
Код: Выделить всё
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 10m;
ssl_session_tickets off;
А вот ssl_session_tickets off; - осознанное решение, и оно стоит в эталоне. Тикеты позволяют возобновлять сессию без серверного состояния, но если не ротировать ключ тикетов, они ломают forward secrecy: украв один ключ, злоумышленник расшифрует весь записанный трафик. Без грамотной ротации ключей безопаснее тикеты выключить и полагаться на session cache. В TLS 1.3 механизм возобновления другой (PSK), но логика та же - не раздавай долгоживущие секреты бесконтрольно.
HSTS, ssl_dhparam и несколько сертификатов
HSTS. Заголовок Strict-Transport-Security говорит браузеру "ходи сюда только по https, http не пробуй вообще":
Код: Выделить всё
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
ssl_dhparam - частый карго-культ. Эта директива задаёт параметры Diffie-Hellman и нужна только для классических DHE-шифров (без EC). Если у тебя современный набор на ECDHE (а он у тебя такой) и TLS 1.3 с ECDHE - DHE не используется, и ssl_dhparam не нужен. Не копируй генерацию 4096-битного dhparam "потому что все так делают" - на чистом ECDHE/TLS 1.3 это мёртвая строка. Нужна она, только если ты осознанно держишь DHE-шифры для каких-то древних клиентов.
Несколько сертификатов - RSA и ECDSA. nginx умеет отдавать разные сертификаты на один server_name в зависимости от того, что поддерживает клиент. ECDSA быстрее и компактнее, но старый клиент может его не понять - тогда отдаём RSA:
Код: Выделить всё
ssl_certificate /etc/nginx/ssl/cyberlake.ru/ecdsa_fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/cyberlake.ru/ecdsa_privkey.pem;
ssl_certificate /etc/nginx/ssl/cyberlake.ru/rsa_fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/cyberlake.ru/rsa_privkey.pem;
Типичные грабли
- В ssl_certificate подложен только листовой сертификат без промежуточных. Chrome молчит, curl и мобилки ругаются. Всегда fullchain.
- Правишь ssl_ciphers и ждёшь, что изменится TLS 1.3. Не изменится - для 1.3 это ssl_conf_command Ciphersuites.
- Скопировал ssl_stapling из старого гайда на Let's Encrypt-сертификат. Бесполезно с мая 2025, OCSP-URL у LE нет.
- Сгенерил dhparam.pem на 4096 бит и подключил, хотя весь трафик на ECDHE. Мёртвая директива, форвард-секреси она тебе не добавит.
- Включил HSTS с includeSubDomains и preload, а поддомен blog. ещё на http. Браузеры на max-age отказываются туда ходить.
- Забыл http2 on; и удивляешься, почему мультиплексирование не работает. Параметр listen ... http2 устарел.
- Положил приватный ключ с правами 644. Любой пользователь системы его прочитает. chmod 600, владелец root.
Повтори по шагам на тестовом nginx (1.30+).
- Шаг 1. Получи сертификат LE: certbot certonly --webroot -w /var/www/html -d test.example.com. Найди в /etc/letsencrypt/live/test.example.com/ файлы fullchain.pem и privkey.pem.
- Шаг 2. Создай global_ssl.conf с ssl_protocols TLSv1.2 TLSv1.3; набором ssl_ciphers, ssl_prefer_server_ciphers on; ssl_ecdh_curve X25519:prime256v1; session_cache 50m, session_tickets off. Подключи его include в http-блок.
- Шаг 3. В server-блоке пропиши listen 443 ssl; listen [::]:443 ssl; http2 on; и обе директивы ssl_certificate/ssl_certificate_key на fullchain.
- Шаг 4. Проверь конфиг: nginx -t, затем nginx -s reload.
- Шаг 5. Локальная проверка цепочки: openssl s_client -connect test.example.com:443 -servername test.example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates. Убедись, что в "Certificate chain" есть и 0, и 1.
- Шаг 6. Проверь протокол: openssl s_client -connect test.example.com:443 -tls1_3 - должно подключиться. Затем -tls1_1 - должно отлететь (протокол выключен).
- Шаг 7. Прогони домен через SSL Labs (ssllabs.com/ssltest) и сверь конфиг с Mozilla SSL Configuration Generator (ssl-config.mozilla.org), профиль Intermediate. Это эталон, на который стоит равняться.
- Шаг 8 (продвинутый). Если OpenSSL 3.5+ - добавь ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1; перезагрузи и открой сайт в свежем Chrome: в DevTools -> Security увидишь согласованную пост-квантовую группу.
- Почему правка ssl_ciphers не влияет на соединения по TLS 1.3 и какой директивой настраиваются ciphersuites 1.3?
- Что должно лежать в файле, указанном в ssl_certificate, и почему Chrome может не заметить проблему, которую видит curl?
- Почему в 2026 году ssl_stapling для сертификата Let's Encrypt не имеет смысла?
- В каком случае ssl_dhparam реально нужен, а в каком это мёртвая строка в конфиге?
Рабочий nginx ssl в 2026 - это fullchain в ssl_certificate, ssl_protocols TLSv1.2 TLSv1.3, понимание что ssl_ciphers - только про TLS 1.2 (а 1.3 - это ssl_conf_command), выключенные тикеты, осознанный HSTS на уровне server, отсутствие культовых dhparam и мёртвого OCSP stapling для LE, и гибридный X25519MLKEM768, если OpenSSL это уже умеет. Не копируй чужие ssl-блоки вслепую: сверяйся с Mozilla SSL Config Generator и SSL Labs, и каждая строка должна делать ровно то, что ты от неё ждёшь.