Node.js HTTPS リクエスト時の self-signed certificate in certificate chain エラーに対応する (https、fetch、Axios 対応)

Node.js でビルトインの https モジュール、fetch、サードパーティーでよく使われている axios パッケージで https インターネットアクセスする際に TLS インスペクションが入っている環境で発生する self-signed certificate in certificate chain エラーに対応する方法を紹介します。

やりたいこと

企業の環境等で TLS インスペクションが入っている環境において Node.js のビルトインの https モジュール等で https インターネットアクセスする際に self-signed certificate in certificate chain というエラーが発生することがあります。このエラーの対応方法をビルトインの https モジュール、fetch、サードパーティー「でよく使われている axios パッケージそれぞれで紹介します。

環境

OS
Microsoft Windows 10 22H2
Node.js
v22.6.0
axios
1.7.7

事象

例えば TLS インスペクションがある環境で以下のようなコードで Yes か No を返してくれる YESNO API を叩いてみます。

  • Node.js のビルトインの https モジュール
    import { get } from 'node:https';
    
    get('https://yesno.wtf/api', (res) => {
      let chunks = '';
      res.on('data', (chunk) => chunks += chunk);
      res.on('end', () => console.log(chunks));
    })
  • Node.js ビルトインの fetch
    const res = await fetch('https://yesno.wtf/api');
    const data = await res.json();
    console.log(data);
  • axios パッケージ
    import axios from 'axios';
    
    const res = await axios.get('https://yesno.wtf/api');
    console.log(res.data);

すると何れのパターンでも以下のようなエラーが発生します。

[cause]: Error: self-signed certificate in certificate chain
code: 'SELF_SIGNED_CERT_IN_CHAIN'

このエラーは TLS インスペクションが入っていることにより証明書検証でエラーとなっていることがほとんどです。 TLS インスペクションの簡単な説明と、後ほど使用する証明書の特定、取得方法を こちらの記事 で紹介していますので必要であればご参照ください。

対応方法

証明書検証をオフにする (非推奨)

Node.js では環境変数 NODE_TLS_REJECT_UNAUTHORIZED'0' を設定することで証明書検証をオフにすることができます。 ただしあらゆる HTTPS における証明書検証がなされなくなるので、この方法はおすすめしません。

NODE_TLS_REJECT_UNAUTHORIZED=value
If value equals '0', certificate validation is disabled for TLS connections. This makes TLS, and HTTPS by extension, insecure. The use of this environment variable is strongly discouraged.
Command-line API | Node.js v22.8.0 Documentation

一応設定方法について書いておきます。

  • PowerShell の場合
    $env:NODE_TLS_REJECT_UNAUTHORIZED = '0'
  • コマンドプロンプトの場合
    set NODE_TLS_REJECT_UNAUTHORIZED=0

で設定できます。

先ほどのコードを実行してみると、httpsfetchaxios 何れのパターンでも以下のワーニングが表示されますが、

(node:10476) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests 
insecure by disabling certificate verification.

API のレスポンスは受け取れていることがわかります。

{
  "answer": "yes",
  "forced": false,
  "image": "https://yesno.wtf/assets/yes/13-c3082a998e7758be8e582276f35d1336.gif"
}

上述した通りセキュリティ的によろしくないので、試した場合は NODE_TLS_REJECT_UNAUTHORIZED 環境変数を削除しておきましょう。

  • PowerShell の場合
    $env:NODE_TLS_REJECT_UNAUTHORIZED = $null
  • コマンドプロンプトの場合
    set NODE_TLS_REJECT_UNAUTHORIZED=

で削除できます。

環境変数で自己署名証明書を指定する

本質的にはプロキシの自己署名証明書を追加することがあるべき姿です。 環境変数 NODE_EXTRA_CA_CERTS で追加のルート証明書を指定することができます。 繰り返しになりますが、追加する証明書の特定、取得方法を こちらの記事 で紹介していますので必要であればご参照ください。

NODE_EXTRA_CA_CERTS=file
When set, the well known “root” CAs (like VeriSign) will be extended with the extra certificates in file. The file should consist of one or more trusted certificates in PEM format. A message will be emitted (once) with process.emitWarning() if the file is missing or malformed, but any errors are otherwise ignored.
(中略) The NODE_EXTRA_CA_CERTS environment variable is only read when the Node.js process is first launched. Changing the value at runtime using process.env.NODE_EXTRA_CA_CERTS has no effect on the current process.
Command-line API | Node.js v22.7.0 Documentation

