Sửa Lỗi DEPTH_ZERO_SELF_SIGNED_CERT trong Node.js Khi Gọi HTTPS API Nội Bộ

intermediate🌐 Networking2026-07-04| Node.js 14+ trên Linux/macOS/Windows, dùng https module, fetch hoặc axios để gọi API nội bộ có chứng chỉ TLS tự ký

Error Message

Error: self signed certificate (code: 'DEPTH_ZERO_SELF_SIGNED_CERT')
#nodejs#https#ssl#certificate#fetch#axios

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_SIGNATURESELF_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_CERTS là 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: false vào codebase dùng chung. Một pre-commit hook sẽ phát hiện ra ngay: grep -r "rejectUnauthorized.*false" src/.

Related Error Notes