Node.js HTTPSリクエストで「Error: unable to get local issuer certificate」を修正する

intermediate💚 Node.js2026-03-20| Node.js(全バージョン)、Windows/macOS/Linux、SSL検査を行う企業ネットワーク、自己署名証明書

Error Message

Error: unable to get local issuer certificate
#nodejs#ssl#https#certificate#tls

エラーの内容

Node.jsでfetchaxioshttps、またはgotを使ってHTTPSリクエストを送ると、次のエラーが発生します:

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)

または以下のいずれかのバリアント:

Error: self-signed certificate in certificate chain
Error: certificate has expired
Error: UNABLE_TO_GET_ISSUER_CERT_LOCALLY

リクエストは完全に失敗します。Node.jsのTLSスタックがサーバーの証明書を検証しようとしたものの、署名者を確認できず、接続を切断したのです。

根本原因

Node.jsはMozillaのCAリストから作られた独自の証明書ストアを持っています。OSのトラストストアは使用しません。そのため、NodeのバンドルにないCAが署名した証明書(企業の中間CA、内部PKI、自己署名証明書など)をサーバーが提示すると、Nodeはハンドシェイクを拒否します。

同じマシンでブラウザやcurlは正常に動作するため、開発者がこの動作に驚くことがよくあります。それらはOSのトラストストアを使用しますが、Node.jsは使いません。

よくある発生ケース:

  • SSLインスペクション付きの企業ネットワーク(プロキシがHTTPSトラフィックを傍受し、独自のルートCAで再署名する)
  • 自己署名または独自発行の証明書を使用した内部API
  • ローカルのHTTPS開発環境
  • チェーン内のどこかで期限切れになった中間証明書

まずデバッグする

推測で判断しないでください。次のコマンドを実行して、サーバーが提示している証明書チェーンを正確に確認しましょう:

# 証明書チェーンの詳細(発行者、サブジェクト、有効期限)を表示
openssl s_client -connect your-api-host.com:443 -showcerts 2>/dev/null | openssl x509 -noout -text | grep -E "Issuer|Subject|Not After"
# curlは明確でわかりやすいエラーメッセージを表示する
curl -v https://your-api-host.com 2>&1 | grep -E "SSL|certificate|issuer"

出力を見れば、自己署名証明書なのか、中間証明書の欠落なのか、期限切れ証明書なのか、企業プロキシによる傍受なのかがわかります。修正方法は原因によって異なるため、対処前に必ず確認してください。

修正方法1 — CA証明書を追加する(推奨)

信頼するCAをNode.jsに伝えます。CA証明書ファイルを入手し(ITチームに依頼するか、ブラウザからエクスポートするか、サーバーから取得)、リクエストのエージェントに直接渡します:

// 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);
});
// 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');
// ネイティブfetchを使用する場合(Node.js 18以降) — ネイティブfetchはagentオプションをサポートしていないため、
// 代わりにundiciのAgentをdispatcherとして使用する
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 });

ChromeやFirefoxからCA証明書をエクスポートするには:該当URLにアクセス → 鍵アイコンをクリック → 証明書を表示 → PEM形式でエクスポート。

Linuxでは、カスタムファイルの代わりにシステムのCAバンドルを直接指定できます:

const agent = new https.Agent({
  ca: fs.readFileSync('/etc/ssl/certs/ca-certificates.crt')
});

修正方法2 — NODE_EXTRA_CA_CERTSを設定する(コード変更不要)

アプリケーションのコードを変更できない場合は、NODE_EXTRA_CA_CERTS環境変数を使用します。Nodeはその証明書を組み込みのトラストストアに追加します。既存のCAはそのまま残り、新しい証明書がその上に追加されます。既存の動作は壊れません。

# Linux/macOS
export NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
node your-app.js
# Windows(コマンドプロンプト)
set NODE_EXTRA_CA_CERTS=C:\certs\ca-certificate.pem
node your-app.js
# .envファイル内(dotenvを使用)
NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem

