🌟

nginx.confの中身を理解したいので一つずつ調べました

2023/03/31に公開

こんにちは

Nginxを導入する上でnginx.confファイルを記述するのですが、初学者あるあるとして適当に「rails nginx docker」って調べて出てきたコードをそのままコピペして導入することがあるとおもいます。
しかしそれはあまりよろしくないと思ったので、ある程度は理解できるようにここにアウトプットします。

nginx.confの例(Railsのpumaを用いる場合)

「rails nginx docker」で調べたら出てきた
こちらの記事を参考にとりあえず全体像を貼り付けておきます。

nginx.conf
# プロキシ先の指定
# 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のディレクティブは {} で囲まれているものと ; で終わるコードが該当します。
そしてディレクティブの中でも{}で囲まれた範囲内はコンテキストと言います。
つまり以下のようになります。

nginx.conf
# upstreamディレクティブの中にserverディレクティブがある
upstream webapp {
  # このserverディレクティブはupstream{}で囲まれているのでupstreamコンテキストに属する
  server unix:///webapp/tmp/sockets/puma.sock;
}

あるコンテキストの範囲の中でしか使えないディレクティブも存在します。
例えばlocationディレクティブはserverコンテキストの中かlocationコンテキストの中にしか記述できません。


upstreamコンテキストについて

nginx.conf
# プロキシ先の指定
# 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ディレクティブ

nginx.conf
listen 80;
  • 待ち受けするポート番号を指定します
  • port(窓)が空いていてもlisten(聞く耳)を持たなかったら意思疎通できないでおなじみですね
  • 80番はHTTPのデフォルトのポート番号です
  • 複数個指定できます

server_nameディレクティブ

nginx.conf
server_name example.com [or 192.168.xx.xx [or localhost]];
  • ホスト名を指定します
  • サーバー名を並べて指定することで複数個指定できます
  • 正規表現やワイルドカードによる指定もできます
  • 複数のserver_nameがマッチする場合はホスト名全体を書き記した完全一致が最も優先されます

access_log,error_log

nginx.conf
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;
  • アクセスログとエラーログの出力先ファイルを記述しています
  • dockerを用いている場合はdocker exec -it コンテナ名 shでコンテナ内に入ってファイルを確認することができます

rootディレクティブ

nginx.conf
  root /webapp/public;
  • ドキュメントルートを設定しています。つまりそのWebサーバーのツリー構造における最上部となるディレクトリを指定しています

client_max_body_sizeディレクティブ

nginx.conf
  client_max_body_size 100m;
  • クライアントのリクエストボディの最大許容サイズを指定しています
  • リクエストボディが上記の値を超えると、413 (Request Entity Too Large)エラーをクライアントに返しますが、ブラウザには正しく表示されないので注意が必要です

error_pageディレクティブ

nginx.conf
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;
  • エラーが起きたときにどのページに飛ばすかを指定しています
  • 上記では404エラーが起きたときに /404.htmlにリダイレクトするように指定しています
  • 「=」を用いて次のようにも書けますerror_page 404 = /404.html;

try_filesディレクティブ

nginx.conf
  try_files  $uri/index.html $uri @webapp;
  • try_filesディレクティブは静的なコンテンツと動的なコンテンツを振り分けたり、存在しないファイルを指定された場合にホームページに飛ばしたりといったことができます

  • try_filesは複数の引数を取ります

  • 最初の引数で指定されたファイルを探していき、見つからなければ次の引数に移行します。そして最初に見つかったファイルをクライアントのリクエストの処理に用います。ファイルがみつからなかった場合は最後の引数に指定したURIかステータスコードを返します

  • つまり、$uri/index.html $uriとファイルを探していき、見つからなければ後述のlocationで定義してある@webappに内部リダイレクトします

    • $uriはNginxの組み込み変数でアクセス先のパス名を指定しています。そして末尾に/を用いることでそのディレクトリが存在するかを確認しています。また@はロケーションに名前をつけることができます。しかし名前付きlocationはネストさせることができません

keepalive_timeoutディレクティブ

nginx.conf
  keepalive_timeout 5;
  • クライアントと常時接続する時間を指定してます
  • デフォルトでは75秒となっていますが高負荷なので少なく設定してあります

locationコンテキスト

  • location URI {...} はURIに基づいたリクエストに対する設定を行います
  • location / {...}とすればすべてのパスが一致します
  • 「=」演算子を用いた完全一致が最も優先されます
  • 今回は@をつけた名前付きlocationなので、try_filesからの内部リダイレクトに対する処理を記述しています

proxy_set_headerディレクティブ

nginx.conf
  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はバックエンドにアクセスする時のリクエストにヘッダを付与します

  1. X-Real-IPヘッダーに$remote_addrを用いて、クライアントのIPを指定しています
  2. X-Forwarded-For (以下XFF)はロードバランサーやプロキシを経由したIPを全て判別しています。そして、$proxy_add_x_forwarded_forはXSSヘッダが付与されている場合はIPをカンマ区切りにしてくれます。しかし、受け取ったリクエストにXFFヘッダが付与されいない場合、自動でXFFヘッダを作成してクライアントIP($remote_addr)を割り当てます。
  3. 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
      代替テキスト

引用元
https://www.serotoninpower.club/archives/780/
https://www.serotoninpower.club/archives/790/
:::

proxy_passディレクティブ

nginx.conf
    proxy_pass http://webapp;
  • リバースプロキシの設定を行うディレクティブです
  • ここではupstreamで指定されたAPサーバーのパスを指定しています

安全なproxy_set_headerの設定

先程あげた問題点を解決する設定です

nginx.conf
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