Fix SSL: HANDSHAKE_FAILURE — SSL Handshake Thất Bại Khi Kết Nối HTTPS

intermediate🔒 SSL/TLS2026-03-18| Linux/macOS/Windows — curl, Python requests, Node.js, Java, OpenSSL 1.0.x–3.x

Error Message

SSL: HANDSHAKE_FAILURE — SSL routines:ssl3_read_bytes:sslv3 alert handshake failure
#ssl#handshake#tls#openssl

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_client và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 = False trong 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

Related Error Notes