RE:NODE

Ошибки SSL-сертификата: что означает каждая и как исправить

Автор Команда RE:NODE
16 мин чтения


Любое предупреждение браузера о сертификате - это одна из примерно шести проблем, и код ошибки подсказывает, какая именно. NET::ERR_CERT_DATE_INVALID означает, что сертификат просрочен или у посетителя неверные часы. NET::ERR_CERT_COMMON_NAME_INVALID означает, что сертификат выдан на другое имя. NET::ERR_CERT_AUTHORITY_INVALID означает, что браузер не может проследить сертификат до удостоверяющего центра, которому доверяет: это самоподписанный сертификат или сервер, который забыл отправить свой промежуточный сертификат. ERR_SSL_PROTOCOL_ERROR означает, что стороны вообще не дошли до проверки сертификата. А замок с надписью "Не защищено" при совершенно исправном сертификате - это смешанное содержимое (mixed content). Прочитайте код, выполните одну команду openssl с машины за пределами вашей сети - и исправление обычно сводится к одной строке конфигурации.

Эта статья - вторая половина темы, посвящённая диагностике. О том, как сертификаты выпускаются и продлеваются, рассказано в материале HTTPS и Let's Encrypt простыми словами, а о том, как направить домен на сервер, чтобы сертификат вообще можно было выпустить, - в материале как направить домен на сервер и добавить SSL.

Сначала прочитайте код ошибки#

Большая красная страница создана, чтобы отпугнуть посетителей, а не помочь вам, поэтому полезная часть на ней небольшая. В Chrome и Edge это серый код под пояснением или за кнопкой "Дополнительно". В Firefox он тоже спрятан за "Дополнительно" и записывается иначе. Safari в основном пишет "Это соединение не защищено" и прячет причину за "Показать подробности" и "просмотреть сертификат".

Chrome / EdgeFirefoxЧто это значит
NET::ERR_CERT_DATE_INVALIDSEC_ERROR_EXPIRED_CERTIFICATEПросрочен, ещё не действует или неверные часы
NET::ERR_CERT_COMMON_NAME_INVALIDSSL_ERROR_BAD_CERT_DOMAINВ сертификате нет этого имени
NET::ERR_CERT_AUTHORITY_INVALIDSEC_ERROR_UNKNOWN_ISSUERНедоверенный издатель или нет промежуточного сертификата
NET::ERR_CERT_AUTHORITY_INVALIDMOZILLA_PKIX_ERROR_SELF_SIGNED_CERTСамоподписанный сертификат
ERR_SSL_PROTOCOL_ERRORSSL_ERROR_RX_RECORD_TOO_LONGНа этом порту нет TLS, часто это обычный HTTP
ERR_SSL_VERSION_OR_CIPHER_MISMATCHSSL_ERROR_NO_CYPHER_OVERLAPНет общей версии TLS или шифра
NET::ERR_CERT_REVOKEDSEC_ERROR_REVOKED_CERTIFICATEУдостоверяющий центр отозвал сертификат

Пока вы на этой странице, обратите внимание ещё на два момента. Во-первых, есть ли ссылка "Всё равно перейти": если её нет, на сайте включён HSTS (или он находится в домене из предзагруженного списка HSTS, например в .dev или .app), и браузер не позволяет никому пройти дальше. Это HSTS делает свою работу, а не отдельная неисправность. Во-вторых, кто видит ошибку. Предупреждение, которое видит только один человек, почти всегда связано с его компьютером: часы, антивирус, корпоративный прокси, wifi в отеле. Предупреждение, которое видят все, - это сервер.

Проверка снаружи через openssl s_client#

У браузера есть кеши, сохранённые исключения и собственная копия промежуточных сертификатов, полученных где-то ещё. У openssl s_client ничего этого нет. Он подключается, проходит рукопожатие, выводит ровно то, что отправил сервер, и сообщает, проходит ли проверка. Запускайте его с машины, которая не находится в одной сети с сервером: локальная подмена DNS или запись в файле hosts с лёгкостью покажет вам не тот сервер, который видит остальной мир.

bash
$ openssl s_client -connect example.com:443 -servername example.com </dev/null

Параметр -servername важен. Он отправляет SNI - часть рукопожатия, которая сообщает серверу с множеством сайтов, какой сертификат вам нужен. Без него вы получите то, что сервер считает сертификатом по умолчанию, а это часто не тот сертификат, и вы начнёте искать проблему, которой нет. Свежие версии OpenSSL отправляют SNI автоматически по хосту из -connect, а старые - нет, поэтому указывайте параметр каждый раз. Часть </dev/null закрывает соединение сразу после рукопожатия, вместо того чтобы ждать, пока вы введёте HTTP-запрос.

