デプロイ後の予期せぬトラブル
SSLの導入や、ロードバランサー配下へのアプリ移行が無事に終わった直後、ログを確認すると400エラーが急増していることに気づくかもしれません。ユーザーがポート443を指定しながら、誤ってhttp://ではなくhttps://と入力してしまった場合、Nginxは次のようなそっけないエラーメッセージを返します:
400 Bad Request: The plain HTTP request was sent to HTTPS port
これは、Nginxがポート443で暗号化されたSSL/TLSハンドシェイクを厳格に待機しているために発生します。もし代わりに標準の暗号化されていないGETリクエストを受信すると、Nginxはどう処理すべきか判断できません。意図を推測するのではなく、セキュアなソケットを保護するために接続を終了します。
30秒でできる解決策
これを修正するには、この特定のエラーをインターセプトし、ユーザーを正しいセキュアなURLにリダイレクトするようNginxに指示します。SSLトラフィックを処理するserverブロック内に、以下の行を追加してください:
server {
listen 443 ssl;
server_name example.com;
# 内部エラー497をHTTPS版にリダイレクト
error_page 497 https://$host$request_uri;
# ... 残りのSSL設定
}
ファイルを保存した後、nginx -s reloadで変更を適用します。Nginxは、HTTPとHTTPSのポート不一致に対して、内部コード497を専用に使用しています。このディレクティブはそのイベントをキャッチし、セキュアなプロトコルへの301スタイルのリダイレクトを実行します。
なぜこのエラーが発生するのか?
プロトコルの厳密さが根本的な原因です。listen 443 ssl;を定義すると、Nginxは接続の最初の1バイトがTLSハンドシェイクの開始であることを期待します。レガシースクリプトや手動のURL入力によって、そのポートにプレーンテキストでGET / HTTP/1.1が送信されると、Nginxはそれをゴミデータとして認識します。通信の途中で接続をアップグレードすることはできないため、400レスポンスをトリガーします。
通常、以下の3つの特定のシナリオでこの現象が発生します:
- **手動のURL入力ミス:** ユーザーがブラウザに`http://yourdomain.com:443`と明示的に入力した場合。
- **ロードバランサーの設定不一致:** AWS ALBやHAProxyなどのアップストリームサービスが、プレーンなHTTPでNginxと通信するように設定されているにもかかわらず、NginxのSSL有効ポートをターゲットにしている場合。
- **ハードコードされたAPIクライアント:** 古いモバイルアプリやIoTデバイスが、最近ポート443に移行したサービスに対して`http://`をハードコードしてアクセスしようとしている場合。
高度な設定方法
方法1:error_page 497ディレクティブ(ベストプラクティス)
これはエラーを発生源で処理するため、最も効率的なアプローチです。接続が切断されるのを防ぎ、ユーザーの特定のパスやクエリ文字列を保持します。
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# プレーンテキストからSSLポートへの不一致を処理
error_page 497 https://$host$request_uri;
location / {
proxy_pass http://localhost:8080;
}
}
方法2:プロキシとロードバランサーのループを解決する
もしNginxインスタンスが、SSL終端(SSL Termination)を行うロードバランサーの背後にある場合、セットアップが変わります。このケースでは、ロードバランサーが証明書を処理し、プレーンなHTTP経由でNginxと通信します。もしNginxのリスニングポートで依然としてsslが有効になっていると、ロードバランサーからのトラフィックを拒否してしまいます。
解決策: Nginxからsslディレクティブを削除し、X-Forwarded-Protoヘッダーに依存して元のリクエストがセキュアであったかどうかを判断します。
server {
listen 80; # ロードバランサーはポート80でNginxと通信
server_name example.com;
# ロードバランサーの内部IP範囲を信頼する (例: 10.0.0.0/8)
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;
location / {
proxy_pass http://app_backend;
}
}
ソリューションのテスト
リダイレクトをテストする際、ブラウザのキャッシュは紛らわしい場合があります。ターミナルでcurlを使用して、Nginxが不一致に対して正しく応答しているか確認してください。
1. エラー状態をシミュレートする:
curl -I http://yourdomain.com:443
修正前、このコマンドはHTTP/1.1 400 Bad Requestを返します。方法1を適用した後は、サイトのhttps://版を指し示す301 Moved Permanentlyまたは302 Foundが表示されるはずです。
2. Nginxの構文を確認する:
nginx -t
必ずこのチェックを実行してください。セミコロンが1つ抜けているだけで、リロード時にウェブサーバー全体がダウンする可能性があります。
Dockerとネットワークにおけるよくある落とし穴
- **Dockerポートマッピング:** Dockerの実行コマンドで`-p 443:80`を使用している場合、外部のSSLトラフィックを内部の非SSLポートにマッピングしていることになります。そのコンテナ内のNginxがポート80でSSLを期待している場合、不一致は避けられません。内部ポートと外部ポートのロジックを一貫させてください。
- **HSTSの影響:** 通常、HSTS(Strict Transport Security)はブラウザがこのミスを犯すのを防ぎます。しかし、`curl`やPostman、バックエンド間のAPIコールには効果がありません。
- **ファイアウォールの検査:** 一部の企業向けファイアウォールは、ディープパケットインスペクション(DPI)を実行します。TLSハンドシェイクが削除または変更されると、Nginxは受信データをプレーンテキストとして認識する可能性があります。

