Lỗi Gặp Phải
Khi kết nối đến một endpoint HTTPS, bạn nhận được thông báo:
SSL: HANDSHAKE_FAILURE — SSL routines:ssl3_read_bytes:sslv3 alert handshake failure
Hoặc trong curl:
curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to example.com:443
Hoặc trong Python:
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded ... caused by SSLError(SSLError(1, '[SSL: HANDSHAKE_FAILURE]
ssl handshake failure (_ssl.c:997)'))
Quá trình TLS handshake là nơi client và server thỏa thuận về cipher suite và trao đổi certificate. Khi quá trình này thất bại, kết nối bị ngắt ngay lập tức — không có dữ liệu nào được truyền qua.
Nguyên Nhân Gây Ra Lỗi
Sáu nguyên nhân chính chiếm phần lớn các trường hợp lỗi này:
- Không khớp phiên bản TLS — server yêu cầu TLS 1.2+ nhưng client chỉ hỗ trợ SSLv3/TLS 1.0
- Không có cipher suite chung — danh sách cipher của client không có cipher nào trùng với cipher server cho phép
- OpenSSL lỗi thời — các phiên bản dưới 1.1.1 (EOL tháng 12/2019) không hỗ trợ các cipher suite ECDHE và AES-GCM mà hầu hết server hiện nay yêu cầu
- Server yêu cầu SNI — client không gửi Server Name Indication, khiến server từ chối handshake ngay lập tức
- Certificate hết hạn hoặc chain bị hỏng — cert đã hết hạn hoặc thiếu intermediate CA certificate
- Firewall/proxy can thiệp — một thiết bị trung gian làm mất hoặc làm hỏng gói ClientHello trước khi đến server
Chẩn Đoán Nhanh
Đừng đoán mò — hãy chạy các lệnh kiểm tra sau để xác định chính xác nơi quá trình thỏa thuận thất bại.
Bước 1 — Kiểm tra các phiên bản TLS mà server hỗ trợ
# Kiểm tra phiên bản TLS cụ thể
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# Kiểm tra xem server còn cho phép phiên bản cũ không (sẽ thất bại trên server được cấu hình chặt)
openssl s_client -connect example.com:443 -ssl3
openssl s_client -connect example.com:443 -tls1
Nếu -tls1_2 kết nối thành công nhưng -tls1 thất bại, server đã loại bỏ TLS 1.0 — client của bạn cần cập nhật.
Bước 2 — Kiểm tra quá trình thỏa thuận cipher suite
openssl s_client -connect example.com:443 -cipher 'ALL' 2>&1 | grep -E 'Cipher|Protocol|Alert'
Bước 3 — Xem output chi tiết của curl
curl -v --tlsv1.2 https://example.com 2>&1 | head -40
Chú ý đến TLSv1.2, Handshake, Client hello theo sau ngay bởi alert handshake failure. Chuỗi đó xác nhận server đã từ chối ClientHello của bạn.
Fix 1 — Ép Buộc Phiên Bản TLS Tương Thích (Quick Fix)
Trong hầu hết các trường hợp, ép buộc TLS 1.2 sẽ giải quyết vấn đề ngay lập tức.
curl
curl --tlsv1.2 https://example.com
# hoặc cho TLS 1.3
curl --tlsv1.3 https://example.com
Python requests
import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
class TLS12Adapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = create_urllib3_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
kwargs['ssl_context'] = ctx
return super().init_poolmanager(*args, **kwargs)
session = requests.Session()
session.mount('https://', TLS12Adapter())
response = session.get('https://example.com')
Node.js (https module)
const https = require('https');
const options = {
hostname: 'example.com',
port: 443,
path: '/',
minVersion: 'TLSv1.2', // Tối thiểu TLS 1.2; vẫn cho phép TLS 1.3
};
https.get(options, (res) => console.log(res.statusCode));
Lưu ý: minVersion (Node 12+) là cách tiếp cận hiện đại. secureProtocol: 'TLSv1_2_method' cũ hơn sẽ ghim chính xác TLS 1.2 và chặn TLS 1.3 — tránh dùng cho code mới.
Fix 2 — Cập Nhật OpenSSL và Thư Viện Hệ Thống
OpenSSL 1.0.2 đã kết thúc vòng đời vào tháng 12/2019. Bất kỳ phiên bản nào dưới 1.1.1 đều thiếu các cipher suite ECDHE và AES-GCM mà server hiện đại yêu cầu.
# Kiểm tra phiên bản hiện tại
openssl version -a
# Ubuntu/Debian
sudo apt update && sudo apt upgrade openssl libssl-dev
# CentOS/RHEL
sudo yum update openssl
# macOS (qua Homebrew)
brew upgrade openssl
Sau khi nâng cấp, hãy cài lại hoặc rebuild các cài đặt Python/Node.js của bạn. Cả hai đều liên kết với OpenSSL hệ thống tại thời điểm biên dịch và sẽ không tự động dùng phiên bản mới.
Fix 3 — Chỉ Định Cipher Suites Một Cách Rõ Ràng
Khi bạn kiểm soát client, hãy liệt kê rõ ràng ít nhất một cipher mà server chấp nhận.
curl với ciphers được chỉ định rõ ràng
curl --tlsv1.2 \
--ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384' \
https://example.com
Python ssl context
import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
CIPHERS = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:AES128-GCM-SHA256'
class CipherAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = create_urllib3_context(ciphers=CIPHERS)
kwargs['ssl_context'] = ctx
return super().init_poolmanager(*args, **kwargs)
session = requests.Session()
session.mount('https://', CipherAdapter())
response = session.get('https://example.com')
Fix 4 — Vấn Đề SNI (Virtual Hosting)
Khi một server host nhiều domain trên một IP, nó cần extension SNI để chọn đúng certificate. Các client bỏ qua SNI sẽ bị từ chối.
# Kiểm tra với SNI một cách rõ ràng
openssl s_client -connect example.com:443 -servername example.com
# Không có SNI (mô phỏng client cũ)
openssl s_client -connect example.com:443 -noservername
Lệnh đầu thành công, lệnh thứ hai thất bại? Server yêu cầu SNI. Tin vui là: Python 2.7.9+, Node.js 0.12+, và Java 8u161+ đều gửi SNI tự động. Nếu bạn đang dùng phiên bản cũ hơn, hãy nâng cấp runtime — việc vá SNI support thủ công không đáng.
Fix 5 — Vấn Đề Certificate Chain
Thiếu intermediate certificate trên server là nguyên nhân phổ biến đáng ngạc nhiên.
# Kiểm tra toàn bộ cert chain
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -dates
# Kiểm tra ngày hết hạn
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -enddate
Nếu bạn thấy verify error:num=20:unable to get local issuer certificate, server không gửi intermediate CA cert. Đây là lỗi phía server — hãy liên hệ admin và yêu cầu họ cài đặt full chain.
Xác Nhận Đã Fix Thành Công
# Output khi handshake thành công trông như sau:
openssl s_client -connect example.com:443 -tls1_2
# ...
# SSL-Session:
# Protocol : TLSv1.2
# Cipher : ECDHE-RSA-AES128-GCM-SHA256
# ...
# Verify return code: 0 (ok) ← đây là kết quả bạn muốn
# Hoặc với curl
curl -v https://example.com 2>&1 | grep -E 'SSL|TLS|Connected'
# Sẽ hiển thị: * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
Verify return code: 0 (ok) nghĩa là handshake đã thành công và certificate chain hợp lệ. Bất kỳ kết quả nào khác đều có nghĩa là bạn chưa xong.
Mẹo
Việc debug certificate thường liên quan đến việc kiểm tra tính toàn vẹn của file. Hash Generator của ToolCraft cho phép bạn tạo SHA-256 hoặc MD5 hash của các file cert ngay trên trình duyệt — không có gì được tải lên, điều này quan trọng khi xử lý các key nhạy cảm.
Để phòng ngừa lâu dài, hãy duy trì các thói quen sau:
- Đặt TLS 1.2 là phiên bản tối thiểu trong tất cả cấu hình HTTP client — đừng để ở chế độ "auto"
- Thêm lệnh kiểm tra
openssl s_clientvào CI pipeline để phát hiện cert hết hạn trước người dùng - Không bao giờ đặt
ssl.SSLContext.check_hostname = Falsetrong Python — nó sẽ vô hiệu hóa hoàn toàn việc xác minh hostname - Nếu bạn quản lý server, hãy kiểm tra cấu hình TLS với SSL Labs; hướng đến đánh giá A