Вывод длинный. Важны три части:

code
Certificate chain 0 s:CN = example.com   i:C = US, O = Let's Encrypt, CN = R11 1 s:C = US, O = Let's Encrypt, CN = R11   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1...Verify return code: 0 (ok)

Цепочка перечисляет то, что отправил сервер: 0 - это конечный сертификат (ваш), s: - субъект, i: - издатель. Издатель каждого сертификата должен совпадать с субъектом следующего, а заканчиваться цепочка должна тем, чему доверяет браузер. Названия промежуточных сертификатов со временем меняются, так что не беспокойтесь, если у вас не R11; беспокойтесь, если есть только строка 0. Последняя строка - вердикт. 0 (ok) означает, что цепочка проходит проверку по хранилищу доверия вашей системы. Всё остальное - это ошибка, описанная словами OpenSSL.

Чтобы увидеть даты и имена без лишнего шума, передайте сертификат в openssl x509:

bash
$ openssl s_client -connect example.com:443 -servername example.com </dev/null \    2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

Для -ext нужен OpenSSL 1.1.1 или новее. В более старой системе используйте -text и найдите блок X509v3 Subject Alternative Name глазами.

Коды проверки, которые вы действительно встретите:

КодСообщениеЗначение
10certificate has expiredПродление не удалось или не было настроено
18self-signed certificateКонечный сертификат подписан сам собой
19self-signed certificate in certificate chainЧастный CA, которому клиент не доверяет
20unable to get local issuer certificateЦепочка неполная или нет хранилища доверия
21unable to verify the first certificateСервер отправил конечный сертификат без промежуточного
62hostname mismatchИмени нет в сертификате (с -verify_hostname)

Учтите, что s_client по умолчанию не проверяет имя - он ответит 0 (ok) для исправного сертификата, принадлежащего совсем другому домену. Добавьте -verify_hostname example.com, чтобы он проверял имя, или сами прочитайте вывод subjectAltName.

NET::ERR_CERT_DATE_INVALID: сертификат просрочен или неверные часы#

У сертификата есть дата "не ранее" и дата "не позднее", и за пределами этого окна он недействителен. Выйти за него можно двумя совершенно разными способами.

Сертификат действительно просрочен. Если openssl x509 -dates показывает notAfter в прошлом, продление не удалось. Сертификаты Let's Encrypt живут 90 дней, значит, что-то незаметно ломалось около месяца: клиенты обычно продлевают за 30 дней до конца срока. Обычные причины те же, что и мешают выпуску с самого начала: домен больше не указывает на сервер, порт 80 закрыт после изменения firewall, перед исходным сервером включили CDN или таймер продления так и не установили, потому что первый сертификат получили вручную. Проверьте журнал продления - для Certbot это /var/log/letsencrypt/letsencrypt.log - и выполните certbot renew --dry-run, чтобы воспроизвести сбой по требованию.

Другая ловушка с просроченным сертификатом: продление прошло, но ничего не перезагрузилось. Certbot записывает новые файлы, а nginx и большинство других серверов читают сертификаты один раз при запуске и продолжают отдавать старый из памяти, пока их не перезагрузят. Если у файла на диске свежая дата, а s_client показывает старый сертификат, проблема именно в этом. Навсегда решает её deploy-хук:

bash
$ certbot renew --deploy-hook "systemctl reload nginx"

Более короткие сроки жизни сертификатов приходят во всей отрасли, а не только в Let's Encrypt: CA/Browser Forum согласовал постепенное снижение максимального срока в ближайшие годы, до 47 дней в 2029 году. Всё, что по-прежнему продлевает человек с напоминанием в календаре, будет ломаться с каждым годом чаще. Если какой-то сертификат в вашей системе продлевается вручную, автоматизируйте в первую очередь его.

У посетителя неверные часы. Если ошибку видит только один человек, а его браузер пишет что-то вроде "Ваши часы отстают", то сертификат в порядке, а компьютер считает, что сейчас другой год. Так бывает на машинах с севшей батарейкой материнской платы, в системах с двойной загрузкой, где Windows и Linux расходятся во мнении, хранятся ли аппаратные часы в местном времени или в UTC, и на виртуальных машинах, восстановленных из старого снимка. Исправление системного времени чинит сразу все сайты. Владелец сайта этого исправить не может и не должен пытаться.

Ещё не действителен. Реже, но бывает: у только что выпущенного сертификата notBefore на несколько секунд опережает часы сервера или клиента, которые уходят вперёд. Держите NTP запущенным на серверах - timedatectl должен сообщать System clock synchronized: yes.

