チュートリアル

NGINXリバースプロキシの設定・検証・トラブルシューティング

Ubuntu/DebianでNGINXリバースプロキシを構築し、バックエンド応答や転送ヘッダーを検証する方法を解説。502/504、proxy_passのパス、Socks5.IOのアウトバウンドプロキシとの違いも確認します。

Valerie QuinnValerie Quinn約14分で読める

NGINXをリバースプロキシとして使うには、管理下のサーバー設定からアプリケーションのバックエンドへリクエストを転送し、アプリが信頼するリクエストヘッダーを定義したうえで、設定を検証してNGINXを再読み込みします。nginx -tの成功やウェルカムページの表示だけでは、バックエンドまで正しく転送できたとは判断できません。NGINX経由の応答本文とログも確認してください。

この記事では、Ubuntu/Debian系のローカル環境にHTTPの検証用構成を作ります。NGINXは127.0.0.1:8080でリクエストを受け、127.0.0.1:3000で動くデモアプリへ転送します。公開ドメインは不要で、既定サイトも変更せず、外部公開プロキシも作成しません。公開HTTPS環境を構築する前に、まずローカルで検証してください。

リバースプロキシはアプリへ入ってくる通信を処理します。一方、Socks5.IOはアプリが許可された外部接続を行う際に検討する別のアウトバウンドプロキシです。通信方向と設定箇所を混同しないようにしましょう。

以下のコマンドはLinuxサーバー向けで、Windows PowerShell用ではありません。原稿に記録されたテスト結果を掲載していますが、この日本語版の作成時にコマンドは再実行していません。原稿のローカルテストでは、Ubuntuへのインストール、systemdサービス、公衆DNS/TLS/ファイアウォール、実際のSocks5.IO経路は検証していません。

NGINXのリバースプロキシはどのように動作しますか?

NGINXはクライアント接続を受け付け、設定に応じて仮想ホストとlocationを選び、対象の上流アプリケーションへリクエストを送ります。アプリの応答はNGINXを通じてクライアントへ返されるため、クライアントがバックエンドのポートやアドレスを直接知る必要はありません。

texttext
リクエスト:クライアント -> NGINXの待受 -> アプリケーションのバックエンド
レスポンス:クライアント <- NGINXの待受 <- アプリケーションのバックエンド

この検証環境:
curl -> 127.0.0.1:8080、Hostはproxy-demo.test -> 127.0.0.1:3000
inbound-and-outbound

リバースプロキシは、複数のパスへのルーティング、クライアントTLSの終端、複数バックエンドへの分散、意図的なキャッシュ制御などに使われます。これらは個別の設定が必要な機能であり、proxy_passを追加するだけで自動的に有効になるものではありません。リバースプロキシを導入しても、アプリケーションの脆弱性が解消されるわけではなく、WAFが標準で提供されたり、別経路からオリジンサーバーへ到達できなくなったりするわけでもありません。

リバースプロキシとフォワードプロキシの違い

比較項目 本記事のNGINXリバースプロキシ アプリケーションのアウトバウンドプロキシ
最初に接続する側 アプリケーションへアクセスするクライアント 外部の情報源へアクセスするアプリケーション
接続先 管理下にあるバックエンドアプリ 対応するアウトバウンドゲートウェイと接続先
設定する主体 アプリ運用者がNGINXを設定 リクエストを送るアプリ/クライアントがプロキシを設定
成功から確認できること 指定した待受を経由して、想定したバックエンドに届いたこと 特定のクライアント接続が検証済みの経路を使ったこと

フォワードプロキシを使うと、接続先から見える送信元アドレスが変わる場合があります。ただし、これは通信全体の匿名性を保証するものではありません。リバースプロキシ、アウトバウンドプロキシサービス、接続先はそれぞれ通信の一部を観測できます。また、プロキシの利用によってデータへのアクセス許可が得られるわけではありません。

詳細はNGINX公式のリバースプロキシガイドを参照してください。

Ubuntu/DebianでNGINXリバースプロキシを設定する方法

使い捨ての開発サーバー、または利用が承認されたステージング環境を用意し、sudo権限を確認します。ここではsystemd管理下のUbuntu/Debianパッケージ構成と、/etc/nginx/sites-availableおよびsites-enabledを使います。ほかのディストリビューション、コンテナ、マネージド環境では、設定の読み込み先やサービス操作が異なることがあります。

既存の本番サイトを上書きしないでください。ローカルの3000番と8080番ポートが空いているか確認します。使用中の場合は別のポートを選び、設定とコマンドのすべてで同じ値に変更してください。

1. ツールをインストールし、既存サービスを確認する

