Chuyện Gì Đã Xảy Ra
Bạn đang gọi một endpoint HTTPS nội bộ — máy chủ staging, một microservice trên localhost:8443, hoặc một công cụ nội bộ của công ty đặt sau proxy — và Node.js ném ra lỗi:
Error: self signed certificate (code: 'DEPTH_ZERO_SELF_SIGNED_CERT')
Có ba tình huống gần như lúc nào cũng kích hoạt lỗi này: chuyển đổi một dịch vụ nội bộ từ HTTP sang HTTPS, khởi động môi trường dev local với chứng chỉ tự ký, hoặc làm việc sau proxy của công ty có chức năng chấm dứt và ký lại lưu lượng TLS.
Mỗi trường hợp đều có cách xử lý gọn gàng. Không cần phải tắt xác minh SSL hoàn toàn.
Nguyên Nhân Gốc Rễ
Node.js xác thực toàn bộ chuỗi chứng chỉ trên mỗi request HTTPS. Chứng chỉ tự ký không có chuỗi — nó tự ký chính mình. Không có Certificate Authority (CA) đáng tin cậy nào đứng sau, TLS stack của Node sẽ từ chối ngay lập tức.
Mã lỗi DEPTH_ZERO_SELF_SIGNED_CERT rất cụ thể: chứng chỉ ở độ sâu zero (chính chứng chỉ của server) là tự ký và không có trong CA store tích hợp của Node. Cần phân biệt với UNABLE_TO_VERIFY_LEAF_SIGNATURE và SELF_SIGNED_CERT_IN_CHAIN — những lỗi đó liên quan đến chứng chỉ trung gian cao hơn trong chuỗi. Ở đây, chứng chỉ server chính là vấn đề trực tiếp.
Tái Hiện Lỗi
Tạo một chứng chỉ tự ký để kiểm tra:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'
Khởi động một HTTPS server nhanh:
const https = require('https');
const fs = require('fs');
https.createServer(
{ key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') },
(req, res) => res.end('ok')
).listen(8443);
Gọi nó từ một tiến trình Node.js thứ hai:
const https = require('https');
https.get('https://localhost:8443', res => console.log(res.statusCode));
// Error: self signed certificate (code: 'DEPTH_ZERO_SELF_SIGNED_CERT')
Cách Sửa 1: Trust Chứng Chỉ Cụ Thể (Khuyến Nghị)
Báo cho Node.js tin tưởng đúng một chứng chỉ này. Mọi request khác vẫn đi qua xác thực TLS bình thường — không có gì thay đổi.
Dùng module https tích hợp sẵn
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
ca: fs.readFileSync('/path/to/cert.pem') // chứng chỉ tự ký của server
});
https.get('https://internal-api.company.local/health', { 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/cert.pem')
});
const client = axios.create({ httpsAgent });
await client.get('https://internal-api.company.local/health');
Dùng native fetch (Node 18+)
const fs = require('fs');
const { fetch, Agent } = require('undici');
const dispatcher = new Agent({
connect: {
ca: fs.readFileSync('/path/to/cert.pem')
}
});
const res = await fetch('https://internal-api.company.local/health', { dispatcher });
console.log(res.status);
Cách Sửa 2: Thêm Chứng Chỉ vào CA Store của Node qua Biến Môi Trường
Khi nhiều service trong cùng một tiến trình cần dùng chung chứng chỉ, bỏ qua việc cấu hình agent cho từng lần gọi và dùng NODE_EXTRA_CA_CERTS. Không cần thay đổi code.
NODE_EXTRA_CA_CERTS=/path/to/cert.pem node app.js
Hoặc thêm vào file .env (dùng với dotenv):
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/internal-ca.pem
Đặc biệt hữu ích trong môi trường chia sẻ: thêm biến môi trường vào workflow GitHub Actions, file Docker Compose, hoặc cấu hình CI, và mọi lập trình viên cũng như mỗi lần pipeline chạy đều tự động tin tưởng đúng chứng chỉ — không cần thay đổi code.
Cách Sửa 3: Xuất và Trust Chứng Chỉ Toàn Hệ Thống
Khi nhiều công cụ cần tin tưởng cùng một chứng chỉ — curl, script Python, không chỉ Node.js — hãy thêm vào OS trust store.
Ubuntu/Debian
sudo cp cert.pem /usr/local/share/ca-certificates/internal-api.crt
sudo update-ca-certificates
RHEL/CentOS/Fedora
sudo cp cert.pem /etc/pki/ca-trust/source/anchors/internal-api.crt
sudo update-ca-trust
macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain cert.pem
Một lưu ý: Node.js mặc định không đọc OS trust store trừ khi bạn đang dùng bản build đã được distro vá (một số gói Debian/Ubuntu có bật tính năng này). Riêng với Node, NODE_EXTRA_CA_CERTS đáng tin cậy hơn và có tính di động cao hơn.
Những Gì KHÔNG Nên Làm
Bạn sẽ thấy gợi ý này ở khắp nơi trên mạng:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // KHÔNG bao giờ làm thế này trên production
Hoặc với axios:
const agent = new https.Agent({ rejectUnauthorized: false }); // cũng nguy hiểm không kém
Cả hai tùy chọn này đều tắt xác thực chứng chỉ TLS cho toàn bộ tiến trình Node.js — không chỉ riêng request này. Ứng dụng của bạn sẽ âm thầm chấp nhận bất kỳ chứng chỉ nào, kể cả chứng chỉ giả mạo từ kẻ tấn công MITM. Dùng để test local trong 5 phút thì được. Còn lại đều là lỗ hổng bảo mật thực sự. Hãy dùng cách sửa với chứng chỉ cụ thể ở trên cho bất cứ thứ gì đưa lên production.
Xác Minh Bản Sửa Lỗi
Chạy lại lệnh gọi HTTPS — bạn sẽ nhận được 200 (hoặc bất cứ gì endpoint trả về) thay vì lỗi. Để kiểm tra chứng chỉ mà server đang thực sự trình bày:
openssl s_client -connect internal-api.company.local:443 -showcerts 2>/dev/null | openssl x509 -noout -text | grep -E 'Issuer|Subject|Not After'
Issuer và Subject giống nhau? Đó là chứng chỉ tự ký. Lấy chứng chỉ trực tiếp bằng lệnh:
openssl s_client -connect internal-api.company.local:443 2>/dev/null | openssl x509 > server.pem
Sau đó dùng server.pem làm giá trị ca trong bất kỳ cách sửa nào ở trên. Xác nhận Node.js tin tưởng nó:
NODE_EXTRA_CA_CERTS=server.pem node -e "
const https = require('https');
https.get('https://internal-api.company.local/health', r => console.log('OK:', r.statusCode))
.on('error', e => console.error('FAIL:', e.message));
"
Bài Học Rút Ra
- Chứng chỉ tự ký hoàn toàn ổn cho các service nội bộ — chỉ cần phân phối và tin tưởng chứng chỉ đó một cách tường minh thay vì bỏ qua hoàn toàn việc kiểm tra.
NODE_EXTRA_CA_CERTSlà cách sửa ít xâm lấn nhất cho môi trường CI/CD và Docker — một biến môi trường, không cần thay đổi code.- Nếu bạn kiểm soát service nội bộ, hãy cân nhắc chuyển sang Let's Encrypt hoặc một CA nội bộ thay vì dùng chứng chỉ tự ký cho từng service. Một CA nội bộ nghĩa là chỉ cần tin tưởng một root cert là đủ cho tất cả service của bạn, thay vì một chứng chỉ riêng cho mỗi service.
- Không bao giờ commit
rejectUnauthorized: falsevào codebase dùng chung. Một pre-commit hook sẽ phát hiện ra ngay:grep -r "rejectUnauthorized.*false" src/.