NET::ERR_CERT_COMMON_NAME_INVALID: сертификат выдан на другое имя#

Имя из адресной строки должно присутствовать в списке Subject Alternative Name сертификата. Браузеры полностью игнорируют старое поле Common Name - Chrome перестал его читать в 2017 году, - поэтому сертификат, у которого имя есть только в CN, не проходит, хотя на первый взгляд всё правильно. В основном это касается самодельных сертификатов: любой публичный удостоверяющий центр записывает имена в список SAN.

Самые частые причины:

  • `www` и основной домен - это разные имена. Сертификат для example.com не покрывает www.example.com, и наоборот. Либо получите один сертификат с обоими именами, либо настройте перенаправление с одного на другое - при этом сам редирект тоже требует действительного сертификата на то имя, с которого он идёт, потому что браузер проверяет сертификат раньше, чем увидит редирект. О том, как выбрать основное имя, рассказано в материале www и основной домен: редиректы.
  • Wildcard покрывает ровно один уровень. *.example.com покрывает shop.example.com, но не example.com и не api.shop.example.com. Wildcard-сертификаты, которые "не работают", почти всегда просят покрыть одно из этих двух имён.
  • На общем сервере нет совпадения по SNI. Если для запрошенного имени на сервере не настроен сертификат, он отдаёт сертификат по умолчанию - а тот принадлежит какому-то другому сайту на этой же машине. В nginx это значит, что запрос попал в блок server, в server_name которого нет вашего хоста, поэтому ответил первый блок listen 443 ssl (или тот, что помечен как default_server). Проверьте server_name на опечатки и выполните nginx -T, чтобы увидеть конфигурацию, которую nginx действительно загрузил, а не файл, который вы, по вашему мнению, отредактировали. Рабочая схема есть в руководстве по reverse proxy на nginx.
  • DNS указывает не на ту машину. Вы перенесли сайт, старая A-запись всё ещё на месте или осталась в кеше, и посетители попадают на прежний хост, у которого сертификат совсем на другое имя. Сравните dig +short example.com @1.1.1.1 с ожидаемым адресом. О задействованных записях рассказано в материале DNS-записи простыми словами.
  • Заход по IP-адресу. https://203.0.113.10 никогда не совпадёт с сертификатом на домен. Используйте имя.

Неполная цепочка: в вашем браузере работает, везде остальном - нет#

Эта ошибка отнимает больше всего времени, потому что человек, настроивший сервер, не может её воспроизвести. Сертификат действителен, имя верное, даты в порядке, и сайт открывается в его браузере. А тем временем curl падает с curl: (60) SSL certificate problem: unable to get local issuer certificate, скрипт на Python выбрасывает CERTIFICATE_VERIFY_FAILED, вебхук платёжного провайдера отказывается доставляться, а на некоторых телефонах появляется предупреждение.

подписанподписандоверяетВаш сертификатотправляет серверБраузер или curlПромежуточныйтоже отправляетсяКорневой сертификатуже в хранилище
Браузер идёт от вашего сертификата к корневому, который у него уже есть

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

Десктопные браузеры часто скрывают эту ошибку. Chrome и другие сами докачают недостающий промежуточный сертификат по адресу, указанному внутри сертификата, или возьмут тот, что закеширован с другого сайта, который отправил его правильно. Инструменты командной строки, HTTP-библиотеки языков программирования и многие другие клиенты так не делают и падают. s_client показывает это сразу: в цепочке только строка 0 и Verify return code: 21 (unable to verify the first certificate).

Исправление - отдавать файл с полной цепочкой вместо одного сертификата. В Certbot это значит fullchain.pem вместо cert.pem:

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

Apache 2.4.8 и новее читают цепочку из того же файла, поэтому SSLCertificateFile тоже должен указывать на fullchain.pem; старая директива SSLCertificateChainFile устарела. Если сертификат куплен у коммерческого удостоверяющего центра, обычно вы получаете сертификат и "bundle" или "CA bundle" отдельными файлами. Объедините их, поставив свой сертификат первым:

bash
$ cat example_com.crt ca-bundle.crt > fullchain.crt

Порядок важен. Если промежуточный сертификат окажется первым, nginx пожалуется, что ключ не соответствует сертификату, потому что он сверяет ключ с первым сертификатом в файле. Сам корневой сертификат включать не нужно: он уже есть у клиентов, а его отправка лишь делает каждое рукопожатие больше.

Самоподписанные сертификаты и NET::ERR_CERT_AUTHORITY_INVALID#

