Learning Platform
Глоссарий Troubleshooting
Урок 12.04 · 22 мин
Средний
TLSDebuggingOpenSSLTroubleshooting

Дебаг TLS на практике — openssl s_client, SSL Labs, типичные ошибки

В реальной работе TLS ломается. Сертификат истёк, цепочка собрана неправильно, сервер настроен на устаревший TLS 1.0, name mismatch — классические проблемы, которые junior встречает каждую неделю. Умение быстро diagnosticate TLS-issue — ценный практический навык.

В этом уроке: команды и инструменты для дебага TLS, типичные ошибки и их симптомы, как читать вывод openssl s_client.


openssl s_client — швейцарский нож для TLS

openssl s_client — CLI-инструмент, который подключается к TLS-серверу и показывает все детали handshake. Это основной debug-инструмент.

Базовое использование:

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

Что делают флаги:

  • -connect host:port — куда подключиться.
  • -servername example.com — указать SNI (для виртуального хостинга). Без этого сервер вернёт default cert, который может не совпадать.

После запуска видим многострочный вывод. Запускайте с < /dev/null чтобы команда не зависла в interactive mode после handshake:

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

Что важное в выводе

Разберём типичный успешный вывод (упрощённо):

CONNECTED(00000003)
depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=R3
verify return:1
depth=0 CN=example.com
verify return:1
---
Certificate chain
 0 s:CN = example.com
   i:C = US, O = Let's Encrypt, CN = R3
 1 s:C = US, O = Let's Encrypt, CN = R3
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFazCC...
-----END CERTIFICATE-----
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R3
---
SSL handshake has read 4172 bytes and written 410 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: ...
    Master-Key: ...
    Verify return code: 0 (ok)

Главное:

Ключевые поля вывода openssl s_client
Certificate chainЦепочка от leaf (depth=0) до root (depth=N). Должна быть полная. Если только depth=0 -- missing intermediate, проблема
Verify return: 11 на каждой ступени -- проверка прошла. 0 -- провалена. Также внизу 'Verification: OK' или 'Verification error'
Subject и issuers: subject (кому выдан) -- должен совпадать с доменом. i: issuer (кто выдал) -- должен быть recognized CA
ProtocolTLS-версия согласованная. TLSv1.3 -- идеально. TLSv1.2 -- OK. TLSv1.0/1.1 -- небезопасно, проблема
CipherСогласованный cipher suite. TLS_AES_*_GCM_SHA* или TLS_CHACHA20_POLY1305_* -- безопасно. CBC -- старое, RC4/3DES -- очень плохо
Verify return code: 00 = ok. Не-ноль -- проблема. Коды: 9 = expired, 18 = self-signed, 19 = self-signed in chain, 20 = unable to get local issuer, 21 = unable to verify first cert

Если Verification: OK — TLS работает. Если что-то не так, посмотрите конкретный код в Verify return code.


Типичные ошибки и их диагноз

Ошибка 1: Missing intermediate

Симптомы:

  • В браузере: NET::ERR_CERT_AUTHORITY_INVALID (Chrome) или SEC_ERROR_UNKNOWN_ISSUER (Firefox).
  • В curl: unable to get local issuer certificate.
  • В openssl: Verify return code: 20 (unable to get local issuer certificate).

Diagnostic:

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | grep -E 's:|i:'

Если видите только один сертификат в chain (только depth=0), а не два-три — intermediate отсутствует.

Причина: на сервере (nginx, apache) указан только leaf cert, не fullchain.

Решение для nginx:

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

fullchain.pem — это leaf + intermediate, не просто leaf. Let’s Encrypt создаёт его автоматически. Часто люди ставят cert.pem — это только leaf, проблема.

Ошибка 2: Expired certificate

Симптомы:

  • В браузере: NET::ERR_CERT_DATE_INVALID.
  • В openssl: Verify return code: 10 (certificate has expired).

Diagnostic:

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

