Дебаг 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)
Главное:
Если 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 — сертификат для другого домена.
Причины:
- На сервере default vhost отдаёт чужой cert.
- DNS неправильно резолвится — попали на чужой IP.
- Cert выпущен для другого имени (typo в
-dcertbot’а).
Ошибка 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 минуты получите:
Цель — получить 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
Что вы должны вынести
openssl s_client— основной debug-инструмент. Ключевые поля: chain, Protocol, Cipher, Verify return code.- Missing intermediate (verify code 20) — одна из самых частых проблем. Решение — fullchain.pem.
- Expired (10) — автоматизировать renewal + мониторинг expiry заранее.
- Name mismatch — проверить SAN сертификата.
- Старые TLS-версии и weak ciphers — настраивать по Mozilla SSL Configuration Generator.
- SSL Labs (https://www.ssllabs.com/) — быстрая общая оценка. Цель — A+.
- testssl.sh, sslyze — более детальные CLI tools.
- Включать monitoring expiry за 30+ дней — предотвращать incidents.
openssl, curl и tcpdump вместе: полный TLS debugging workflow