Самоподписанный сертификат подписан собственным ключом. Трафик он шифрует ничуть не хуже любого другого, но за него никто не ручается, поэтому каждый браузер отклоняет его с NET::ERR_CERT_AUTHORITY_INVALID. Для публичного сайта со стороны браузера исправлять нечего: получите настоящий сертификат. Let's Encrypt бесплатен и полностью автоматизируется, так что самоподписанный сертификат на публичном домене в 2026 году - это конфигурация, которая задумывалась как временная.

Где они встречаются законно:

  • Свежая установка. Многие панели управления, роутеры, NAS, Proxmox и пакеты веб-серверов по умолчанию создают самоподписанный сертификат при установке, чтобы HTTPS вообще работал. Предупреждение ожидаемо, пока вы не установите нормальный сертификат.
  • Локальная разработка. Создать такой сертификат можно одной командой:
bash
$ openssl req -x509 -newkey rsa:2048 -nodes -days 365 \    -keyout localhost.key -out localhost.crt \    -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"

Строку -addext чаще всего и пропускают, а без неё Chrome отклоняет сертификат и по имени, и по доверию. Для всего, что серьёзнее быстрого теста, проще mkcert: он создаёт небольшой локальный удостоверяющий центр, устанавливает его в хранилища доверия системы и браузера и выпускает сертификаты, которым доверяет ваша машина без всяких предупреждений - и ни одна другая.

  • Внутренние сервисы с частным CA. Компании держат собственный удостоверяющий центр для внутренних имён. Такие сертификаты показывают 19 (self-signed certificate in certificate chain) в s_client на любой машине, где не установлен корневой сертификат компании. Исправление - распространить корневой сертификат, а не отключать проверку.

Два предупреждения о коротких путях. Нажатие "Продолжить" приучает пользователей нажимать "Продолжить", а именно эта привычка и нужна атакующему. А curl -k, verify=False в Python requests или NODE_TLS_REJECT_UNAUTHORIZED=0 полностью отключают проверку: если они остались в рабочем коде, любая машина на пути может читать и изменять трафик. Вместо этого укажите клиенту правильный файл CA: curl --cacert, REQUESTS_CA_BUNDLE или NODE_EXTRA_CA_CERTS.

ERR_SSL_PROTOCOL_ERROR: рукопожатие не завершилось#

Все описанные выше ошибки означают, что сертификат пришёл, но браузеру он не понравился. ERR_SSL_PROTOCOL_ERROR обычно означает, что сертификат не пришёл вовсе, потому что на порту отвечало не то, что говорит на TLS так, как ожидает браузер.

Обычный HTTP на HTTPS-порту. Самая частая причина с большим отрывом. В блоке server указано listen 443; без ssl, приложение слушает порт 443 напрямую без настроенного TLS, или вы набрали https:// перед портом, который отвечает только по HTTP, например перед собственным :3000 или :8080 приложения. Вариант этой ошибки в Firefox, SSL_ERROR_RX_RECORD_TOO_LONG, говорит об этом честнее: он получил HTTP-ответ и попытался прочитать его как TLS. s_client в строках ошибок сообщает wrong version number или packet length too long. Проверьте директиву listen:

nginx
listen 443 ssl;http2 on;

http2 on; - форма, начиная с nginx 1.25.1; в более старых версиях пишется listen 443 ssl http2;. В любом случае ssl должно быть на месте.

Для этого имени не настроен сертификат. Некоторые серверы, получив имя SNI, для которого у них нет сертификата и нет варианта по умолчанию, обрывают рукопожатие, а не отправляют что-то неправильное. Браузер сообщает об ошибке протокола. Исправление то же, что и при несовпадении имени: сделайте так, чтобы это имя было в конфигурации.

Только старые версии протокола. Современные браузеры отказались от TLS 1.0 и 1.1. Сервер, устройство или старое встраиваемое оборудование, которые говорят только на них, вызывают ERR_SSL_VERSION_OR_CIPHER_MISMATCH, а не ошибку протокола, но проверка та же:

bash
$ openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null$ openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null

Если не работают обе, сервер нужно обновить, а в nginx стоит задать строку ssl_protocols TLSv1.2 TLSv1.3;.

Что-то посередине. Антивирус с "сканированием HTTPS", корпоративный прокси для инспекции и страницы авторизации в wifi отелей и поездов перехватывают TLS. В зависимости от качества реализации результатом становится ошибка протокола или ошибка удостоверяющего центра, а издателем сертификата, если его видно, оказывается производитель антивируса или компания, а не публичный удостоверяющий центр. Если проблема только в одной сети или на одной машине, подозревайте это в первую очередь. Сама страница тут ничего исправить не может.