Видим:

notBefore=Jan 15 00:00:00 2025 GMT
notAfter=Apr 15 00:00:00 2025 GMT

Если notAfter в прошлом — expired. Часто причина: автоматический renewal через certbot не сработал (ошибка cron, ошибка validation — например, .well-known директория стала недоступной).

Решение:

sudo certbot renew --force-renewal
sudo systemctl reload nginx

Чтобы заранее предотвращать — мониторинг expiry. Минимально:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -checkend $((30*24*60*60)) && echo "OK (>30 days)" || echo "WARNING: cert expires soon"

В production используют exporters (blackbox_exporter для Prometheus), которые проверяют все ваши certs и алертят за месяц до expiry.

Ошибка 3: Name mismatch

Симптомы:

  • В браузере: NET::ERR_CERT_COMMON_NAME_INVALID.
  • В curl: SSL: no alternative certificate subject name matches target host name.

Diagnostic:

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -text | grep -A 1 'Subject Alternative Name'

Видим:

X509v3 Subject Alternative Name:
    DNS:other-domain.com, DNS:www.other-domain.com

Если в SAN нет example.com — сертификат для другого домена.

Причины:

  1. На сервере default vhost отдаёт чужой cert.
  2. DNS неправильно резолвится — попали на чужой IP.
  3. Cert выпущен для другого имени (typo в -d certbot’а).

Ошибка 4: Старый TLS protocol

Симптомы:

  • В современных браузерах: NET::ERR_SSL_VERSION_OR_CIPHER_MISMATCH.
  • В curl: SSL routines:ssl_choose_client_version:unsupported protocol.

Diagnostic:

# Узнать, какие версии поддерживает сервер
for v in 1_0 1_1 1_2 1_3; do
  result=$(echo | openssl s_client -connect example.com:443 -servername example.com -tls$v 2>&1 | grep -i 'protocol\|cipher' | head -2)
  echo "TLS 1.$v: $result"
done

Если сервер поддерживает только 1.0 или 1.1 — современный Chrome не подключится (с 2020 года 1.0/1.1 deprecated).

Решение для nginx:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers off;

Mozilla имеет SSL Configuration Generator — генератор актуального оптимального конфига для разных серверов и режимов (intermediate, modern, old).

Ошибка 5: Weak ciphers

Симптомы:

  • Современный браузер: NET::ERR_SSL_VERSION_OR_CIPHER_MISMATCH.
  • В SSL Labs: оценка C или D, упоминания BEAST, POODLE, FREAK, Logjam.

Diagnostic через nmap:

nmap --script ssl-enum-ciphers -p 443 example.com

Покажет список ciphers с оценками каждого.

Решение: настроить nginx согласно Mozilla Modern или Intermediate profile. Modern (TLS 1.3 only, новые ciphers) — если у вас нет очень старых клиентов. Intermediate (TLS 1.2+, recommended ciphers) — общий случай.


SSL Labs: один сайт для общей оценки

SSL Labs — бесплатный сервис, который сканирует ваш сервер и даёт детальный отчёт. Введите домен, через 1-2 минуты получите:

SSL Labs scan output
Grade A+/A/B/C/D/FОбщая оценка. A+ -- идеально. A -- хорошо. B -- проблемы с конфигурацией. C-F -- серьёзные проблемы
CertificateSubject, issuer, expiry, chain, SAN. Хорошо, если 100
Protocol SupportКакие версии TLS поддерживаются. Лучшая оценка -- TLS 1.3 + 1.2, без 1.0/1.1/SSL
Key ExchangeРазмер DH-параметров, кривые. ECDHE с X25519 -- хорошо. RSA 1024 -- плохо
Cipher StrengthКакие ciphers, в каком порядке. AES-GCM -- хорошо. RC4, 3DES -- плохо
Known issuesУязвимости: Heartbleed, BEAST, POODLE, FREAK, Logjam, ROBOT. Также HSTS, OCSP stapling, forward secrecy

