Lỗi Gặp Phải
Ứng dụng Node.js của bạn kết nối đến một endpoint HTTPS và gặp lỗi:
Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE
at TLSSocket.onConnectEnd (_tls_wrap.js:1495:19)
at Object.onceWrapper (events.js:422:26)
at TLSSocket.emit (events.js:327:22)
Đang là 2 giờ sáng. Endpoint đó mở bình thường trên Chrome, curl trên laptop của bạn cũng không có vấn đề gì, nhưng Node.js thì cứng đầu từ chối kết nối. Chứng chỉ hoàn toàn hợp lệ — bạn thấy rõ ràng bằng mắt thường. Vậy vấn đề ở đâu?
Lỗi Này Thực Sự Có Nghĩa Là Gì
Đây không phải lỗi chứng chỉ hết hạn. Cũng không phải chứng chỉ tự ký. Vấn đề là Node.js không thể xây dựng một chuỗi tin cậy từ chứng chỉ của máy chủ lên đến root CA mà nó nhận ra.
Chín trên mười lần, nguyên nhân rất đơn giản: máy chủ chỉ gửi chứng chỉ lá (leaf certificate) trong quá trình bắt tay TLS nhưng bỏ qua chứng chỉ CA trung gian (intermediate CA). Không có intermediate thì không có chuỗi. Không có chuỗi thì không có kết nối.
Trình duyệt xử lý điều này một cách linh hoạt — chúng tự động tải các intermediate còn thiếu thông qua Authority Information Access (AIA) và lưu vào cache cục bộ. Node.js thì không. Nó cần toàn bộ chuỗi được cung cấp đầy đủ ngay từ đầu, mỗi lần một lần.
Tái Hiện và Chẩn Đoán
Bước 1 — Kiểm tra xem máy chủ có chuỗi chứng chỉ bị lỗi không
Chạy OpenSSL trực tiếp với máy chủ:
openssl s_client -connect yourdomain.com:443 -showcerts
Một chuỗi hợp lệ sẽ hiển thị ít nhất hai chứng chỉ:
Certificate chain
0 s:CN=yourdomain.com
i:CN=Some Intermediate CA
1 s:CN=Some Intermediate CA
i:CN=Root CA
Một chuỗi bị lỗi chỉ hiển thị một:
Certificate chain
0 s:CN=yourdomain.com
i:CN=Some Intermediate CA
Chỉ có chứng chỉ lá. Intermediate đã biến mất. Đó chính là thủ phạm.
Bước 2 — Xác nhận Node.js đang thất bại
node -e "require('https').get('https://yourdomain.com', r => console.log(r.statusCode)).on('error', e => console.error(e.message))"
Thấy UNABLE_TO_VERIFY_LEAF_SIGNATURE trong kết quả? Đã xác nhận.
Các Giải Pháp
Giải pháp 1 — Sửa từ phía máy chủ (cách đúng đắn)
Đây là lỗi cấu hình máy chủ, không phải lỗi của Node.js. Hãy sửa từ gốc rễ: cấu hình máy chủ để gửi toàn bộ chuỗi chứng chỉ.
Với Nginx, gộp các chứng chỉ vào một file duy nhất và trỏ ssl_certificate vào đó:
# Nối leaf + intermediate
cat your_domain.crt intermediate.crt > fullchain.pem
# Trong nginx.conf
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/your_domain.key;
Với Apache:
SSLCertificateFile /etc/ssl/certs/your_domain.crt
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
Với Node.js HTTPS server:
const https = require('https');
const fs = require('fs');
https.createServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
ca: fs.readFileSync('intermediate.crt') // thêm dòng này
}, app).listen(443);
Reload lại máy chủ, rồi chạy lại openssl s_client. Lúc này bạn sẽ thấy cả chứng chỉ lá lẫn intermediate trong kết quả chuỗi.
Giải pháp 2 — Cung cấp CA cert từ phía client Node.js
Không kiểm soát được máy chủ? Có thể bạn đang gọi đến API bên thứ ba có chuỗi chứng chỉ bị lỗi. Trong trường hợp đó, hãy chỉ định rõ cho Node.js biết CA cert nào cần tin tưởng:
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
ca: fs.readFileSync('/path/to/intermediate-or-root-ca.crt')
});
https.get({
hostname: 'yourdomain.com',
path: '/',
agent: agent
}, (res) => {
console.log(res.statusCode);
});
Đang dùng axios? Tương tự, chỉ khác tên tùy chọn:
const axios = require('axios');
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
ca: fs.readFileSync('/path/to/ca-bundle.crt')
});
const response = await axios.get('https://yourdomain.com/api', {
httpsAgent: agent
});
Đây là cách xử lý tạm thời, không phải giải pháp lâu dài. Hãy thúc đẩy team quản lý máy chủ sửa chuỗi chứng chỉ đúng cách.
Giải pháp 3 — Cập nhật CA bundle của hệ thống (máy chủ Linux)
Các máy Linux mới đôi khi được cài với CA bundle cũ, thiếu root CA phát hành. Cập nhật nhanh thường giải quyết được:
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y ca-certificates
sudo update-ca-certificates
# RHEL/CentOS
sudo yum update ca-certificates
update-ca-trust
Node.js đọc CA bundle của hệ thống khi khởi động. Không cần thay đổi code sau khi cập nhật — chỉ cần khởi động lại tiến trình của bạn.
Những Gì KHÔNG Nên Làm
Gợi ý này xuất hiện khắp nơi trên Stack Overflow. Hãy kiên quyết tránh xa:
// ĐỪNG LÀM ĐIỀU NÀY TRÊN PRODUCTION
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
// Tương tự vấn đề:
const agent = new https.Agent({ rejectUnauthorized: false });
Tắt xác minh chứng chỉ không phải là cách sửa lỗi. Nó vô hiệu hóa hoàn toàn việc xác thực TLS, khiến ứng dụng của bạn dễ bị tấn công man-in-the-middle. Dùng trong script tạm thời ở local thì được. Nhưng trên môi trường production, đó là một sự cố bảo mật nghiêm trọng.
Xác Minh Kết Quả Sửa Lỗi
Ba lệnh để xác nhận mọi thứ đang hoạt động:
# 1. Kiểm tra máy chủ gửi đầy đủ chuỗi chứng chỉ
openssl s_client -connect yourdomain.com:443 -showcerts 2>&1 | grep -E '(subject|issuer|Certificate chain)'
# 2. Xác minh chuỗi hợp lệ với CA store của hệ thống
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /dev/null console.log('OK:', r.statusCode)).on('error', e => console.error('FAIL:', e.message))"
Bạn sẽ thấy OK: 200. Hoặc bất kỳ status code nào mà endpoint thường trả về — điều quan trọng là không còn lỗi nữa.
Để kiểm tra sâu hơn, hãy chạy máy chủ qua SSL Labs. Công cụ này sẽ đánh dấu rõ các intermediate còn thiếu và cung cấp hình ảnh trực quan về chuỗi chứng chỉ — rất đáng để bookmark lại.
Bài Học Rút Ra
- Trình duyệt đánh lừa bạn. Chrome và Firefox âm thầm tải các intermediate còn thiếu và lưu vào cache. Bạn có thể load trang hoàn toàn bình thường trong khi Node.js,
curl, và mọi client server-to-server đều từ chối cùng một endpoint đó. Luôn kiểm tra TLS bằngopenssl s_client— không phải bằng tab trình duyệt. - Gia hạn chứng chỉ là nguyên nhân số 1. Ai đó gia hạn chứng chỉ lá, copy vào vị trí cũ, rồi quên không gộp lại intermediate. Hãy thêm bước kiểm tra chuỗi bằng
openssl s_clientvào quy trình triển khai của bạn. Chỉ mất năm giây và bắt được lỗi này mỗi lần. - Các workaround phía client là dấu hiệu xấu. Nếu bạn đang inject
caoverrides vào mọi service kết nối đến một endpoint, đó là nợ kỹ thuật đang tích lũy. Chuỗi chứng chỉ phải được sửa từ phía máy chủ. Hãy tạo ticket và theo dõi. - Base image của container bị lỗi thời. Một ứng dụng Node.js trong Docker image được build từ
node:16một năm trước có thể không tin tưởng các root certificate mới hơn. ThêmRUN apt-get install -y ca-certificatesvào Dockerfile và build lại — đừng để lỗi này đuổi bạn trên production lúc 2 giờ sáng.