Node.js を実行する場所からの相対パスで指定可能ですが、絶対パスの方がよいでしょう。

  • PowerShell の場合
    $env:NODE_EXTRA_CA_CERTS = ".\xxxx.pem"
  • コマンドプロンプトの場合
    set NODE_EXTRA_CA_CERTS=.\xxxx.pem

で設定できます。

Node.js https.Agent を使用する (https モジュール、Axios の場合)

ここまで紹介した証明書検証のオフやルート証明書の追加は https.Agent を使用することで、コード内でこれらを指定することができます。ただしこれができるのは Node.js の https モジュールや Axios を使用する場合で、fetch では使用できません。 fetch でコード内で指定する方法は後述します。

まず https.Agent を見ていきます。Node.js 公式ドキュメントの https.Agent の項では触れられていませんが、以下の例から https.Agent のコンストラクターオプションには tls.connect() のオプションも使用できることが伺えます。

Example using options from tls.connect():

const options = {
  hostname: 'encrypted.google.com',
  port: 443,
  path: '/',
  method: 'GET',
  key: fs.readFileSync('private-key.pem'),
  cert: fs.readFileSync('certificate.pem'),
};
options.agent = new https.Agent(options);

const req = https.request(options, (res) => {
  // ...
});

HTTPS | Node.js v22.8.0 Documentation

余談ですが、TypeScript の Node.js のビルトイン型セットである @types/nodeAgentOptions を見てみると、確かに tls.ConnectOptions が継承されています。

@types/node/https.d.ts
interface AgentOptions extends http.AgentOptions, tls.ConnectionOptions

さて、 tls.connect() のオプションを見てみると、証明書検証有無を指定できる rejectUnauthorized プロパティーが見つかります。

rejectUnauthorized
If not false, the server certificate is verified against the list of supplied CAs. An 'error' event is emitted if verification fails; err.code contains the OpenSSL error code. Default: true.
TLS (SSL) | Node.js v22.8.0 Documentation

さらに tls.createSecureContext() のオプションも含められる記載もあります。

tls.createSecureContext() options that are used if the secureContext option is missing, otherwise they are ignored.
TLS (SSL) | Node.js v22.8.0 Documentation

再度余談ですが、@types/node でもそのように定義されています。

@types/node/tls.d.ts
interface ConnectionOptions extends SecureContextOptions, CommonConnectionOptions

というわけで tls.createSecureContext() のオプションを見てみると ca プロパティーで証明書を指定できることがわかります。

ca | <string[]> | | <Buffer[]>
Optionally override the trusted CA certificates. Default is to trust the well-known CAs curated by Mozilla. Mozilla’s CAs are completely replaced when CAs are explicitly specified using this option. The value can be a string or Buffer, or an Array of strings and/or Buffers. Any string or Buffer can contain multiple PEM CAs concatenated together. The peer’s certificate must be chainable to a CA trusted by the server for the connection to be authenticated. When using certificates that are not chainable to a well-known CA, the certificate’s CA must be explicitly specified as a trusted or the connection will fail to authenticate. If the peer uses a certificate that doesn’t match or chain to one of the default CAs, use the ca option to provide a CA certificate that the peer’s certificate can match or chain to. For self-signed certificates, the certificate is its own CA, and must be provided. For PEM encoded certificates, supported types are “TRUSTED CERTIFICATE”, “X509 CERTIFICATE”, and “CERTIFICATE”. See also tls.rootCertificates.
TLS (SSL) | Node.js v22.8.0 Documentation

では https.Agent を使用して実装してみます。プロキシの自己署名証明書は pem 形式であらかじめ用意してください。 繰り返しになりますが、追加する証明書の特定、取得方法を こちらの記事 で紹介していますので必要であればご参照ください。

なお、証明書を使用せず証明書検証をスキップする場合は、以下のコードで ca: certrejectUnauthorized: false としてください。ただしこの方法はおすすめしません。

import { Agent, get } from 'node:https';
import { readFileSync } from 'node:fs';

const cert = readFileSync('cert_decryption_trusted.crt');

const agent = new Agent({ ca: cert });

get('https://yesno.wtf/api', 
  { agent }, 
  (res) => {
    let chunks = '';
    res.on('data', (chunk) => chunks += chunk);
    res.on('end', () => console.log(chunks));
  }
);

Axios の場合はオプションの httpsAgent プロパティーに https.Agent を渡すことで設定できます。

// `httpAgent` と `httpsAgent` は、それぞれ Node.js で http と https のリクエストを
// 実行するときに使用するカスタムエージェントを定義します。
// これにより、デフォルトでは有効になっていない `keepAlive` のようなオプションを追加できます。
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),