Цель — получить A+. Это требует:

  • TLS 1.2 + TLS 1.3 (без старых).
  • Modern ciphers (AEAD only).
  • Forward Secrecy.
  • HSTS с max-age >= 31536000; includeSubDomains; preload.
  • OCSP stapling.
  • Сертификат с правильным SAN, не expired, intermediate в chain.

Дополнительные tools

testssl.sh

testssl.sh — shell-script с глубоким анализом TLS. Альтернатива SSL Labs для CLI:

# Скачать и запустить
git clone https://github.com/drwetter/testssl.sh
cd testssl.sh
./testssl.sh example.com

Выводит ВСЁ: cipher list, protocol support, известные уязвимости (BEAST, POODLE, ROBOT, GOLDENDOODLE, Heartbleed), цепочку сертификатов. Очень детально.

sslyze

Python-инструмент для автоматизированного TLS scanning. Используется в CI/CD:

pip install sslyze
sslyze --regular example.com

JSON output удобен для интеграции в pipeline.

curl с разными версиями

curl полезен для diagnostic клиентского поведения:

# Принудить TLS 1.3
curl --tls13 -v https://example.com 2>&1 | grep TLS

# Принудить TLS 1.2
curl --tls-max 1.2 -v https://example.com 2>&1 | grep TLS

# Проверить, как curl видит сертификат
curl --cacert /path/to/ca.pem https://internal-service 2>&1 | head -20

# Игнорировать TLS errors (debug only -- не использовать в prod-скриптах!)
curl --insecure https://example.com

Реальный сценарий: production incident

В пятницу вечером production сайт упал. Метрики показывают 100% запросов с TLS-ошибками. Лог:

SSL handshake failed
upstream: "https://backend.internal:8443/..."
SSL routines:tls_process_server_certificate:certificate verify failed

Шаги диагностики:

# 1. Что отдаёт сервер
openssl s_client -connect backend.internal:8443 -servername backend.internal < /dev/null 2>&1 | head -30

# 2. Проверить цепочку
echo | openssl s_client -connect backend.internal:8443 -servername backend.internal 2>/dev/null | openssl x509 -noout -dates
# notAfter=Apr 15 ... -- expired!

# 3. Проверить, запущен ли certbot
sudo systemctl status certbot.timer
# Inactive -- timer выключен

# 4. Попробовать обновить
sudo certbot renew --force-renewal
# Output: ... renewed successfully ...

# 5. Перезагрузить nginx
sudo systemctl reload nginx

# 6. Проверить, что починилось
echo | openssl s_client -connect backend.internal:8443 -servername backend.internal 2>/dev/null | openssl x509 -noout -dates
# notAfter теперь через 90 дней -- OK

# 7. Включить timer обратно
sudo systemctl enable --now certbot.timer

Также: добавить алерт в monitoring (Prometheus + blackbox_exporter), чтобы такая ситуация была замечена за 30 дней до expiry, а не в день incident’а.


Попробуй сам

# 1. Сравнить TLS-конфиги популярных сайтов
for site in github.com cloudflare.com google.com badssl.com; do
  echo "=== $site ==="
  echo | openssl s_client -connect $site:443 -servername $site 2>/dev/null | grep -E 'Protocol|Cipher|Verify return code' | head -3
done

# 2. Проверить, сколько дней до expiry
echo | openssl s_client -connect github.com:443 -servername github.com 2>/dev/null | openssl x509 -noout -dates

# 3. Намеренно сломанные сайты для тренировки
# https://badssl.com/ -- список endpoint'ов с известными проблемами:
# - expired.badssl.com -- expired cert
# - wrong.host.badssl.com -- name mismatch
# - self-signed.badssl.com -- self-signed
# - untrusted-root.badssl.com -- unknown CA
# - tls-v1-0.badssl.com -- старый TLS
# - dh1024.badssl.com -- weak DH
#
# Для каждого:
echo | openssl s_client -connect expired.badssl.com:443 -servername expired.badssl.com 2>&1 | grep -E 'Verify return code'
echo | openssl s_client -connect wrong.host.badssl.com:443 -servername wrong.host.badssl.com 2>&1 | grep -E 'Verify return code'
echo | openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com 2>&1 | grep -E 'Verify return code'

