エラー修正: Node.js HTTPSリクエストでのUNABLE_TO_VERIFY_LEAF_SIGNATURE

intermediate🔒 SSL/TLS2026-05-14| Linux/macOS/Windows上のNode.js 14以降、https、axios、node-fetch、gotなどのライブラリでHTTPSリクエストを行う場合

Error Message

Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE
#nodejs#https#ssl#certificate-chain#intermediate-cert

エラーの概要

Node.jsアプリがHTTPSエンドポイントにアクセスすると、次のエラーで落ちます:

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)

深夜2時です。Chromeではそのエンドポイントは問題なく開けるし、手元のPCでcurlしても文句ひとつ言わないのに、Node.jsだけが頑なに接続を拒否します。証明書は有効です——目で確認できています。いったい何が起きているのでしょうか?

このエラーの本当の意味

これは証明書の有効期限切れではありません。自己署名証明書でもありません。問題は、Node.jsがサーバーの証明書からNode.jsが認識できるルートCAまでの信頼チェーンを構築できないことです。

十中八九、原因はシンプルです。サーバーがTLSハンドシェイク時にリーフ証明書を送信するものの、中間CA証明書を省略しているのです。中間証明書がなければチェーンは成立しません。チェーンがなければ接続できません。

ブラウザはこれをうまく処理します——Authority Information Access(AIA)を使って欠落している中間証明書を自動的に取得し、ローカルにキャッシュします。Node.jsにはその機能がありません。毎回、完全なチェーンが最初から提供される必要があります。

再現と診断

ステップ1 — サーバーのチェーンが壊れていないか確認する

サーバーに対してOpenSSLを直接実行します:

openssl s_client -connect yourdomain.com:443 -showcerts

正常なチェーンには少なくとも2つの証明書が表示されます:

Certificate chain
 0 s:CN=yourdomain.com
   i:CN=Some Intermediate CA
 1 s:CN=Some Intermediate CA
   i:CN=Root CA

壊れたチェーンには1つしか表示されません:

Certificate chain
 0 s:CN=yourdomain.com
   i:CN=Some Intermediate CA

リーフ証明書だけです。中間証明書がありません。これが原因です。

ステップ2 — Node.jsで失敗することを確認する

node -e "require('https').get('https://yourdomain.com', r => console.log(r.statusCode)).on('error', e => console.error(e.message))"

出力にUNABLE_TO_VERIFY_LEAF_SIGNATUREが表示されれば、原因が確定です。

解決策

解決策1 — サーバーを修正する(正しい対処法)

これはNode.jsのバグではなく、サーバーの設定ミスです。根本から修正しましょう。完全な証明書チェーンを送信するようサーバーを設定します。

Nginxの場合、証明書を1つのファイルにまとめ、ssl_certificateでそのファイルを指定します:

# Concatenate leaf + intermediate
cat your_domain.crt intermediate.crt > fullchain.pem

# In nginx.conf
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/your_domain.key;

Apacheの場合:

SSLCertificateFile /etc/ssl/certs/your_domain.crt
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt

Node.js HTTPSサーバーの場合:

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')  // add this line
}, app).listen(443);

サーバーをリロードして、openssl s_clientを再実行してください。チェーンの出力にリーフ証明書と中間証明書の両方が表示されるはずです。

解決策2 — Node.jsクライアント側でCA証明書を指定する

サーバーを管理できない場合はどうでしょうか?チェーンが壊れたサードパーティAPIを呼び出しているケースもあります。その場合、Node.jsに信頼するCA証明書を明示的に指定します:

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);
});

axiosを使っている場合も同様です。オプション名が違うだけです:

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
});

これは恒久的な修正ではなく、あくまで回避策です。サーバーチームに働きかけて、チェーンを正しく解決してもらいましょう。

解決策3 — システムのCAバンドルを更新する(Linuxサーバーの場合)

セットアップしたばかりのLinuxサーバーは、発行元のルートCAが含まれていない古いCAバンドルを持っていることがあります。手軽なアップデートで大抵解決します:

# 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は起動時にシステムのCAバンドルを読み込みます。更新後はコードを変更する必要はありません——プロセスを再起動するだけです。

やってはいけないこと

Stack Overflowにあふれているこの方法には手を出してはいけません:

// DO NOT DO THIS IN PRODUCTION
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// Same problem:
const agent = new https.Agent({ rejectUnauthorized: false });

証明書の検証を無効にしても問題は解決しません。TLS検証が完全に無効化され、アプリが中間者攻撃に対して無防備な状態になります。ローカルの使い捨てスクリプトならまだしも、本番環境では深刻なセキュリティインシデントになりかねません。

修正の確認

すべてが正常に動作していることを確認する3つのコマンドです:

# 1. Check the server sends the full chain
openssl s_client -connect yourdomain.com:443 -showcerts 2>&1 | grep -E '(subject|issuer|Certificate chain)'

# 2. Verify the chain validates against the system CA store
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /dev/null  console.log('OK:', r.statusCode)).on('error', e => console.error('FAIL:', e.message))"

OK: 200が表示されるはずです。あるいはそのエンドポイントが通常返すステータスコードが表示されるでしょう——大事なのは、エラーが消えていることです。

より詳細な監査を行うには、SSL Labsでサーバーをチェックしてみてください。欠落している中間証明書を明示的に指摘し、ブックマークしておく価値のあるチェーンの可視化も提供してくれます。

学んだこと

  • **ブラウザは嘘をつく。**ChromeやFirefoxは欠落している中間証明書を静かに取得してキャッシュします。ページは問題なく表示されているのに、Node.js、curl、そしてすべてのサーバー間通信クライアントが同じエンドポイントを拒否する、ということが起こり得ます。TLSのテストは常にopenssl s_clientで行いましょう——ブラウザのタブではありません。
  • **証明書の更新が最大のトリガー。**誰かがリーフ証明書を更新してコピーしたものの、中間証明書の再バンドルを忘れたのです。デプロイ手順書にopenssl s_clientによるチェーン確認を追加してください。5秒で完了し、毎回これを検知できます。
  • **クライアント側の回避策は匂いがする。**あるエンドポイントと通信するすべてのサービスにcaのオーバーライドを注入しているなら、それは技術的負債が蓄積しているサインです。チェーンはサーバー側で対処すべきです。チケットを起票しましょう。
  • **コンテナのベースイメージは古くなる。**1年前にnode:16からビルドしたDockerイメージ上のNode.jsアプリは、新しいルート証明書を信頼しない可能性があります。DockerfileにRUN apt-get install -y ca-certificatesを追加してリビルドしましょう——深夜2時に本番環境でこのバグを追いかけることにならないように。

Related Error Notes