企業環境では、シェルプロファイル(~/.bashrc~/.zshrc)またはCI/CDの環境変数に追加してください。1行追加するだけで、そのマシン上のすべてのNodeプロセスが自動的に読み込みます。

修正方法3 — 企業SSLインスペクション(プロキシ環境)

企業プロキシの内側にいる場合、すべてのHTTPSトラフィックが傍受され、企業のルートCAで再署名されている可能性が高いです。そのCAはOSのトラストストアにインストールされているためブラウザは信頼しますが、Node.jsはそこを参照しません。

企業のルートCAをエクスポートし、NODE_EXTRA_CA_CERTSで指定します:

# macOS — システムキーチェーンからすべての証明書をPEM形式でエクスポート
security find-certificate -a -p /Library/Keychains/System.keychain > corporate-ca.pem
export NODE_EXTRA_CA_CERTS=corporate-ca.pem
# Ubuntu/Debian — システムバンドルをそのまま使用
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# Windows — 証明書ストアからエクスポート(PowerShell)
$certs = Get-ChildItem Cert:\LocalMachine\Root
$certs | ForEach-Object {
    $_ | Export-Certificate -FilePath "$($_.Thumbprint).cer" -Type CERT
}

修正方法4 — 検証を無効化する(開発環境限定)

これは最後の手段です。ローカル開発環境のみで使用し、ステージングや本番環境では絶対に使わないでください。TLS検証を無効にすると、攻撃者がMITM攻撃で使う偽造証明書を含め、あらゆる証明書が通過します。セキュリティ監査では即座に指摘されます。

// プロセス内のすべてのHTTPSリクエストに影響する
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// またはaxiosで特定のリクエストにのみ適用
const client = axios.create({
  httpsAgent: new https.Agent({ rejectUnauthorized: false })
});
# または起動時にインラインで設定
NODE_TLS_REJECT_UNAUTHORIZED=0 node your-app.js

最低限、このフラグが有効になっていることが一目でわかるよう、目立つログを追加してください:

if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
  console.warn('WARNING: TLS verification disabled. Do not use in production!');
}

修正の確認

この最小限のテストスクリプトを実行して、接続が正常に動作するか確認してください:

// test-ssl.js
const https = require('https');
const fs = require('fs');

const options = {
  hostname: 'your-api-host.com',
  port: 443,
  path: '/',
  method: 'GET',
  // 修正方法1を使用する場合のみ:
  // agent: new https.Agent({ ca: fs.readFileSync('/path/to/ca.pem') })
};

const req = https.request(options, (res) => {
  console.log(`SUCCESS: Status ${res.statusCode}`);
});

req.on('error', (e) => {
  console.error(`FAILED: ${e.message}`);
});

req.end();
node test-ssl.js
# 期待される出力: SUCCESS: Status 200

まとめ

  • まずNODE_EXTRA_CA_CERTSを試してください — セキュアで、コード変更不要、Node.jsのTLSスタックを使うすべてのライブラリで機能します。
  • 本番環境でTLS検証を無効にしないでくださいrejectUnauthorized: falseは事実上MITM攻撃を招く設定です。セキュリティスキャナーは最初のパスで検出します。
  • Node.jsはOSのトラストストアを無視します — ブラウザや他のランタイムに慣れた開発者はこの動作に驚くことがよくあります。DockerイメージやCIパイプラインでは明示的なCA設定が必要です。
  • Dockerの場合、CAをイメージに組み込みます:COPY ca.pem /usr/local/share/ca-certificates/ca.crt && update-ca-certificatesを実行し、エントリポイントまたは環境変数にNODE_EXTRA_CA_CERTSを設定します。
  • 以前動いていた証明書が突然失敗する場合、まず有効期限を確認してください:openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -dates。中間証明書は静かに期限切れとなり、複数のサービスを同時にダウンさせることがよくあります。

Related Error Notes