# 4. SSL Labs scan
# Откройте https://www.ssllabs.com/ssltest/analyze.html?d=YOUR-DOMAIN
# Изучите подробный отчёт. Какие у вашего сервера слабые места?

# 5. Запустить локальный TLS server и проверить с openssl
# Терминал 1:
openssl req -x509 -newkey rsa:2048 -nodes -keyout /tmp/key.pem -out /tmp/cert.pem -days 365 -subj '/CN=localhost'
openssl s_server -cert /tmp/cert.pem -key /tmp/key.pem -accept 4443 -www

# Терминал 2:
openssl s_client -connect localhost:4443 < /dev/null 2>&1 | head -30
# Видите своё self-signed cert
# Verify return code: 18 (self signed certificate) -- expected

Что вы должны вынести

  1. openssl s_client — основной debug-инструмент. Ключевые поля: chain, Protocol, Cipher, Verify return code.
  2. Missing intermediate (verify code 20) — одна из самых частых проблем. Решение — fullchain.pem.
  3. Expired (10) — автоматизировать renewal + мониторинг expiry заранее.
  4. Name mismatch — проверить SAN сертификата.
  5. Старые TLS-версии и weak ciphers — настраивать по Mozilla SSL Configuration Generator.
  6. SSL Labs (https://www.ssllabs.com/) — быстрая общая оценка. Цель — A+.
  7. testssl.sh, sslyze — более детальные CLI tools.
  8. Включать monitoring expiry за 30+ дней — предотвращать incidents.

openssl, curl и tcpdump вместе: полный TLS debugging workflow
Проверка знанийKnowledge check
Ваш Python-скрипт, ходящий в internal API, начал падать с 'ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate'. В браузере при подключении к тому же API -- всё работает, замок зелёный. Что происходит и как пофиксить (без отключения проверки)?
ОтветAnswer
Это классический симптом missing intermediate certificate в server config + разное поведение клиентов. Браузеры (Chrome, Firefox) КЭШИРУЮТ intermediate certs, которые видели на других сайтах. Если ваш сервер отдаёт только leaf cert, а Chrome где-то раньше получал нужный intermediate -- он может построить chain локально и работать. Python's requests/urllib3 -- НЕТ. Использует certifi (built-in Mozilla root store), но НЕ кэширует intermediates между connections. Каждый раз ожидает, что сервер отдаст полную chain. Diagnostic: (1) Откройте openssl s_client -connect api.internal:443 < /dev/null -- посчитайте сертификаты в Certificate chain. Если только один (depth=0) -- сервер не отдаёт intermediate. Это и есть проблема. (2) В Chrome F12 -> Security tab -> View certificate -- посмотрите, какая chain полная. Если в browser cert chain больше уровней, чем в openssl output -- браузер построил локально через cache. Решение (на сервере): на сервере nginx/apache использовать fullchain.pem (leaf + intermediate), не cert.pem (только leaf). Для Let's Encrypt -- ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;. Перезагрузить nginx. После этого openssl s_client покажет полную chain, Python будет работать. Альтернативное решение, если не можете править сервер: в Python скрипте дать certifi явный bundle, содержащий intermediate. requests.get(url, verify='/path/to/full-ca-bundle.pem'). Но это hack -- надо чинить на сервере. НИКОГДА не используйте verify=False в production -- это disable TLS-validation полностью, открывает MITM.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. openssl s_client возвращает 'Verify return code: 20 (unable to get local issuer certificate)'. Что это значит и как починить НА СЕРВЕРЕ?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4