ホスト名不一致(Hostname Mismatch)への対処法最近、マイクロサービスプロジェクトのローカルDocker-compose環境を構築している際に問題に直面しました。ある内部サービスが別のサービスからHTTPS経由でデータを取得しようとしたところ、Node.jsが即座に接続を遮断し、以下のエラーが発生しました。/p>```
Error: Hostname/IP does not match certificate's altnames: Host: api.example.com is not in the cert's altnames: DNS:example.com
Node.jsは、保護のために `ERR_TLS_CERT_ALTNAME_INVALID` エラーを発生させます。これはセキュリティ上のハンドシェイク(握手)のようなものだと考えてください。クライアントは、接続先のサーバーがその特定のホスト名を使用する権限を実際に持っているかどうかを検証します。もし `api.example.com` をリクエストしたのに、証明書が `example.com` しかカバーしていない場合、Node.jsは中間者攻撃を防ぐために接続を中止します。
## なぜ現代の環境では証明書が拒否されるのか歴史的に、ブラウザやランタイムは身元確認のために `Common Name (CN)` フィールドに依存していました。しかし、現在はそうではありません。Node.jsやChromiumベースのブラウザなどの現代的なシステムでは、現在 **Subject Alternative Name (SAN)** 拡張が必要になっています。ターゲットとなるホスト名がそのSAN配列に明示的にリストされていない場合、たとえCNが完全に一致していても接続は失敗します。
この問題は、主に以下の3つの一般的なシナリオで発生します。
- **ローカル開発:** `localhost` やカスタムの `.local` ドメインに自己署名証明書を使用している場合。- **サブドメインの増加:** 証明書を再発行せずに、インフラに `api.` や `staging.` を追加した場合。- **IPによる直接アクセス:** 証明書がDNS名のみを検証している環境で、`192.168.1.50` 経由で接続した場合。## 避けるべき「一時的な」解決策Stack Overflowには、セキュリティをバイパスするためにグローバル設定を切り替えるよう勧めるアドバイスが溢れています。以下のようなスニペットを推奨されることがあるかもしれません。
// 本番環境では絶対に使用しないでください process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
あるいは、リクエストロジック内での以下のようなコードです。
const agent = new https.Agent({ rejectUnauthorized: false });
これはやめてください。すべての証明書検証を無効にすることで、実質的にHTTPSから「S(Secure)」を取り除いてしまうことになります。通信が傍受に対して無防備になります。どうしても必要な場合に10秒程度の診断チェックとして使うのは構いませんが、真の解決策は証明書自体にあります。
## ステップ 1: SANを含む証明書を生成する開発でOpenSSLを使用する場合、SAN拡張を明示的に定義する必要があります。手順の再現性を保ち、タイポ(打ち間違い)を減らすために、設定ファイルを使用することをお勧めします。
### 1. 設定ファイル (server.conf) を作成する`server.conf` という名前のファイルを作成します。`api.example.com` をローカルのサービス名に置き換えてください。開発環境では `127.0.0.1` と `localhost` を含めておくのがベストプラクティスです。
[req] distinguished_name = req_distinguished_name x509_extensions = v3_req prompt = no
[req_distinguished_name] CN = api.example.com
[v3_req] subjectAltName = @alt_names
[alt_names] DNS.1 = api.example.com DNS.2 = localhost IP.1 = 127.0.0.1
### 2. 鍵と証明書を生成する設定ファイルを使用して、2048ビットのRSA鍵と365日有効な証明書を生成するために、以下のコマンドを実行します。
openssl req -x509 -nodes -days 365 -newkey rsa:2048
-keyout server.key
-out server.crt
-config server.conf
-extensions v3_req
これにより `server.crt` が生成されます。標準的な自己署名証明書とは異なり、これにはNode.jsが `api.example.com` の身元を確認するために必要なメタデータが含まれています。
## ステップ 2: 特殊なケースにおけるカスタム検証レガシーな環境でIPベースの接続を余儀なくされている一方で、証明書がドメイン名に固定されていることがあります。証明書を再発行できない場合は、セキュリティを完全に放棄することなく、`checkServerIdentity` を使って手動でそのギャップを埋めることができます。
この関数を使用すると、Node.jsがサーバーの身元をどのように検証するかを正確に定義できます。
const https = require('https'); const fs = require('fs'); const tls = require('tls');
const options = { hostname: '10.0.0.5', port: 443, ca: fs.readFileSync('server.crt'), checkServerIdentity: (host, cert) => { // 内部IPにアクセスしている場合、この証明書に対して手動で許可する if (host === '10.0.0.5') { return undefined; // 検証成功 } // それ以外の場合は、標準のNode.jsロジックを使用する return tls.checkServerIdentity(host, cert); } };
`undefined` を返すと、Node.jsに身元が検証されたことを伝えます。身元が正しくない場合は、関数は接続を終了させるために `Error` オブジェクトを返す必要があります。
## 検証:推測せず、検査するNode.jsアプリケーションを再起動する前に、証明書の生データを検査してください。これには5秒もかかりませんが、数時間にわたるデバッグを防ぐことができます。
openssl x509 -in server.crt -text -noout
**X509v3 Subject Alternative Name** セクションまでスクロールしてください。以下のようなリストが表示されるはずです。
X509v3 extensions: X509v3 Subject Alternative Name: DNS:api.example.com, DNS:localhost, IP Address:127.0.0.1
その特定の `DNS:` または `IP Address:` エントリが欠けている場合、Node.jsは引き続き `ERR_TLS_CERT_ALTNAME_INVALID` エラーで接続を拒否します。
## まとめチェックリスト- **ワイルドカード:** ローカル開発クラスター内のすべてのサブドメインをカバーするには、`DNS.1 = *.example.test` を使用します。- **CAチェーン:** SANを修正した後に `UNABLE_TO_VERIFY_LEAF_SIGNATURE` が発生する場合は、Node.jsリクエストの `ca` オプションにルート証明書を渡しているか確認してください。- **OSの信頼設定:** macOSでは、ローカルブラウザの警告を止めるために、`server.crt` を「キーチェーンアクセス」にドラッグして「常に信頼」に設定する必要がある場合があります。

