「プロキシ」と「リバースプロキシ」は名前が似ていますが、置く人も、守る対象も逆です。フォワードプロキシはクライアント側が置き、リバースプロキシはサーバー側が置きます。この1点さえ押さえておけば、設定ファイルがどこにあるのか、障害のときにどこから疑うのか、アクセスログのIPアドレスをなぜそのまま信用してはいけないのかが、一本の線でつながります。
この記事ではまず RFC の定義で両者を整理し、次に実際に本番サイトのレスポンスヘッダーを見てリバースプロキシの痕跡を確認します。さらに20行ほどのプロキシを書いて X-Forwarded-For がどう変化するかを実測し、最後に502/504の切り分けやリダイレクトループなど、実務でハマる点をまとめます。
結論:どちら側に置くかが逆
| フォワードプロキシ(いわゆるプロキシ) | リバースプロキシ | |
|---|---|---|
| 置く人 | クライアント側(社内ネットワークの管理者・利用者) | サーバー側(サイトの運営者) |
| 誰の代理か | クライアントの代理として外に出ていく | サーバーの代理としてリクエストを受ける |
| 相手から見た姿 | 多数の利用者がまとめられた1つの送信元 | そのサイト本体(オリジンに見える) |
| 設定の置き場所 | ブラウザ・OS・PACファイルなどクライアント側 | サーバー前段の設定(nginx等)やCDNの管理画面 |
| 存在に気づくか | 利用者は自分で設定するので知っている | 利用者は普通、存在に気づかない |
| 主な目的 | 出口の制御・フィルタリング・監査・匿名化 | TLS終端・キャッシュ・負荷分散・防御 |
HTTPの仕様である RFC 9110 は、通信の途中に入る装置(中間装置)を3つに分類しています。
- proxy:クライアントが選んだ、メッセージを転送するエージェント(3.7節)
- gateway(別名 reverse proxy):外向きの接続に対してはオリジンサーバーとして振る舞い、受け取ったリクエストを内側の別のサーバーへ転送する中間装置(3.7節)
- tunnel:2つの接続の間をメッセージを変えずに中継するだけの存在。動き出したあとは通信の当事者とみなされない(3.7節)
注目したいのは、RFCがリバースプロキシを gateway と呼び、「外向きの接続に対してはオリジンサーバーとして振る舞う」と定義していることです。RFCは「オリジンサーバーに適用されるHTTPの要件は、すべてゲートウェイの外向き通信にも適用される」とまで書いています。クライアントから見れば、リバースプロキシはそのサイトそのものだということです。この後の実測でも、まさにそのとおりの挙動が観測できます。
フォワードプロキシ:クライアントが選んで置く
RFCの定義にあるとおり、プロキシはクライアントが選ぶものです。だから設定はクライアント側にあります。「自分の環境だけ特定のサイトに繋がらない」というトラブルで、まずブラウザやOSのプロキシ設定、PACファイルを疑うのはこのためです。
何のために置くのか
- 出口の一点集約:外部への通信を1か所に通し、許可カテゴリや宛先で遮断する
- 記録:誰がいつどこへアクセスしたかのログを残す(監査・インシデント調査の基礎データ)
- 検査:ダウンロードファイルのマルウェアスキャンなど
- キャッシュ:同じコンテンツを複数の利用者で共有し、回線を節約する(回線が細かった時代の主目的)
- 送信元IPの隠蔽:外部から見える送信元をプロキシのIPに統一する
通信を止めるという点では ファイアウォールの基本 と目的が重なりますが、レイヤーが違います。ファイアウォールは主にIPアドレスとポートで通す・通さないを判断し、フォワードプロキシはHTTPの中身を理解したうえでURLやメソッド単位の判断ができます。両方を組み合わせて「外に出るときは必ずプロキシ経由、それ以外の直接通信はファイアウォールで遮断」という構成が定番です。
HTTPSの中身は見えない:CONNECT というトンネル
HTTPのリクエストならプロキシは中身を読めますが、HTTPSは暗号化されています。ではどうやって中継するのか。ここで使われるのが CONNECT メソッドです。
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
Proxy-Authorization: basic ...クライアントは接続したいホストとポートだけを書いた形式(authority-form)でトンネルの開設を要求します。ポートの省略はできません。2xx系の応答が返るとプロキシはトンネルモードに入り、以降は中身を解釈せずにTCPのバイト列を双方向に流すだけになります。つまり暗号化された中身はプロキシからは見えません。
企業がHTTPSの中身までフィルタしたい場合に「TLSインスペクション」(社内の認証局証明書を端末に配布し、プロキシでいったん復号して検査し、再暗号化する)が必要になるのは、この制約があるためです。証明書の検証がどう成立しているかは HTTPSとTLSの仕組み で扱っています。会社のPCだけ証明書の発行者が社内CAになっている、という現象の正体はこれです。
なお、認証が必要なプロキシは 407 Proxy Authentication Required を返し、クライアントは Proxy-Authorization ヘッダーを付けて再送します。401(サーバー自身の認証)と紛らわしいですが、407が返ったら相手はサーバーではなく途中のプロキシだと判断できます。
MDNは、CONNECTの接続先を既知のポートや許可リストに制限すべきだと注意しています。誰でも使えるオープンプロキシを放置すると、スパムの中継のような踏み台に使われるためです。内部にあるプロキシが外から使える状態になっていると、外部からの攻撃で内部リソースへ到達する経路にもなります。この構図は SSRF(サーバーサイドリクエストフォージェリ) と本質的に同じ問題です。
リバースプロキシ:サーバー側が黙って置く
リバースプロキシはクライアントが選びません。DNSがリバースプロキシを指しているので、利用者は「サイトに接続した」としか思っていません。それでいて、実際にリクエストを最初に受け取っているのはアプリケーションではなくこの前段です。
アプリの前に1台置くだけで、次のことがまとめて手に入ります。
- TLS終端:証明書の管理と更新をここに集約する。背後のアプリはHTTPだけ喋ればよくなる
- キャッシュと静的配信:画像やビルド済みHTMLを前段が返し、アプリに触れさせない
- 負荷分散・冗長化:複数のアプリサーバーへ振り分け、応答しなくなったものを外す
- ルーティング:
/apiはアプリへ、それ以外は静的ファイルへ、のようにパスで振り分ける - 防御の一枚目:レート制限、WAF、セキュリティヘッダーの付与(HTTPセキュリティヘッダー)
- 圧縮・プロトコル変換:外はHTTP/2やHTTP/3、内側はHTTP/1.1、といった使い分け(HTTPバージョンの違い)
実測:このサイトのレスポンスにも痕跡がある
言葉で説明するより、実物を見たほうが早いです。本サイト(Vercelでホスティング)のページに curl でヘッダーだけを取得すると、次の応答が返りました(2026年8月30日実測、抜粋)。
$ curl -sSI https://secutils.jp/learn/network/dns-basics
HTTP/1.1 200 OK
Cache-Control: public, max-age=0, must-revalidate
Age: 1206
Etag: "cb0352485eefbaa78e9eb7ea5f58f464"
Server: Vercel
X-Vercel-Cache: HIT
X-Vercel-Id: kix1::wbkhp-1788056830566-4b2d8503aef5読み取れることが3つあります。
Server: Vercel── 応答を返しているのは、私が書いたアプリケーションではなく前段のプラットフォームです。RFCの言う「外向きにはオリジンサーバーとして振る舞う」がそのまま現れています。X-Vercel-Cache: HITとAge: 1206── 約20分前に生成された応答がキャッシュから返っています。このリクエストではアプリのコードは1行も動いていません。RFC 9111 はAgeを「オリジンサーバーが応答を生成または検証してから経過したと送信側が推定する秒数」と定義しています(5.1節)。X-Vercel-Id: kix1::…── 応答した拠点を示す識別子です。同じ時刻に旧ドメイン(secutils.vercel.app)へリクエストしたときはhnd1::…という別拠点で、しかも 308 Permanent Redirect が前段から返っていました。リダイレクトのためにアプリまで到達する必要すらないわけです。
ここで言うキャッシュは、RFC 9111 が共有キャッシュと呼ぶものです。同じ仕様は、利用者ひとり専用のプライベートキャッシュ(ブラウザのキャッシュなど)と対比して、共有キャッシュを「複数の利用者のために応答を再利用して保存するもので、通常は中間装置の一部として配置される」と定義しています(1.3節)。「誰に配られても問題ない応答か」を意識しないといけないのは、この共有という性質から来ています。後述するキャッシュ事故の話につながります。
前段を挟むと、クライアントの情報が消える
リバースプロキシを挟むと、アプリから見た接続元は常にプロキシのIPアドレスになります。素朴にソケットの接続元を取得すると、全アクセスが同じIPに見えてしまいます。
そこで、消えてしまう情報を前段がヘッダーに詰めて渡す慣習が生まれました。
| ヘッダー | 渡すもの | 位置づけ |
|---|---|---|
X-Forwarded-For | 元のクライアントのIPアドレス | 事実上の標準(仕様書はない) |
X-Forwarded-Proto | クライアントが使ったプロトコル(http / https) | 同上 |
X-Forwarded-Host | クライアントが要求した元のホスト名 | 同上 |
Forwarded | 上記をまとめた標準形(for= / by= / proto= / host=) | RFC 7239 で標準化 |
Via | 経路上のプロキシ自身の情報 | RFC 9110。クライアントではなくプロキシについての情報 |
標準は Forwarded ですが、実際の現場では X-Forwarded-For が圧倒的に使われています。RFC 7239 自身も、標準化以前から X-Forwarded-For などの非標準ヘッダーが使われてきたと認めたうえで、相互運用性のための標準形を定義する、という立て付けになっています。
実験:20行のプロキシで挙動を確かめる
「X-Forwarded-For は偽装できる」という話は有名ですが、実際にどう見えるのかを確かめたことがある人は多くないと思います。この記事のために、最小のリバースプロキシとオリジンサーバーをNode.jsで書いて手元で動かしました。プロキシの中核はこれだけです。
const proxy = http.createServer((creq, cres) => {
const prior = creq.headers["x-forwarded-for"]; // 既に付いている値
const client = creq.socket.remoteAddress; // 実際の接続元
const headers = {
...creq.headers,
// 既存の値があれば「追記」する(多くの実装がこうする)
"x-forwarded-for": prior ? prior + ", " + client : client,
"x-forwarded-proto": "https",
};
// ここでオリジンへ転送する
});オリジン側は受け取ったヘッダーをそのまま返すだけにしてあります。まず普通にリクエストした場合。
{
"remoteAddress": "127.0.0.1",
"host": "origin.internal:3999",
"xff": "127.0.0.1",
"xfproto": "https"
}次に、クライアントが自分で X-Forwarded-For: 1.2.3.4 を付けて同じプロキシに送った場合。
{
"remoteAddress": "127.0.0.1",
"host": "origin.internal:3999",
"xff": "1.2.3.4, 127.0.0.1",
"xfproto": "https"
}偽の値 1.2.3.4 は消えずに左端に残り、プロキシが観測した本物のIPはその右に追記されました。もしアプリが「X-Forwarded-For の左端=クライアントIP」という素直な実装をしていたら、このリクエストのクライアントIPは 1.2.3.4 として記録されます。攻撃者は好きなIPアドレスを名乗れることになります。
これは私の書いたコードが特殊なのではありません。nginx の公式ドキュメントは、よく使われる変数 $proxy_add_x_forwarded_for を「クライアントリクエストの X-Forwarded-For に $remote_addr をカンマ区切りで追記した値」と定義しています。多くの構成例がこの変数をそのまま使っているため、外から来た嘘の値も一緒に引き継がれます。
RFC 7239 のセキュリティ考慮事項も同じことを警告しています。「Forwarded ヘッダーフィールドは正しいものとして信頼できない。サーバーに至るまでのすべてのノードが、誤って、あるいは悪意を持って変更しうるからである」。
では何を信用すればよいのか
原則はひとつです。自分が管理している最前段のプロキシが書いた値だけを信用する。具体的には次の2段構えにします。
- 入口では追記ではなく上書きする。nginx なら
proxy_set_header X-Forwarded-For $remote_addr;と書けば、外から届いた値は捨てられます。信頼境界の一番外側では、持ち込まれた値を必ず洗い流します。 - アプリ側では信頼するプロキシを限定する。Express なら
trust proxyの設定に、ホップ数やIPアドレス/サブネットを指定します。公式ドキュメントは、これをtrue(全面的に信頼)にするなら前段がX-Forwarded-*を必ず上書きしていることが前提だと明記し、ホップ数で指定する場合も「アプリへ至る経路すべてでホップ数が同じでないと、ホップの少ない経路から詐称できる」と注意しています。
なぜここまで気にするかというと、IP制限・レート制限・地域判定・監査ログ・不正検知が、すべてこの1つの値に依存しているからです。ここが偽装できると、レート制限はリクエストごとにIPを変えるだけで無限に回避され、インシデント調査で頼りにする監査ログには嘘のIPアドレスが残ります。IPアドレスの表記を確認したいときは IP Address Converter や CIDR Calculator が使えます(CIDR表記の読み方も参考にしてください)。
CDNやPaaSを使っている場合は、そのプラットフォームが改ざんできない形で提供している値(プラットフォーム固有のヘッダーや実行環境のAPI)があるかをドキュメントで確認し、あればそちらを優先してください。自分で管理していない前段の X-Forwarded-For を素通しで信用するより確実です。
実務でハマるところ
502と504は「前段からの信号」
502 Bad Gateway はゲートウェイやプロキシが上流から不正な応答を受け取ったことを、504 Gateway Timeout は上流の応答を待ってタイムアウトしたことを示します。どちらも共通して意味しているのは、前段は生きていて、その奥のオリジンが応答できていないということです。
したがって502/504がブラウザに出たときの切り分けは、①アプリのプロセスは生きているか ②前段からアプリへ到達できるか(ポート・ネットワーク)③前段のタイムアウト設定を超えていないか、の順になります。逆に、アプリが自力で返す500との違いを意識すると、どちらのログを見に行くべきかで迷わなくなります。ステータスコードの意味をまとめて確認したいときは HTTP Status Code Reference を使ってください。
タイムアウトの不一致で「正常なのに504」
nginx の proxy_read_timeout の既定値は60秒です。アプリ側に90秒かかる処理があると、アプリは正常に処理を続けているにもかかわらず、利用者には504が返ります。しかもアプリのログにはエラーが残らないため、原因にたどり着きにくい種類の障害です。重い処理を持つなら、前段のタイムアウト値も必ずセットで確認します(あるいは、そもそも同期処理から切り離します)。
Hostヘッダーが書き換わって生成URLが壊れる
nginx は既定で、元のリクエストの Host をそのまま渡しません。公式ドキュメントによれば、既定は転送先のホスト名($proxy_host)に書き換えられます。アプリが絶対URLを生成していると、リダイレクト先やcanonical、メール本文中のリンクが内部のホスト名になってしまいます。proxy_set_header Host $host; を明示するのが定石です。
TLS終端によるリダイレクトループ
前段でTLSを終端すると、アプリにはHTTPで届きます。ここでアプリが「HTTPで来たらHTTPSへリダイレクト」を素直に実装していると、外側はHTTPSなのにアプリは毎回HTTPだと判断し、無限にリダイレクトし続けます。判定には X-Forwarded-Proto を見る必要があり、そのためにフレームワーク側で「前段を信頼する」設定(前述の trust proxy 等)を入れます。リダイレクトループを見たら、まずこの組み合わせを疑ってください。
共有キャッシュに個人向けの応答が入る
前段のキャッシュは複数の利用者で共有されるため、ログイン後のページが入り込むと他人に配られます。RFC 9111 は、Authorization ヘッダーを含むリクエストへの応答について、共有キャッシュがそれを再利用してよいのは must-revalidate / public / s-maxage のような明示的なディレクティブがある場合に限る、と定めています(3.5節)。
また、同じURLでも言語や端末で内容が変わるなら Vary の指定が必要です。RFC 9111 は、Vary に挙げられた要求ヘッダーが元のリクエストと一致しない限り、保存された応答を検証なしに使ってはならないと定めています(4.1節)。「キャッシュは前段の仕事だから」とアプリ側が無関心でいると事故になります。キャッシュしてほしくない応答には、アプリ側から Cache-Control: private や no-store を明示してください。
リクエストスマグリング:前段と後段の解釈のズレ
前段と後段でリクエストの区切り方の解釈が食い違うと、1つの接続に紛れ込ませた別のリクエストが後段にだけ見える、という攻撃が成立します。原因になりやすいのが、ボディの長さの指定が2通りある点です。
RFC 9112 は、Transfer-Encoding と Content-Length の両方があるメッセージを受け取った場合、Transfer-Encoding が Content-Length を上書きすると定めています(6.3節)。中間装置は転送する前に受け取った Content-Length を除去して処理すべきだ、とも書かれています。同じ仕様のセキュリティ考慮事項(11.2節)は、リクエストスマグリングが「複数の受信者間の解釈の違いを悪用して追加のリクエストを隠すもの」であり、6.3節のメッセージ区切りに関する要件はその有効性を下げるために導入された、と説明しています。
実務上の対策はシンプルで、前段と後段のHTTP実装を新しく保つこと、そして古いプロキシと新しいアプリサーバーのような世代の違う組み合わせを避けることです。片方だけ更新して安心しない、という点だけ覚えておいてください。
目的別:どちらを使うか
| やりたいこと | 使うもの |
|---|---|
| 社員の外向き通信を制御・記録したい | フォワードプロキシ |
| 取引先に対して固定の送信元IPで出たい | フォワードプロキシ(またはNATの設計) |
| 1台のサーバーで複数のサイト・アプリを配信したい | リバースプロキシ(ホスト名/パスで振り分け) |
| 証明書の管理を1か所にまとめたい | リバースプロキシ(TLS終端) |
| アクセス集中に耐えたい | リバースプロキシ+キャッシュ/負荷分散 |
| 自宅サーバーやVPSを安全に公開したい | ポート開放+リバースプロキシ(アプリを直接外に出さない) |
| 通信内容を秘匿したい・別拠点のネットワークに入りたい | VPN |
最後の行に関連して、プロキシとVPNの違いもここで整理しておきます。プロキシはアプリケーション層で特定のプロトコル(多くはHTTP)を中継する仕組みで、中身を理解して手を加えられます。VPNはネットワーク層で経路ごと包む仕組みで、その中を流れるのがHTTPだろうがSSHだろうが関知しません。「ブラウザの通信だけ会社の出口を通したい」ならプロキシ、「端末の通信すべてを別のネットワークに置きたい」ならVPN、という選び分けになります。
リバースプロキシを置くときのチェックリスト
- ☐
Hostを明示的に転送している(既定のままにしていない) - ☐ 最前段で
X-Forwarded-Forを上書きしている(追記のままにしていない) - ☐ アプリ側で信頼するプロキシを限定している(無条件に信頼していない)
- ☐ HTTPSへのリダイレクト判定に
X-Forwarded-Protoを使っている - ☐ 前段とアプリのタイムアウトの大小関係を把握している
- ☐ 認証が必要な応答が共有キャッシュに入らない設定になっている
- ☐ 要求ヘッダーで内容が変わる応答に
Varyを指定している - ☐ セキュリティヘッダーを前段とアプリのどちらが付けるか決め、重複していない
- ☐ 502/504が出たときに、すぐオリジンのログを見られる導線がある
- ☐ 前段・後段のHTTP実装を更新している(リクエストスマグリング対策)
最終的に外へ出ているヘッダーの状態は、レスポンスヘッダーを貼り付けて採点できる Security Headers Analyzer で確認できます。前段とアプリの両方でヘッダーを設定していると、片方が上書きしていたり二重に付いていたりすることがあるので、設定ファイルではなく実際の応答で確認するのが確実です。
実体験:自分のサイトの前にも他人のプロキシが立っていた
本サイトを作り始めたころ、リバースプロキシは「大きなシステムの構成図に出てくるもの」だと思っていました。実際には、Vercelにデプロイした時点で自分のアプリの前には既にリバースプロキシが立っていたわけですが、そのことを意識したのは、この記事のために自分のサイトへ curl を打ったときでした。Server: Vercel、X-Vercel-Cache: HIT、Age: 1206。自分が書いたコードが1行も動いていない状態で、正しいページが返っているという事実は、頭で理解しているのと画面で見るのとでは受け取り方がかなり違いました。旧ドメインの308リダイレクトが前段から返っていたのも同様です。
「どこが応答しているのか」を取り違える失敗も経験しました。記事を1本追加してビルドし直したのに、ブラウザでは404のまま。原因は、前に起動していた開発サーバーのプロセスがポートを掴んだまま残っていて、新しいサーバーの起動が失敗し、古いサーバーが応答し続けていたことでした。コードを何度見直しても直らないはずです。この一件以来、まず「いま応答しているのは自分が思っている相手か」を確認するようになりました。前段が入ると、この種の取り違えは確実に増えます。
そして今回、記事のために20行ほどのプロキシを書いて X-Forwarded-For を観察しました。「偽装できる」という知識自体は前からありましたが、1.2.3.4, 127.0.0.1 という文字列を実際に目にすると、この値を素直に「クライアントIP」としてログに書いている実装がどれだけ危ういかが腹に落ちました。信用できるのは、自分が管理している最前段が書いた1つだけ。これは実際に手を動かして初めて実感できた部分です。
もう一つ、セキュリティヘッダーの設定で気づいたことがあります。本サイトのヘッダーは next.config.ts に書いていますが、実際にブラウザへ届けているのは前段です。どこで付けるか(アプリ/リバースプロキシ/CDN)を決めておかないと、二重に付いたり、片方の設定だけが効いていたりします。設定ファイルを読んで満足せず、必ず実際のレスポンスで確認する。当たり前のようで、前段がある環境では特に効いてくる習慣です。
参考にした一次情報
- RFC 9110(HTTP Semantics) 3.7節 Intermediaries(proxy / gateway=reverse proxy / tunnel の定義)、9.3.6節 CONNECT
- RFC 7239(Forwarded HTTP Extension)──
for/by/proto/hostの定義とセキュリティ考慮事項 - RFC 9111(HTTP Caching) 1.3節(プライベート/共有キャッシュ)、3.5節(Authorization付きリクエストへの応答の保存)、4.1節(Vary)、5.1節(Age)
- RFC 9112(HTTP/1.1) 6.3節(Transfer-Encoding が Content-Length を上書きする)、11.2節(リクエストスマグリング)
- nginx 公式ドキュメント ngx_http_proxy_module(
proxy_pass、proxy_set_headerの既定値、$proxy_add_x_forwarded_for、proxy_read_timeoutの既定60秒) - Express 公式「Express behind proxies」(
trust proxyの設定値と警告) - MDN「Proxy servers and tunneling」「CONNECT」
- 実測:2026年8月30日に
https://secutils.jpへcurlしたレスポンスヘッダー、および同日ローカルで実行した検証コードの出力
ミドルウェアの既定値は版によって変わることがあります。設定前に、お使いのバージョンの公式ドキュメントで確認してください。
よくある質問
プロキシとリバースプロキシの違いは何ですか?
どちら側が置くかが逆です。フォワードプロキシはクライアント側が置き、クライアントの代理として外部へリクエストを送ります。リバースプロキシはサーバー側が置き、サーバーの代理としてリクエストを受け取ります。RFC 9110 はリバースプロキシを「gateway」と呼び、外向きの接続に対してはオリジンサーバーとして振る舞う中間装置と定義しています。利用者はフォワードプロキシの存在を(自分で設定するので)知っていますが、リバースプロキシの存在には普通気づきません。
リバースプロキシを置くと何が良いのですか?
TLS終端で証明書管理を1か所に集約でき、キャッシュでアプリの負荷を減らし、複数のサーバーへ負荷分散でき、パスやホスト名でルーティングでき、レート制限やセキュリティヘッダーの付与といった防御の一枚目を置けます。アプリを直接インターネットに晒さずに済むことも大きな利点です。
X-Forwarded-For のIPアドレスは信用できますか?
そのままでは信用できません。多くのプロキシは既存の値に追記する動作をするため、クライアントが最初から偽の値を付けて送ると、それが左端に残ったまま転送されます(実際に検証すると 1.2.3.4, 127.0.0.1 のようになります)。RFC 7239 も「経路上のすべてのノードが誤って、あるいは悪意を持って変更しうるため信頼できない」と警告しています。信用してよいのは自分が管理している最前段のプロキシが書いた値だけです。入口では追記ではなく上書きし、アプリ側では信頼するプロキシをIPやホップ数で限定してください。
502エラーと504エラーの違いは何ですか?
502 Bad Gateway はゲートウェイやプロキシが上流から不正な応答を受け取ったことを、504 Gateway Timeout は上流からの応答を待ってタイムアウトしたことを示します。どちらも「前段は動いていて、その奥のアプリが正常に応答できていない」という意味なので、まずアプリのプロセスと前段からの到達性、そして前段のタイムアウト設定を確認します。nginx の proxy_read_timeout の既定は60秒で、アプリの処理がこれを超えると、アプリ側は正常でも504が返ります。
プロキシとVPNはどう違いますか?
レイヤーが違います。プロキシはアプリケーション層で特定のプロトコル(多くはHTTP)を中継する仕組みで、URLを見て遮断するなど中身に手を加えられます。VPNはネットワーク層で通信を丸ごと包む仕組みで、中を流れるプロトコルには関知しません。ブラウザの通信だけを会社の出口経由にしたいならプロキシ、端末の通信すべてを別のネットワークに所属させたいならVPNが適しています。
おわりに
プロキシとリバースプロキシの違いは、機能の一覧を覚えるよりも「誰が置いて、誰の代理をしているか」で捉えるほうが応用が利きます。クライアントが置けばフォワードプロキシ、サーバーが置けばリバースプロキシ。それだけで、設定ファイルの場所も、障害時に最初に疑う場所も決まります。
そして実務で最も事故につながりやすいのは、性能でも構成でもなく「前段を挟んだことで変わった情報」です。接続元IP、プロトコル、ホスト名、そしてキャッシュ。この4つについて「いまアプリが見ている値は誰が書いたものか」を答えられる状態にしておけば、大半のハマりどころは避けられます。手元で curl -sSI を打って、自分のサイトの前に何が立っているかを確かめるところから始めてみてください。