nginx.confの中身を理解したいので一つずつ調べました
こんにちは
Nginxを導入する上でnginx.confファイルを記述するのですが、初学者あるあるとして適当に「rails nginx docker」って調べて出てきたコードをそのままコピペして導入することがあるとおもいます。
しかしそれはあまりよろしくないと思ったので、ある程度は理解できるようにここにアウトプットします。
nginx.confの例(Railsのpumaを用いる場合)
「rails nginx docker」で調べたら出てきた
こちらの記事を参考にとりあえず全体像を貼り付けておきます。
# プロキシ先の指定
# Nginxが受け取ったリクエストをバックエンドのpumaに送信
upstream webapp {
# ソケット通信したいのでpuma.sockを指定
server unix:///webapp/tmp/sockets/puma.sock;
}
server {
listen 80;
# ドメインもしくはIPを指定
server_name example.com [or 192.168.xx.xx [or localhost]];
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# ドキュメントルートの指定
root /webapp/public;
client_max_body_size 100m;
error_page 404 /404.html;
error_page 505 502 503 504 /500.html;
try_files $uri/index.html $uri @webapp;
keepalive_timeout 5;
# リバースプロキシ関連の設定
location @webapp {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://webapp;
}
}
基礎知識
nginx.confはディレクティブとコンテキストといった要素で構成されています。
ディレクティブは直訳すると「指令、指示文」といった意味があります。
このファイルは
- UTF-8だよ とか
- HTMLファイルだよ とかもディレクティブですね。
nginx.confのディレクティブは {} で囲まれているものと ; で終わるコードが該当します。
そしてディレクティブの中でも{}で囲まれた範囲内はコンテキストと言います。
つまり以下のようになります。
# upstreamディレクティブの中にserverディレクティブがある
upstream webapp {
# このserverディレクティブはupstream{}で囲まれているのでupstreamコンテキストに属する
server unix:///webapp/tmp/sockets/puma.sock;
}
あるコンテキストの範囲の中でしか使えないディレクティブも存在します。
例えばlocationディレクティブはserverコンテキストの中かlocationコンテキストの中にしか記述できません。
upstreamコンテキストについて
# プロキシ先の指定
# Nginxが受け取ったリクエストをバックエンドのpumaに送信
upstream webapp {
# ソケット通信したいのでpuma.sockを指定
server unix:///webapp/tmp/sockets/puma.sock;
}
- upstreamコンテキストはバックエンドのアプリケーションサーバーを指定することができます
- webappというのはupstreamの名前をつけています。
- serverディレクティブを使ってUNIXドメインソケットのパスを指定しています
- upstreamコンテキストの中のserverディレクティブはロードバランサーの振り分け先を記述するディレクティブとなります
つまりNginxとpumaは同一ホスト上でのソケット通信になり、Railsで3000番のポートをあけなくても相互に通信できるようになっています
serverコンテキストについて
- このコンテキスト内に外部からアクセスされた際のWebサーバーの挙動を記述します
listenディレクティブ
listen 80;
- 待ち受けするポート番号を指定します
- port(窓)が空いていてもlisten(聞く耳)を持たなかったら意思疎通できないでおなじみですね
- 80番はHTTPのデフォルトのポート番号です
- 複数個指定できます
server_nameディレクティブ
server_name example.com [or 192.168.xx.xx [or localhost]];
- ホスト名を指定します
- サーバー名を並べて指定することで複数個指定できます
- 正規表現やワイルドカードによる指定もできます
- 複数のserver_nameがマッチする場合はホスト名全体を書き記した完全一致が最も優先されます
access_log,error_log
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
- アクセスログとエラーログの出力先ファイルを記述しています
- dockerを用いている場合は
docker exec -it コンテナ名 sh
でコンテナ内に入ってファイルを確認することができます
rootディレクティブ
root /webapp/public;
- ドキュメントルートを設定しています。つまりそのWebサーバーのツリー構造における最上部となるディレクトリを指定しています
client_max_body_sizeディレクティブ
client_max_body_size 100m;
- クライアントのリクエストボディの最大許容サイズを指定しています
- リクエストボディが上記の値を超えると、413 (Request Entity Too Large)エラーをクライアントに返しますが、ブラウザには正しく表示されないので注意が必要です
error_pageディレクティブ
error_page 404 /404.html;
error_page 505 502 503 504 /500.html;
- エラーが起きたときにどのページに飛ばすかを指定しています
- 上記では404エラーが起きたときに /404.htmlにリダイレクトするように指定しています
- 「=」を用いて次のようにも書けます
error_page 404 = /404.html;
try_filesディレクティブ
try_files $uri/index.html $uri @webapp;
-
try_filesディレクティブは静的なコンテンツと動的なコンテンツを振り分けたり、存在しないファイルを指定された場合にホームページに飛ばしたりといったことができます
-
try_filesは複数の引数を取ります
-
最初の引数で指定されたファイルを探していき、見つからなければ次の引数に移行します。そして最初に見つかったファイルをクライアントのリクエストの処理に用います。ファイルがみつからなかった場合は最後の引数に指定したURIかステータスコードを返します
-
つまり、
$uri/index.html
$uri
とファイルを探していき、見つからなければ後述のlocationで定義してある@webapp
に内部リダイレクトします-
$uri
はNginxの組み込み変数でアクセス先のパス名を指定しています。そして末尾に/
を用いることでそのディレクトリが存在するかを確認しています。また@
はロケーションに名前をつけることができます。しかし名前付きlocationはネストさせることができません
-
keepalive_timeoutディレクティブ
keepalive_timeout 5;
- クライアントと常時接続する時間を指定してます
- デフォルトでは75秒となっていますが高負荷なので少なく設定してあります
locationコンテキスト
-
location URI {...}
はURIに基づいたリクエストに対する設定を行います -
location / {...}
とすればすべてのパスが一致します - 「=」演算子を用いた完全一致が最も優先されます
- 今回は@をつけた名前付きlocationなので、try_filesからの内部リダイレクトに対する処理を記述しています
proxy_set_headerディレクティブ
location @webapp {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
add_header
はクライアントからのリクエストにヘッダを付与するのに対して、proxy_set_header
はバックエンドにアクセスする時のリクエストにヘッダを付与します
-
X-Real-IP
ヘッダーに$remote_addr
を用いて、クライアントのIPを指定しています -
X-Forwarded-For (以下XFF)
はロードバランサーやプロキシを経由したIPを全て判別しています。そして、$proxy_add_x_forwarded_for
はXSSヘッダが付与されている場合はIPをカンマ区切りにしてくれます。しかし、受け取ったリクエストにXFFヘッダが付与されいない場合、自動でXFFヘッダを作成してクライアントIP($remote_addr
)を割り当てます。 -
Host
ヘッダーは、リクエストが送信される先のサーバーのホスト名とポート番号を指定します。ポート番号は省略可能です。そして、$http_host
はHTTPリクエストのHostヘッダーの値です
-
XFF
において、client→ELB→プロキシという具合にIPは変わっていくので基本的に最初に渡されているIPがクライアントのIPですが偽造されている場合もあります。 -
$proxy_add_x_forwarded_for
はIPがあればそれを使い、なければ新しく作るという仕組みですが、フロントエンドでこれを用いている場合に、悪意のあるクライアントがXFFヘッダをつけてリクエストしてきたときにそれをそのままバックエンドに流してしまいます。解決策としてはproxy_set_header X-Forwarded-For $remote_addr;
のようにクライアントのIPをそのままXFFのヘッダに割り当てることで解決できます。 -
下記の構成でのnginx2で
$proxy_add_x_forwarded_for
を使うには注意が必要です。なぜかというと、clientからのipを$remote_addr
でnginx1が取得→そのIPはclientのIPにもかかわらず、$proxy_add_x_forwarded_for
によってnginx2ではnginx1のIPだと誤解してしまいます。なので結果としてproxy2のXFFに差異が生まれます。-
期待されるXFF: X-Forwarded-For: 192.168.100.101, 172.21.0.1
実際に設定されるXFF: X-Forwarded-For: 192.168.100.101, 192.168.100.101
-
期待されるXFF: X-Forwarded-For: 192.168.100.101, 172.21.0.1
引用元
https://www.serotoninpower.club/archives/780/
https://www.serotoninpower.club/archives/790/
:::
proxy_passディレクティブ
proxy_pass http://webapp;
- リバースプロキシの設定を行うディレクティブです
- ここではupstreamで指定されたAPサーバーのパスを指定しています
安全なproxy_set_headerの設定
先程あげた問題点を解決する設定です
upstream webapp {
server unix:///webapp/tmp/sockets/puma.sock;
}
server {
listen 80;
server_name example.com [or 192.168.xx.xx [or localhost]];
# 信頼できるアドレスを指定(多段proxyの場合は前面にあるproxy1を。もしくはVPCなど)
# X-Forwarded-Forは偽装可能なので、ここで指定したIPアドレス以外からは書き換えを行わないようにする。
set_real_ip_from proxy1 or VPC;
# set_real_ip_fromから一番最後のIPアドレスをクライアントIPと判定する。
real_ip_header X-Forwarded-For;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
root /webapp/public;
client_max_body_size 100m;
error_page 404 /404.html;
error_page 505 502 503 504 /500.html;
try_files $uri/index.html $uri @webapp;
keepalive_timeout 5;
location @webapp {
# $http_host はバックエンドサーバーへのホストヘッダのなりすましという脆弱性があるので $host を使うようにする
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
# ここにはクライアントのIPが設定されている
proxy_set_header X-Real-IP $remote_addr;
# 上流のプロキシやALBから送られてきたXFFを$http_x_forwarded_forで追加して、
# それにスペース+カンマでその上流プロキシのIPを追加したものをここのXFFに設定している
proxy_set_header X-Forwarded-For "$http_x_forwarded_for, $realip_remote_addr";
# ALBをSSLの終端としている場合はクライアントのプロトコルがhttpsであることを伝えるための設定
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_pass http://webapp;
}
}
より詳しい内容は下記のサイトが非常に参考になります
多段nginxでもX-Forwarded-ForできちんとバックエンドにクライアントIPアドレスを伝える
https://www.serotoninpower.club/archives/780/
何か間違いがあれば、ご指摘いただければ幸いです。
何卒、よろしくお願い申し上げます!
Discussion