クイック解決 (TL;DR)
このエラーは、ほとんどの場合、Nginxが HTTP しか待ち受けていないバックエンドサービスに対して HTTPS で通信しようとしていることを意味します。最も一般的な解決策は、proxy_pass のプロトコルを変更することです。
# 設定ファイル内の以下の箇所を探します:
proxy_pass https://127.0.0.1:8080;
# 以下のように変更します:
proxy_pass http://127.0.0.1:8080;
変更後、設定をテストしてNginxをリロードしてください:
sudo nginx -t
sudo systemctl reload nginx
詳細な原因
エラーメッセージ SSL_do_handshake() failed (SSL: error:1408F10B:SSL routines:ssl3_get_record:wrong version number) は、アップストリーム(バックエンド)サーバーから受信したデータに対してNginxが混乱していることを示しています。
proxy_pass https://... を使用すると、NginxはTLSハンドシェイクを開始します。まず、バックエンドに「Client Hello」パケットを送信します。バックエンドがプレーンなHTTPサーバー(SSL設定されていないローカルのNode.jsアプリ、Gunicorn、Dockerコンテナなど)である場合、TLSハンドシェイクを理解できません。「Server Hello」を返す代わりに、通常はプレーンテキストのHTTPエラーまたは標準レスポンスを返します。
Nginxはそのレスポンスの最初の数バイトをTLSバージョン番号として読み取ろうとします。バイナリのTLSバージョンヘッダーではなく「HTTP」という文字を検出するため、wrong version number エラーをスローします。これは事実上のプロトコルの不一致です。プレーンな英語しか理解できない相手に対して、暗号化されたスペイン語で話しかけているようなものです。
よくあるシナリオと解決策
シナリオ1: proxy_pass 内での誤った https:// の使用
NginxがSSL(SSL終端)を処理し、バックエンドアプリが同じマシンまたはプライベートネットワーク上で動作している場合、通常、Nginxとバックエンド間のSSLは不要です。多くの開発者が設定をコピー&ペーストし、プロトコルの変更を忘れてしまいます。
location /api/ {
# バックエンドがHTTPの場合、ここでエラーが発生します
proxy_pass https://backend_server;
proxy_set_header Host $host;
}
解決策: https を http に変更します。ローカルループバックや安全なVPC経由であれば、内部トラフィックは一般的に安全です。
シナリオ2: ポートの混同 (ポート443 vs 80)
正しいプロトコルを指定していてもポートが間違っている、あるいはその逆の場合があります。Nginxで https://your-backend:80 を指定すると、プレーンテキストを期待しているポートに対してハンドシェイクを実行しようとします。
解決策: バックエンドが実際にどのポートで待ち受けているかを確認してください。SSL証明書なしで8080番ポートを使用している場合は、http://127.0.0.1:8080 を使用します。もし本当にHTTPSである必要がある場合は、バックエンドに .crt および .key ファイルが正しく設定されていることを確認してください。
シナリオ3: upstreamブロックの不一致
upstream ブロックを使用する場合は、proxy_pass ディレクティブのプロトコルが、ブロック内にリストされているサーバーの機能と一致していることを確認してください。
upstream my_app {
server 10.0.0.5:5000;
}
server {
listen 443 ssl;
# ... ssl 設定 ...
location / {
# 'my_app' 内のサーバーがHTTPの場合、ここは http:// である必要があります
proxy_pass http://my_app;
}
}
確認手順
実際に何が起きているかを確認するには、Nginxを介さずに、サーバーのターミナルから curl を使用して直接バックエンドと通信してみます。
バックエンドがHTTPかどうかをテストする:
curl -I http://127.0.0.1:8080
これがHTTP 200または301を返す場合、バックエンドは間違いなくHTTPであり、proxy_pass には http:// を使用する必要があります。
バックエンドがHTTPSかどうかをテストする:
curl -Ik https://127.0.0.1:8080
これが機能し(ヘッダーが返される)、HTTPテストが失敗する場合、バックエンドは確かにHTTPSです。その場合、wrong version number エラーは、極端に古いTLSバージョンや証明書の誤設定が原因である可能性があります。
予防とプロのアドバイス
プロトコルの不一致をデバッグする際、本番環境での発生を防ぐために私が意識していることをいくつか紹介します。:
- **環境変数:** デプロイスクリプトで環境変数を使用して、バックエンドのプロトコル (`BACKEND_URL=http://...` vs `https://...`) を設定します。
- **一貫したロギング:** 常に `/var/log/nginx/error.log` を確認してください。複数のアップストリームがある場合、失敗した特定のアップストリームIPとポートが記録されているため、非常に重要です。
- **証明書の完全性:** バックエンドに対して意図的にSSLを使用しようとして失敗している場合は、証明書ファイルが破損していないか確認してください。サーバー間で証明書を移動する際、私はよく [ToolCraftのハッシュジェネレーター](https://toolcraft.app/en/tools/developer/hash-generator) を使って `.crt` ファイルのSHA-256ハッシュを生成します。送信元と送信先でハッシュを比較することで、`scp` や `rsync` 転送中にファイルが途切れたり壊れたりしていないことを確認できます。
参考文献
- `proxy_pass` に関する [Nginx ドキュメント](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass)
- SSL終端とエンドツーエンド暗号化の違いを理解する
- Nginx アップストリームエラーのデバッグ

