WSLで npm install を叩くと、SELF_SIGNED_CERT_IN_CHAIN で落ちる。証明書もちゃんと用意して、.bashrc に NODE_EXTRA_CA_CERTS まで設定したのに、なぜか通らない。──これは設定ミスでもnpmのバグでもない。WSLで動いていたnpmが実は"Windows版"で、Linuxのパスに置いた証明書を、そもそも読めていなかっただけだ。
社内プロキシやゼロトラスト環境で開発している人なら、一度は踏む落とし穴だと思います。私が実際にハマって、原因を突き止めて、直すまでの過程をそのまま置いておきます。
大前提:npmはどうやって証明書を検証しているか
npmがレジストリ(https://registry.npmjs.org)からパッケージを取るとき、相手のサーバーは「私は本物です」という証明書を提示します。Node.jsは手元の信頼できる認証局(CA)のリストと照合し、正しく署名されていれば通信を許可する。これがHTTPSの基本です。
ところが社内プロキシやゼロトラスト製品は、セキュリティ検査のために通信を一度復号して中継します。このときnpmが受け取るのは本物の証明書ではなく、プロキシが独自に発行し直した証明書です。その発行元(独自CA)はNode.js標準の信頼リストに入っていないので、「知らない発行元だ」と弾かれる。
だから、その独自CAを「信頼していいよ」とNode.jsに教える必要があります。それが環境変数 NODE_EXTRA_CA_CERTS の役割です。標準のCAリストに加えて、指定したCAも信頼するようになります。
export NODE_EXTRA_CA_CERTS=~/corp-proxy.pem
これを .bashrc に書いておけば、ターミナルを開くたびに効く。──はずでした。
「証明書を読めない」の正体
設定は正しいのに、npm ping(レジストリへの疎通確認)は無情に落ちます。
$ npm ping
npm notice PING https://registry.npmjs.org/
npm error code SELF_SIGNED_CERT_IN_CHAIN
npm error errno SELF_SIGNED_CERT_IN_CHAIN
npm error request to https://registry.npmjs.org/-/ping failed, reason: self-signed certificate in certificate chain
npm error A complete log of this run can be found in: C:\Users\<username>\AppData\Local\npm-cache\_logs\...
ここに決定的な手がかりがあります。最終行のログパスが C:\...(Windows側) になっている。WSLで作業しているはずなのに、なぜWindowsのパスが出るのか。ここが真犯人でした。
原因① WSLで叩いたnpmが、実はWindows版だった
which で実体を確かめると、正体が見えます。
$ which -a npm
/mnt/c/Program Files/nodejs//npm # ← Cドライブ = Windows側
$ file "/mnt/c/Program Files/nodejs/node.exe"
node.exe: PE32+ executable for MS Windows # ← 正真正銘のWindowsバイナリ
WSLは既定でWindowsのPATHをLinux側に引き継ぎます。そのため npm と打つと、/mnt/c/Program Files/nodejs(= C:\Program Files\nodejs)にあるWindows版のnpmが引っかかって起動していたのです。「PATHの共有」「exeをWindowsに委譲するinterop」「Cドライブを見せる /mnt/c」──この3点セットで、WSLからWindowsのNode.jsが動く。便利な仕組みですが、今回はこれが仇になりました。
原因② Windows版nodeはLinuxのパスを読めない
ここで、さっき設定した証明書のパスを思い出してください。
NODE_EXTRA_CA_CERTS = /home/<username>/corp-proxy.pem ← Linuxのパス
これを受け取るのはWindows版のNode.jsです。そしてWindowsは、/home/... というLinux流のパスを理解できない。正確には、先頭のスラッシュを「今のドライブのルート」と解釈し、C:\home\<username>\corp-proxy.pem というまったく別の場所を探しに行ってしまう。当然そんなファイルは無いので、証明書は読み込まれない。
WSL上のファイルは、Windowsから見ると別の表記になります。wslpath で変換すると一目瞭然です。
$ wslpath -w /home/<username>/corp-proxy.pem
\\wsl.localhost\Ubuntu\home\<username>\corp-proxy.pem
つまりWindows版nodeに届けるには、/home/... ではなく \\wsl.localhost\... や C:\... の形にしてやらないといけない。
結果:設定は正しいのに、渡す相手とパス形式が噛み合っていない
証明書ファイルは存在する。中身も正しい。.bashrc の設定も間違っていない。それでも動かなかったのは、「渡す相手(Windows node)」と「パスの形式(Linuxパス)」がすれ違っていたからです。私たちは正しいメモを、それを読めない相手に手渡していた。
だからこそ:解決の手順
原因がパスのすれ違いなら、直し方は「Windows版nodeが読める場所と方法で渡す」の一択です。npmの cafile 設定を使います。
① 証明書をWindowsから見えるパスに置く
cp ~/corp-proxy.pem "/mnt/c/Users/<username>/corp-proxy.pem"
# = C:\Users\<username>\corp-proxy.pem に配置される
② Windows側の .npmrc に cafile を登録する
npm config set cafile "C:/Users/<username>/corp-proxy.pem"
パスは /(フォワードスラッシュ)で書いておくと、バックスラッシュのエスケープで悩まずに済みます。strict-ssl は true のまま、つまり証明書の検証は有効なままで大丈夫です。
③ npm ping で検証する
$ npm ping
npm notice PING https://registry.npmjs.org/
npm notice PONG 366ms
PONG が返ってきました。証明書検証を通過して、レジストリと通信できています。長かった。
まとめ:証明書は"渡す相手のOS"に合わせて置く
今回の一件は、突き詰めれば 「パスのすれ違い」問題でした。
- 症状は
SELF_SIGNED_CERT_IN_CHAIN。一見すると証明書の問題に見える。 - でも真因は「WSLで動いていたnpmが実はWindows版で、Linuxパスの証明書を読めなかった」こと。
- 直し方は、渡す相手(Windows node)とパス形式(Windowsパス)を揃えるだけ。
そして、もっと根本的な教訓もあります。WSLで開発するなら、Linux版のnodeをちゃんと入れる(nvmなど)か、あるいはNode非依存の公式ネイティブインストーラを使う。どちらかにしておけば、「WSLなのに実体はWindows」という"すれ違い"自体が起きません。混在が、すべての混乱の入り口だった。
付録:ついでに学んだこと
このデバッグの過程で拾った小ネタも置いておきます。
-
.bashrcを変えたらsource ~/.bashrc。今開いているターミナルに即反映したいならこれ。新しく開くターミナルなら起動時に自動で読まれるので不要。 - Node.jsはbashの上で動いているわけではない。bashは「起動係」で、実行の瞬間に環境変数を渡しているだけ。だから渡した相手がLinuxかWindowsかで、こういう差が出る。
- Windowsの Docker は WSL 2 が土台。「DockerのためにWSLを入れる」は遠回りでも何でもなく、そのものズバリ正しい判断。
- claudecodeの
npm installと公式インストーラの違い。前者はNode.jsという外部ランタイム前提のJSプログラム。後者はランタイム同梱の単体バイナリで、今回のような"どのNode問題"を最初から回避できる。ツールを入れるなら、Node非依存の公式インストーラの方が事故が少ない。
同じ環境で消耗している人の助けになれば。「WSLなのに、動いていたのはWindowsだった」──たったこれだけの話だ。
推奨タグ: WSL npm Node.js 証明書 Windows