リクエスト設定 | Axios Docs

Axios での実装例はこちら。

import axios from 'axios';
import { Agent } from 'node:https';
import { readFileSync } from 'node:fs';

const cert = readFileSync('cert.pem');
const agent = new Agent({ ca: cert })

const res = await axios.get('https://yesno.wtf/api', { httpsAgent: agent });
console.log(res.data);

Undici の Agent を使用する (fetch の場合)

さて、fetch の場合はどうするか、ですが公式ドキュメントでは以下の通り、仕様はブラウザの fetch の互換実装である、としか書かれておらず、そのブラウザの fetch の使用には証明書検証スキップや追加証明書の指定をするオプションはありません。(RequestInit - Web API | MDN)

fetch
A browser-compatible implementation of the fetch() function.
Global objects | Node.js v22.8.0 Documentation

ここで Node.js の fetch がどう実装されているのかをお話ししますが、実は Node.js の低レベル API を使用して JavaScript で一から作られた別モジュール Undici (Node.js Undici) を内包して実装されています。「低レベル API を使用して」とは Node.js の http モジュールや https モジュールは使用していないということです。

さて、この Undici のドキュメントを見てみると以下のような例があります。

You can pass an optional dispatcher to fetch as:

import { fetch, Agent } from 'undici'

const res = await fetch('https://example.com', {
  // Mocks are also supported
  dispatcher: new Agent({
    keepAliveTimeout: 10,
    keepAliveMaxTimeout: 10
  })
})
const json = await res.json()
console.log(json)

Node.js Undici

ここで fetch のオプションに dispatcher というプロパティーがあることに注目です。このプロパティーはブラウザの fetch にはありません。

Undici のソースを見てみると WHATWG の RequestInit の仕様に沿って実装していると思いきや、最後に undici specific option とコメントされ dispatcher というプロパティーが追加されています。

// https://fetch.spec.whatwg.org/#requestinit
webidl.converters.RequestInit = webidl.dictionaryConverter([
  /* 中略 */
  {
    key: 'dispatcher', // undici specific option
    converter: webidl.converters.any
  }
])  

undici/lib/web/fetch/request.js at main · nodejs/undici · GitHub

Request オブジェクトも WHATWG の仕様に沿っていると思いきや、dispatcher の Symbol である kDispatcher プロパティーが追加されています。

// https://fetch.spec.whatwg.org/#request-class
class Request {
  // https://fetch.spec.whatwg.org/#dom-request
  constructor (input, init = {}) {
      /* 中略 */
    if (typeof input === 'string') {
      this[kDispatcher] = init.dispatcher
      /* 中略 */
    } else {
      this[kDispatcher] = init.dispatcher || input[kDispatcher]
      /* 中略 */
    }
    /* 中略 */
  }
  /* 中略 */
}

node/deps/undici/src/lib/web/fetch/request.js at main · nodejs/node · GitHub

上記の Undici ドキュメントでは dispatcher というプロパティーに Agent オブジェクトを渡していたので、試しに以下の通り Node.js の https.Agent を渡してみましたがダメでした。

import { Agent } from "node:https";
import { readFileSync } from 'node:fs';

const cert = readFileSync('cert.pem');
const agent = new Agent({ ca: cert })

const res = await fetch('https://yesno.wtf/api', { dispatcher: agent });
const data = await res.json();
console.log(data);

以下エラーがでます。https.Agentundice.Agent は別物のようです。

[cause]: TypeError: agent.dispatch is not a function

しょうがないので undici パッケージを追加します。

npm install undici

https.Agentundici.Agent に置き換えます。

import { Agent } from 'undici';
import { readFileSync } from 'node:fs';

const cert = readFileSync('cert_decryption_trusted.crt');

const agent = new Agent({
  connect: {
    ca: cert,
  },  
});

const res = await fetch('https://yesno.wtf/api', { dispatcher: agent });
const data = await res.json();
console.log(data);

これで無事にアクセスすることができました。ただ自己署名証明書の追加のために undici パッケージを追加するのもいけてないのと本番環境では証明書追加は不要であることがほとんどではと思いますので、fetch の場合は環境変数での対応が無難だと思います。

あとがき

TLS インスペクションが入った環境化における Node.js での HTTPS リクエスト時に発生する self-signed certificate in certificate chain エラーの対応方法について纏めました。企業環境は何かと不便ですが、ちゃんと原因と対応方法がわかっているとサクッと対応できるので本記事がお役に立てると幸いです。