Lỗi Gặp Phải
Bạn đang thực hiện request HTTPS trong Node.js — dùng fetch, axios, https, hoặc got — và gặp lỗi:
Error: unable to get local issuer certificate
at TLSSocket.onConnectEnd (_tls_wrap.js:1495:19)
at Object.onceWrapper (events.js:422:26)
at TLSSocket.emit (events.js:327:22)
Hoặc một trong các biến thể sau:
Error: self-signed certificate in certificate chain
Error: certificate has expired
Error: UNABLE_TO_GET_ISSUER_CERT_LOCALLY
Request thất bại hoàn toàn. TLS stack của Node đã kiểm tra certificate từ server, không xác minh được ai đã ký nó, và ngắt kết nối.
Nguyên Nhân Gốc Rễ
Node.js đi kèm với kho certificate riêng — được đóng gói từ danh sách CA của Mozilla. Nó không sử dụng OS trust store của hệ điều hành. Vì vậy khi server trình bày certificate được ký bởi một CA không có trong bundle của Node — một CA trung gian của công ty, PKI nội bộ, hoặc cert tự ký — Node từ chối thực hiện handshake.
Điều này khiến các developer bất ngờ vì browser và curl hoạt động bình thường trên cùng máy. Chúng sử dụng OS trust store. Node thì không.
Những trường hợp thường gặp:
- Mạng nội bộ công ty có SSL inspection (proxy chặn và ký lại traffic HTTPS bằng root CA riêng)
- API nội bộ dùng certificate tự ký hoặc được cấp phát riêng
- Môi trường dev HTTPS cục bộ
- Certificate trung gian đã hết hạn trong chuỗi
Debug Trước Đã
Đừng đoán mò. Chạy các lệnh sau để xem chính xác chuỗi certificate mà server đang trình bày:
# Hiển thị chi tiết chuỗi cert: issuer, subject và ngày hết hạn
openssl s_client -connect your-api-host.com:443 -showcerts 2>/dev/null | openssl x509 -noout -text | grep -E "Issuer|Subject|Not After"
# curl cho thông báo lỗi rõ ràng, dễ đọc
curl -v https://your-api-host.com 2>&1 | grep -E "SSL|certificate|issuer"
Kết quả sẽ cho bạn biết đang gặp phải cert tự ký, thiếu certificate trung gian, cert hết hạn, hay proxy công ty đang chặn. Cách xử lý phụ thuộc vào nguyên nhân — hãy kiểm tra trước khi vá.
Cách Sửa 1 — Thêm CA Certificate (Khuyến Nghị)
Chỉ cho Node.js biết CA nào cần tin tưởng. Lấy file CA certificate — hỏi đội IT, xuất từ browser, hoặc lấy từ server — rồi truyền trực tiếp vào request agent:
// Dùng module https
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
ca: fs.readFileSync('/path/to/ca-certificate.pem')
});
https.get({ hostname: 'your-api-host.com', path: '/', agent }, (res) => {
console.log('Status:', res.statusCode);
});
// Dùng axios
const axios = require('axios');
const https = require('https');
const fs = require('fs');
const httpsAgent = new https.Agent({
ca: fs.readFileSync('/path/to/ca-certificate.pem')
});
const client = axios.create({ httpsAgent });
await client.get('https://your-api-host.com/api/data');
// Dùng native fetch (Node.js 18+) — native fetch không hỗ trợ tùy chọn agent;
// dùng Agent của undici với dispatcher thay thế
import { Agent } from 'undici';
import { readFileSync } from 'fs';
const dispatcher = new Agent({
connect: {
ca: readFileSync('/path/to/ca-certificate.pem')
}
});
fetch('https://your-api-host.com/api/data', { dispatcher });
Để xuất CA cert từ Chrome hoặc Firefox: truy cập URL → nhấp vào ổ khóa → Xem Certificate → xuất dưới định dạng PEM.
Trên Linux, bạn có thể trỏ thẳng vào bundle CA của hệ thống thay vì file tùy chỉnh:
const agent = new https.Agent({
ca: fs.readFileSync('/etc/ssl/certs/ca-certificates.crt')
});
Cách Sửa 2 — Dùng NODE_EXTRA_CA_CERTS (Không Cần Sửa Code)
Không thể chỉnh sửa code ứng dụng? Dùng biến môi trường NODE_EXTRA_CA_CERTS. Node sẽ gắn thêm các certificate đó vào trust store tích hợp sẵn — các CA hiện có vẫn giữ nguyên, CA mới được thêm vào phía trên. Không có gì bị ảnh hưởng.
# Linux/macOS
export NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
node your-app.js
# Windows (Command Prompt)
set NODE_EXTRA_CA_CERTS=C:\certs\ca-certificate.pem
node your-app.js
# Trong file .env (dùng dotenv)
NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
Trong môi trường công ty, thêm dòng này vào shell profile (~/.bashrc, ~/.zshrc) hoặc biến môi trường CI/CD. Chỉ một dòng — mọi tiến trình Node trên máy đó đều tự động nhận cấu hình.
Cách Sửa 3 — SSL Inspection Của Công Ty (Môi Trường Có Proxy)
Đang dùng mạng qua proxy công ty? Khả năng cao proxy đang chặn toàn bộ traffic HTTPS và ký lại bằng root CA của công ty. Browser của bạn tin tưởng nó vì CA đó đã được cài vào OS trust store. Node không bao giờ kiểm tra ở đó.
Xuất root CA của công ty và trỏ NODE_EXTRA_CA_CERTS vào đó:
# macOS — xuất tất cả cert từ System keychain dưới định dạng PEM
security find-certificate -a -p /Library/Keychains/System.keychain > corporate-ca.pem
export NODE_EXTRA_CA_CERTS=corporate-ca.pem
# Ubuntu/Debian — dùng trực tiếp bundle của hệ thống
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# Windows — xuất từ cert store (PowerShell)
$certs = Get-ChildItem Cert:\LocalMachine\Root
$certs | ForEach-Object {
$_ | Export-Certificate -FilePath "$($_.Thumbprint).cer" -Type CERT
}
Cách Sửa 4 — Tắt Xác Minh (Chỉ Dùng Khi Phát Triển)
Đây là lối thoát khẩn cấp. Chỉ dùng trong môi trường dev cục bộ — không bao giờ dùng trên staging hay production. Tắt xác minh TLS đồng nghĩa với việc mọi certificate đều được chấp nhận, kể cả certificate giả mạo từ kẻ tấn công thực hiện MITM. Các công cụ kiểm tra bảo mật sẽ phát hiện điều này ngay lập tức.
// Ảnh hưởng toàn bộ request HTTPS trong tiến trình
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
// Hoặc giới hạn trong các request cụ thể với axios
const client = axios.create({
httpsAgent: new https.Agent({ rejectUnauthorized: false })
});
# Hoặc đặt trực tiếp khi khởi động
NODE_TLS_REJECT_UNAUTHORIZED=0 node your-app.js
Ít nhất hãy thêm một dòng log cảnh báo rõ ràng để biết khi nào flag này đang hoạt động:
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
console.warn('CẢNH BÁO: Đã tắt xác minh TLS. Không được dùng trên production!');
}
Kiểm Tra Sau Khi Sửa
Chạy script test tối giản này để xác nhận kết nối thực sự hoạt động từ đầu đến cuối:
// test-ssl.js
const https = require('https');
const fs = require('fs');
const options = {
hostname: 'your-api-host.com',
port: 443,
path: '/',
method: 'GET',
// Chỉ dùng nếu áp dụng Cách Sửa 1:
// agent: new https.Agent({ ca: fs.readFileSync('/path/to/ca.pem') })
};
const req = https.request(options, (res) => {
console.log(`THÀNH CÔNG: Status ${res.statusCode}`);
});
req.on('error', (e) => {
console.error(`THẤT BẠI: ${e.message}`);
});
req.end();
node test-ssl.js
# Kết quả mong đợi: THÀNH CÔNG: Status 200
Bài Học Rút Ra
- Ưu tiên dùng
NODE_EXTRA_CA_CERTStrước — an toàn, không cần thay đổi code, và hoạt động với mọi thư viện sử dụng TLS stack của Node. - Không bao giờ tắt xác minh TLS trên production —
rejectUnauthorized: falsethực chất là mở cửa cho tấn công MITM. Các công cụ quét bảo mật sẽ phát hiện ngay lần đầu. - Node.js bỏ qua OS trust store — các developer quen với browser hoặc runtime khác thường bất ngờ với điều này. Docker image và CI pipeline cần cấu hình CA tường minh.
- Trong Docker, tích hợp CA vào image:
COPY ca.pem /usr/local/share/ca-certificates/ca.crt && update-ca-certificates, sau đó đặtNODE_EXTRA_CA_CERTStrong entrypoint hoặc biến môi trường. - Nếu cert từng hoạt động nay đột nhiên lỗi, kiểm tra ngày hết hạn trước:
openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -dates. Certificate trung gian hết hạn thường không có cảnh báo và có thể làm sập nhiều dịch vụ cùng lúc.