bashbash
sudo apt update
sudo apt install nginx python3 curl
nginx -v
sudo nginx -t
sudo systemctl status nginx --no-pager
sudo ss -ltnp '( sport = :3000 or sport = :8080 )'

パッケージのインストールによって既定サービスが起動する場合があります。共有サーバーで作業する前に、既存サイトへの影響を確認してください。NGINXが停止している場合は、理由を調べてから、必要な場合にsudo systemctl start nginxを実行します。検証環境を作るためにファイアウォールを無効化したり、別アプリのプロセスを停止したりしないでください。

参考:UbuntuのNGINXインストール手順。

2. 応答を識別できるバックエンドを起動する

一般ユーザーが所有する作業ディレクトリに、次のコードをproxy_demo_backend.pyという名前で保存します。このデモは診断用エンドポイントを1つだけ提供し、ループバックアドレスにのみバインドします。

pythonpython
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit

class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def do_GET(self):
        if urlsplit(self.path).path != "/healthz":
            self.send_error(404)
            return
        payload = {
            "backend": "proxy-demo-backend",
            "path": self.path,
            "host": self.headers.get("Host"),
            "x_real_ip": self.headers.get("X-Real-IP"),
            "x_forwarded_for": self.headers.get("X-Forwarded-For"),
            "x_forwarded_proto": self.headers.get("X-Forwarded-Proto"),
            "forwarded": self.headers.get("Forwarded"),
            "request_id": self.headers.get("X-Request-ID"),
        }
        body = json.dumps(payload).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format, *args):
        pass

if __name__ == "__main__":
    ThreadingHTTPServer(("127.0.0.1", 3000), Handler).serve_forever()

ターミナルAで起動し、プロセスを実行したままにします。

bashbash
python3 proxy_demo_backend.py

ターミナルBからバックエンドへ直接リクエストします。

bashbash
curl --noproxy '*' --fail-with-body --silent --show-error \
  http://127.0.0.1:3000/healthz

成功条件: JSONに"backend": "proxy-demo-backend"と"path": "/healthz"が含まれます。この直接接続では転送ヘッダーを送っていないため、関連項目は通常nullです。このPythonサーバーは学習用であり、本番アプリケーションサーバーや実環境の依存サービスの稼働確認には使えません。

3. NGINXに独立した検証用サイトを追加する

sudoedit /etc/nginx/sites-available/proxy-demoで新しいファイルを作成します。同名ファイルがある場合は内容を確認し、他の設定を上書きせず別名を使ってください。以下は完全な設定例です。このディストリビューションでは、有効化したサイトファイルが既存のhttpコンテキスト内で読み込まれます。

nginxnginx
# /etc/nginx/sites-available/proxy-demo
# この設定片は既存のhttpコンテキスト内に置きます。トップレベルには置きません。
log_format proxy_demo '$request_id $remote_addr $request_method $uri '
                      'status=$status upstream=$upstream_addr '
                      'upstream_status=$upstream_status '
                      'upstream_time=$upstream_response_time total=$request_time';

server {
    listen 127.0.0.1:8080 default_server;
    server_name _;
    access_log off;
    return 404;
}