Впереди CDN, а исходный сервер настроен неверно. За Cloudflare неисправный исходный сервер проявляется собственными страницами ошибок CDN: 525 означает, что рукопожатие между CDN и вашим сервером не удалось, 526 означает, что режим "Full (strict)" признал сертификат вашего исходного сервера недействительным. Режим "Flexible" вместе с исходным сервером, который перенаправляет на HTTPS, даёт ERR_TOO_MANY_REDIRECTS. О том, какой режим к какому серверу подходит, рассказано в материале Cloudflare для сайтов и игровых серверов.

Сертификат исправен, но "Не защищено": смешанное содержимое#

Если сертификат в порядке, а замок отсутствует, зачёркнут или заменён надписью "Не защищено", страница, загруженная по HTTPS, подтягивает что-то по обычному HTTP. Скрипты, таблицы стилей и iframe, запрошенные по http://, блокируются полностью, и обычно это выглядит как страница без оформления или со сломанным меню, а не как очевидная ошибка. Изображения и медиа современный Chrome автоматически переводит на HTTPS или блокирует, если перевод не удался.

Консоль браузера называет каждый проблемный адрес, и поиск превращается в список. В Chrome это выглядит так:

code
Mixed Content: The page at 'https://example.com/' was loaded over HTTPS,but requested an insecure script 'http://example.com/js/menu.js'.This request has been blocked; the content must be served over HTTPS.

Исправление - перестать генерировать URL с http://: обычно это старые абсолютные адреса, сохранённые в базе данных CMS, жёстко прописанный хост ресурсов в теме или приложение за reverse proxy, которое не понимает, что посетитель пришёл по HTTPS, и строит ссылки соответственно. Для последнего случая приложению нужно доверять заголовку X-Forwarded-Proto от прокси. Очистку и настройки прокси описывают и HTTPS и Let's Encrypt простыми словами, и как направить домен на сервер и добавить SSL, а безопасный поиск с заменой в базе данных - перенос WordPress на новый хостинг.

Где сертификатом занимаются за вас#

Большинство описанных сбоев связано с тем, что человек управляет файлами: сломавшееся продление, перезагрузка, которую никто не выполнил, cert.pem там, где нужен был fullchain.pem. На тарифах RE:NODE веб-хостинг и хостинг приложений этим занимается слот прокси. Вы направляете A-запись на показанный адрес, а сертификат выпускается и продлевается автоматически, в окне за 21 день до окончания срока. Ваше приложение получает обычный HTTP на собственном порту и настоящий адрес посетителя в X-Forwarded-For, поэтому сертификата у него нет и собирать цепочку не нужно.

Что остаётся прежним: DNS по-прежнему находится у вашего регистратора домена, поэтому неверная или устаревшая A-запись всё равно приводит к проблеме с именем или выпуском, а смешанное содержимое по-прежнему нужно исправлять в приложении. Если что-то выглядит неправильно, команды s_client из этой статьи работают с любым хостом, в том числе с нашим.

FAQ#

Безопасно ли нажимать "Всё равно перейти"?

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

Почему сайт работает в Chrome, но не в curl или моём приложении?

Сервер отправляет сертификат без промежуточного. Десктопные браузеры восполняют пробел сами, большинство других клиентов - нет. Отдавайте файл с полной цепочкой, и разница исчезнет.

Почему ошибку сертификата видит только один посетитель?

Потому что проблема на его стороне: неверные системные часы, сканирование HTTPS антивирусом, рабочий прокси или страница входа в публичный wifi. Попросите у него точный код ошибки и издателя, указанного в сведениях о сертификате.

Как проверить, когда истекает мой сертификат?

Выполните s_client с передачей вывода в openssl x509 -noout -dates из-за пределов вашей сети, как показано выше. notAfter - это дата окончания. Для чего-то важного настройте проверку в мониторинге, которая сообщит, когда остаётся меньше 14 дней, чтобы сломанное продление заметили раньше посетителей.

Можно ли сбросить ошибку HSTS для сайта, над которым я работаю?

В Chrome на странице chrome://net-internals/#hsts есть поле "Delete domain security policies", которое удаляет запись HSTS сайта из вашего браузера. Оно не может убрать домен из встроенного в браузер списка предзагрузки, а настоящее исправление - всё равно действительный сертификат.

Вызывает ли бесплатный сертификат больше ошибок, чем платный?

Нет. Браузеры доверяют Let's Encrypt так же, как любому коммерческому удостоверяющему центру, а ошибки из этой статьи случаются и с платными сертификатами так же часто, а с отсутствующими цепочками - чаще, потому что файл bundle приходит отдельно.

Это было полезно?