プロキシ設定の六行と、その六行がないとき起きること
一言でいうと
プロキシ設定での事故は、たいてい6行のヘッダー転送と、proxy_passのスラッシュ1つから起こり、どちらが抜けていても画面は問題なく表示されるため、発見が遅れます。
なぜ前にWebサーバーを置くのか
TomcatもHTTPを話せるのに、なぜ前にnginxを置くのでしょうか。SI現場の実際の理由は、性能よりも運用の利便性とポリシーです。
- 静的ファイルをWASに処理させません(WASのスレッドを節約します)
- SSL終端を1か所で行います(証明書の交換を1台のサーバーで済ませます)
- 複数のWASに負荷分散し、1台が死んでもサービスが維持されるようにします
- URLを基準に、別のシステムへルーティングします(
/api/は新規、残りはレガシー) - アクセス制御、リクエストサイズの制限、圧縮、キャッシュヘッダーを、1か所でかけます
特に最後の項目が大きいです。ポリシーをアプリケーションに入れると、デプロイしないと変わりませんが、プロキシに入れるとreloadで変わります。運用組織がプロキシを好む理由です。
プロキシ設定の6行
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 60s;
}
各行がないときに何が起きるかを知ってこそ、暗記ではなく理解したことになります。
Host $hostがないと、バックエンドが受け取るHostは127.0.0.1:8080になります。アプリケーションが絶対URLを作るとき(リダイレクト、メールのリンク、決済コールバック)、http://127.0.0.1:8080/...が出ていきます。これをサービスイン後に発見すると、決済コールバックが戻ってきません。X-Forwarded-Forがないと、アプリケーションが見るクライアントIPは、すべてプロキシのIPです。接続履歴、不正利用の検知、IPベースのアクセス制御が、すべて無意味になります。監査で「接続者のIPを残していますか」に答えられません。$proxy_add_x_forwarded_forは、既存のヘッダーに追記する変数なので、プロキシが複数段なら、リストになります。X-Forwarded-Protoがないと: これがリダイレクト無限ループの最大の原因です。プロキシがTLSを終端して、バックエンドにはHTTPで渡します。バックエンドは「HTTPSではないな」と言って、https://にリダイレクトします。再びプロキシに来て、またHTTPで渡されて、無限に繰り返します。ブラウザーはERR_TOO_MANY_REDIRECTSを表示します。ループの大多数は、TLSの終端地点とアプリケーションのHTTPS強制ロジックが、互いを知らないことから生じます。
そして重要なセキュリティ原則が1つあります。これらのヘッダーは、最前段のプロキシで必ず上書きしなければなりません。クライアントがX-Forwarded-For: 10.0.0.1を直接送れるからです。proxy_set_headerで上書きすれば、クライアントが送った値は無視されます(追記される変数は例外です)。
proxy_passのスラッシュ: 最もよくあるnginxのバグ
location /api/ {
proxy_pass http://backend; # 슬래시 없음 → /api/users/1 이 그대로 전달
}
location /api/ {
proxy_pass http://backend/; # 슬래시 있음 → /api 부분이 잘리고 /users/1 로 전달
}
URI部分があると(スラッシュを含む)、locationにマッチした部分が切り落とされます。バックエンドが/apiコンテキストを期待しているのにスラッシュを付けると、404が大量に出て、逆に、バックエンドがルートを期待しているのにスラッシュを外すと、/api/api/usersになります。
この1文字で半日を費やす人を、毎年見ます。迷ったら、アクセスログの$upstream_addrとバックエンドログのリクエストパスを一緒に見れば、すぐに判別できます。
リクエストサイズ: 413の正体
nginxのclient_max_body_sizeのデフォルト値は1MBです。ファイルアップロードがあるシステムでこの値を上げないと、3MBの添付をアップロードするとき、413 Request Entity Too Largeが出ます。そして、このエラーはnginxが返すので、アプリケーションログには何も残りません。開発者は「サーバーに何のログもありません」と言って、何日も迷います。
client_max_body_size 20m;
client_body_buffer_size 128k;
large_client_header_buffers 4 16k;
large_client_header_buffersは、ヘッダーが大きいときに使います。SSOを付けるとCookieが大きくなりますが、デフォルトのバッファーを超えると、400 Bad Requestが出ます。これも、ログがあいまいです。
圧縮と静的ファイル
gzip on;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_vary on;
gzip_comp_levelは6の付近が、CPUと圧縮率のバランス点です。9に上げても、サイズは数%しか減らず、CPUは目に見えて増えます。gzip_min_length 1000: 1KB未満は圧縮しても得がなく、オーバーヘッドだけがあります。gzip_vary onを外すと、中間キャッシュが圧縮版を、非圧縮のクライアントに渡すことがあります。gzip_typesにtext/htmlは常に含まれているので、書かなくてもかまいません。
静的ファイルは、そもそもプロキシを通さないほうがよいです。
location /static/ {
alias /app/static/;
expires 7d;
access_log off;
}
rootとaliasを混同するのも、常連です。location /static/に対して、root /app;なら/app/static/파일、alias /app/static/;なら/app/static/파일です(プレースホルダーはファイル名です)。同じに見えますが、location /s/にalias /app/static/;なら、/s/a.js→/app/static/a.jsです。locationのパスと実際のディレクトリ名が違うときに、aliasを使います。
管理画面の遮断
Tomcat ManagerやActuatorが外部に開いているのは、実際の事故につながります。プロキシで止めるのが、最も確実です。
location ~ ^/(manager|host-manager)/ { return 403; }
location /actuator/ {
allow 10.0.0.0/8;
deny all;
proxy_pass http://127.0.0.1:8080;
}
ここで学ぶ点: アプリケーション設定でも止められますが、プロキシで止めれば、アプリケーションをデプロイせずに、ポリシーを変更できます。セキュリティ点検の指摘が金曜日の午後に降りてきても、reloadで終わります。
アクセスログに何を残すか
デフォルトのcombinedフォーマットでは、障害分析ができません。最低限、この4つは入れます。
log_format labhub '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'ua="$upstream_addr" us=$upstream_status '
'urt=$upstream_response_time rt=$request_time';
$upstream_addr: どのバックエンドに行ったか(負荷分散では必須)$upstream_status: バックエンドが返したステータスコード(nginxが作った502と区別できます)$upstream_response_time: バックエンドが使った時間$request_time: クライアント基準の全体の時間
$request_timeは大きいのに$upstream_response_timeは小さいなら、問題はバックエンドではなく、クライアントのネットワークやレスポンスの転送です。この2つの値の差1つで、「バックエンドが遅い」という誤解を、何度でも解消できます。
現場での姿
ヘッダー6行が抜けたときに現れる症状は、互いに無関係に見えます。アクセスログのクライアントIPが、すべてプロキシIPの1つで記録され(X-Real-IP・X-Forwarded-Forの欠落)、ログインのあとのリダイレクトが内部アドレスやhttpに飛び(Host・X-Forwarded-Protoの欠落)、IPベースのアクセス制御や監査ログが、丸ごと無意味になります。
共通点は、普段は何も問題がないことです。そのため、サービスインの数週間後に、セキュリティ監査や障害分析で初めて発覚し、そのときにはすでに、そのログでは何も追跡できません。
proxy_passのスラッシュは、もっと静かです。末尾にスラッシュがあれば、locationのパスを除いて渡し、なければ付けたまま渡すのですが、開発環境ではパスがルートなので違いが表れず、本番でコンテキストパスが付いた瞬間に、404になります。