server {
    listen 127.0.0.1:8080;
    server_name proxy-demo.test;

    access_log /var/log/nginx/proxy-demo.access.log proxy_demo;
    error_log /var/log/nginx/proxy-demo.error.log warn;
    add_header X-Request-ID $request_id always;

    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Request-ID $request_id;
    proxy_set_header Forwarded "";

    proxy_connect_timeout 3s;
    proxy_read_timeout 15s;
    proxy_send_timeout 15s;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

上流はローカルアプリであり、アウトバウンドプロキシゲートウェイではありません。この単純なlocationでは、URIを付けないproxy_passが受信したパスを維持します。デフォルトサーバーは、この専用ループバック待受に届く未知のホスト名を拒否します。他の待受ポートやサイトの設定には影響しません。

config-routing

HTTP/1.1を明示し、インストール済みバージョンの既定値に依存しない設定にしています。タイムアウト値は検証用です。proxy_read_timeoutとproxy_send_timeoutは連続する処理間の待機時間を扱い、アプリケーション全体の処理期限ではありません。調整する際はNGINX proxyモジュールのリファレンスを確認してください。

4. サイトを有効化し、設定を検査して再読み込みする

同じ名前のファイルやリンクがsites-enabledにないことを確認してから実行します。

bashbash
sudo ln -s /etc/nginx/sites-available/proxy-demo /etc/nginx/sites-enabled/proxy-demo
sudo nginx -t

シンボリックリンクの作成コマンドを繰り返したり、既存リンクを上書きする強制オプションを付けたりしないでください。設定テストが失敗した場合は、エラーを修正してから次へ進みます。成功時は通常、構文が正しくテストに成功した旨が表示されます。ただし、確認しているのは設定の読み込みであり、バックエンドへの接続ではありません。

bashbash
sudo systemctl reload nginx
sudo systemctl status nginx --no-pager

正常稼働中のサービスにサイト設定を反映する場合、通常は再起動ではなく再読み込みを使います。詳しくはNGINXビギナーズガイドを参照してください。コマンドが成功しても、リクエストが想定した仮想ホストに届いたとは限りません。

syntax-and-scope

NGINXリバースプロキシの動作を確認する方法

バックエンドへの直接接続、設定したNGINX待受、レスポンス本文、関連ログの順に検証します。nginx -tだけでは、リクエストがアプリまで到達したか分かりません。

設定した仮想ホスト経由でバックエンドを確認する

bashbash
curl --noproxy '*' --fail-with-body --silent --show-error --include \
  --resolve proxy-demo.test:8080:127.0.0.1 \
  http://proxy-demo.test:8080/healthz

--resolveはDNSやhostsファイルを変更せず、このリクエストに限って接続先を指定します。--noproxyは、ローカル環境に設定されている別のフォワードプロキシを経由しないための指定です。

成功条件:

  • HTTPステータスが200で、本文にproxy-demo-backendが含まれる。
  • pathが/healthz、hostがproxy-demo.testであり、このHTTP検証環境ではx_forwarded_protoがhttpになっている。
  • x_real_ipとx_forwarded_forが、クライアントから実際に接続してきたループバックアドレスを示す。利用者が送った値がそのまま採用されていない。
  • レスポンスヘッダーのX-Request-IDが、バックエンドJSONと専用アクセスログのrequest_idに一致する。

Hostには$hostを設定しているため、クライアントのポート番号は保持されません。アプリが絶対URLを生成する場合は、正規のホスト名とポート番号の方針を別途設計してください。この診断例が、すべてのフレームワークのURL生成に適合するわけではありません。

backend-response

転送ヘッダーの信頼境界を確認する

意図的に偽の転送ヘッダーを付けて、もう一度リクエストします。

bashbash
curl --noproxy '*' --fail-with-body --silent --show-error \
  --resolve proxy-demo.test:8080:127.0.0.1 \
  -H 'X-Forwarded-For: 198.51.100.77' \
  -H 'X-Real-IP: 198.51.100.77' \
  -H 'X-Forwarded-Proto: https' \
  -H 'Forwarded: for=198.51.100.77;proto=https' \
  http://proxy-demo.test:8080/healthz

成功条件: バックエンドが受け取るクライアントアドレスは引き続き実際のループバックアドレスで、x_forwarded_protoはhttp、forwardedはnullになります。この例は、クライアントと直接接する境界でNGINXが転送ヘッダーを上書きする設定です。実際のアプリでは、管理下にあるプロキシホップとヘッダーだけを信頼してください。これらのヘッダーは認証情報ではありません。

header-trust-boundary

手前に信頼済みCDNやロードバランサーがある場合は、この直接接続向け設定を見直します。利用可能な場合はreal-IPモジュールで信頼する送信元範囲とヘッダーを指定し、クライアントがその境界を迂回できないようにしてください。インターネット上のすべてのアドレスが申告するクライアントIDを信頼してはいけません。外部接続がHTTPSで内部ホップがHTTPの場合も、信頼方針なしに元のスキームを書き換えないでください。

ログを確認し、検証環境で障害を再現する

bashbash
sudo tail -n 20 /var/log/nginx/proxy-demo.access.log
sudo tail -n 20 /var/log/nginx/proxy-demo.error.log

専用アクセスログには上流アドレス、ステータス、応答時間が記録されるため、200だけを見るより多くの情報を得られます。クエリ文字列は記録しない設定ですが、パス、アドレス、エラーログには機密情報が含まれる可能性があります。アクセス権と保存期間を制限し、公開用の証拠は必ずマスキングしてください。

request-log-correlation

この検証環境に限り、ターミナルAでCtrl+Cを押してデモバックエンドを停止し、先ほどのcurlコマンドを再実行します。バックエンドが停止していれば、リクエストは502となり、上流接続エラーがログに記録されるはずです。python3 proxy_demo_backend.pyで再起動し、200に戻ることを確認します。本番アプリケーションを停止して障害を再現しないでください。

failure-and-recovery

想定外のHostがアプリに到達せず、404になることも確認できます。

bashbash
curl --noproxy '*' --silent --show-error --include \
  -H 'Host: other.example' http://127.0.0.1:8080/healthz

この検証で確認できるのは、ローカル待受のHostルーティングです。TLSや公開ネットワークからの到達性は確認できません。curl -IはHEADリクエストを送りますが、この小さなデモアプリが実装しているのはGETです。HEADへの未対応をリバースプロキシの障害と誤認しないでください。

/api/を追加する前にproxy_passのパス変換を確認する

/api/healthz?check=1を受信する場合、単純なプレフィックスlocationでもproxy_pass末尾のURIによってバックエンドのパスが変わります。

locationと上流の指定 バックエンドが受け取るパス
location /api/、proxy_pass http:// 127.0.0.1:3000; /api/healthz?check=1
location /api/、proxy_pass http:// 127.0.0.1:3000/; /healthz?check=1

この2つは比較例です。同じserver内に、同じlocationを両方貼り付ける指示ではありません。テスト用設定に一方だけを追加し、継承されるヘッダー設定を維持したうえでテストと再読み込みを行います。プレフィックスを削る設定では、このバックエンドの/healthzに到達します。維持する設定では、バックエンドが404を返します。変数、正規表現、rewriteを使う場合は、表をそのまま当てはめず個別にパスを検証してください。

proxy-pass-paths

本番公開前の確認項目:HTTPS、オリジン、特殊な通信

ローカルのループバック検証は公開HTTPS環境の構築ではありません。アプリを公開する前に、次の項目を確認してください。

  1. 承認済みの公開仮想ホストを作成します。 実際のドメインと必要な待受ポートを設定します。ローカル検証用サイトは分離して残すか、不要になったことを確認してから廃止します。
  2. 有効な証明書を設定します。 組織で承認された手順に従って証明書と秘密鍵を用意し、クライアントTLSを設定・検証します。HTTPSの接続先が正常に動作してからHTTPをリダイレクトしてください。存在しない証明書パスをコピーしたり、証明書検証を無効にしたりしないでください。
  3. バックエンドへのアクセスを制限します。 同一ホストならバックエンドをループバックにバインドし、別ホストならプライベートなプロキシ経路だけに制限します。コンテナ内の127.0.0.1はそのコンテナを指し、別コンテナやホストを自動的に指すわけではありません。
  4. 信頼できないネットワーク上の上流接続も保護します。 上流TLSのサーバー名、証明書検証、信頼するCAバンドルを設定します。公開側がHTTPSでも、バックエンドへのHTTP通信が暗号化されるわけではありません。詳しくはNGINXの上流HTTP通信保護ガイドを参照してください。
  5. アプリ固有の信頼・認証・タイムアウト方針を設定します。 リダイレクト、Secure Cookie、リクエストボディ、公開クライアント経路をテストし、/healthzだけで判断しないでください。

WebSocketを使う場合は、対象locationでUpgradeヘッダーを明示的に処理し、実際に接続を検証します。詳細はNGINXのWebSocketドキュメントを参照してください。ストリーミングやSSEでは、バッファリング方針を個別に検討する場合があります。この検証設定ではキャッシュを有効にしていません。認証済みレスポンスやユーザーごとに異なる内容へ、一律にキャッシュを適用しないでください。

負荷分散では、upstreamグループにアプリケーションのエンドポイントを登録し、セッション状態や障害時の動作を別途設計します。アドレスを1つ追加しただけでは高可用性を実現したことになりません。住宅プロキシのノードをアプリケーションサーバーと同じようにupstreamグループへ追加することもできません。

NGINXからSocks5.IO経由でアウトバウンド通信できますか?

通信方向に応じて構成要素を分けてください。

texttext
インバウンド:クライアント -> NGINXリバースプロキシ -> アプリケーション
アウトバウンド:アプリケーション -> 対応するSocks5.IOフォワードプロキシ -> 許可された外部接続先

通常のHTTPリバースプロキシ設定におけるproxy_passはSOCKS5クライアントではありません。商用HTTPプロキシを指定しても、そのフォワードプロキシが求めるリクエスト形式やHTTPS CONNECTのネゴシエーションに自動対応するわけではありません。upstreamグループを使っても、プロトコル対応は追加されません。proxy_bindは利用可能なローカル送信元アドレスを選ぶ設定であり、リモートの住宅IPを購入・借用する機能ではありません。

通常、アプリのアウトバウンドプロキシは実際に外部通信を行うHTTP/ブラウザークライアント側で設定するか、対応するアウトバウンドコンポーネントを別途設計します。NGINXには製品やモジュール構成によってフォワードプロキシ機能がありますが、このリバースプロキシ検証では確認しておらず、あらゆる外部プロキシに接続できる汎用アダプターでもありません。

許可された連携で住宅回線からのアウトバウンド通信が必要な場合は、そのアウトバウンドコンポーネントでSocks5.IOの動的住宅プロキシを検討できます。現在のエンドポイント、プロトコル、セッション設定はアカウントで確認してください。住宅プロキシの設定ガイドでは、クライアント側の設定と検証方法を説明しています。プロキシの認証情報を公開NGINX設定や診断レスポンスに記載しないでください。

プロトコルは実際に通信するクライアントに合わせて選びます。SOCKSプロキシとHTTPプロキシの違いも参考になります。SOCKS5と記載されたエンドポイントでも、すべてのブラウザーやリバースプロキシが認証付きSOCKSクライアントとして使えるわけではありません。無関係なシェルではなく、実際のアプリからアウトバウンド経路を検証し、接続先へのアクセス許可も別途確認してください。

たとえば、社内レポートAPIへのインバウンド通信をNGINXで受け、バックグラウンドワーカーが許可された外部の商品フィードを確認する構成が考えられます。NGINXはAPIの入口を担当し、ワーカーはアウトバウンド経路、利用枠、認証情報を別途設定します。/healthzのリクエストが成功しても、ワーカーの出口IPが確認できたことにはなりません。これは構成例であり、顧客事例やSocks5.IOの接続ベンチマークではありません。

NGINXのトラブルシューティング:502/504、誤ったページ、クライアントIP

エラーが発生したら、クライアント、NGINXの待受、上流バックエンドの順に設定、応答、ログを確認します。原因を特定してからネットワークルールの変更を検討してください。

症状 まず確認する項目 避けること
nginx -tが失敗する エラーのファイル名と行番号、設定コンテキスト、重複待受、構文、権限 エラーを無視して再読み込みする。秘密情報を含むnginx -Tの出力を公開する
502 Bad Gateway バックエンドの待受、上流プロトコル、接続拒否や無効な応答。エラーログも確認 すべての502をファイアウォールの遮断と決めつける
504 Gateway Timeout どのホップがタイムアウトしたか、バックエンドの遅延、該当するタイムアウト設定 遅い処理を調べずにすべてのタイムアウトを延ばす
ウェルカムページや予期しない200が返る 宛先アドレス、Host、選択された待受、アプリの応答本文 どの200応答も設定したルートの成功とみなす
/api/が404を返す プレフィックスの維持/置換とアプリ側ルート 理由を調べずにスラッシュを削除・追加する
アプリがNGINXのアドレスを認識する 信頼するプロキシ/ヘッダー設定、バックエンドへの直接到達可否 訪問者が送った任意のX-Forwarded-Forを信頼する
リダイレクトループやスキーム違い TLS終端の段数、信頼するスキーム、アプリの正規URL設定 クライアント由来のX-Forwarded-Protoを無条件に信頼する
ローカルでは成功するが公開リクエストが失敗する DNS、選択されたIP/待受、承認済みのFW/セキュリティグループルール、TLS ファイアウォールを無効にしたり、3000番ポートを公開したりする

タイムアウトだけではファイアウォール遮断と判断できません。各ホップをテストし、ルールを変更する前にポリシーの根拠を確認してください。この検証では両方の待受をループバックに限定しているため、公開用UFWルールは不要です。公開ポート80/443は別途確認し、SSH接続を誤って遮断しないようにします。

まとめ:インバウンド経路とアウトバウンド経路を分けて検証する

NGINXリバースプロキシでは、待受の選択、上流へのルーティング、アプリが信頼する情報を分けて設計します。識別できるバックエンドを用意し、設定をテストして安全に再読み込みした後、実際の応答と関連ログを確認してください。HTTPSや特殊プロトコル、運用制御は、それぞれ明確な合格条件を決めて追加します。

アウトバウンドプロキシは別に設定します。Socks5.IOは、対応クライアントが許可された接続先へ通信する際の選択肢ですが、バックエンドサーバーの代わりにはなりません。リバースプロキシのヘルスチェックからアプリの外向き経路を確認することもできません。

よくある質問

アプリケーション宛てのリクエストを受け取り、設定したバックエンドへ転送します。ルーティング、TLS終端、ログ収集、特定の性能制御などに使えますが、それぞれ個別の設定と検証が必要です。proxy_passだけでキャッシュ、高可用性、WAFが有効になるわけではありません。本番適用前にバックエンドの到達経路とアプリの動作を確認してください。

最新記事

グローバル プロキシ ネットワークの図

グローバル プロキシ ネットワークに接続 - すべての接続がより速く、より安定します

無料で始める