読者です 読者をやめる 読者になる 読者になる

nginx の設定をレビューするときの観点をまとめてみた

こんにちは。 インフラチームの野島(@nojima)です。

チームのメンバーに nginx の設定について気をつけるべき点を共有するために、レビュー観点を書きました。 せっかくなのでここで公開します。

ほとんどの項目は自分やチームのメンバーの実体験に基いています。

レビュー観点

server

  • server_name が他のやつと被っていないか。
    • listen する IP アドレスが同じ場合、server_name で区別できないといけない。
    • TLS を使う場合、SNI をサポートしないクライアントでは TLS 用の設定が default_server のものが使われる点にも注意。
  • TLS を使う場合、listen ディレクティブに ssl オプションを書いているか。

location

  • location のマッチの順番に注意
    • 正規表現の location は前方一致の location よりも優先度が高い。 意図せず別の location を隠してしまっていないか確認する。
    • また正規表現の location 同士は上に書かれたものが優先されるので正規表現 location 同士でも注意が必要。 (前方一致の location の場合、順番は関係なくて、より長い location が一致されるため、普通は大丈夫)
  • 正規表現に注意
    • ^ とか $ を付けるべきか付けないべきか。
    • index.html じゃなくて index\.html
    • 正規表現エンジンに PCRE が使われているので、バックトラックが大量に起こりうる正規表現があると DoS をされる可能性がある。 常にバックトラックが起こらない正規表現を書くこと。
  • location /hoge/ と書くと /hoge にアクセスされたときにマッチしない。
    • location = /hoge を作って return /hoge/$is_args$args; と書いておくと親切だが、実際のところここまでやってる設定は少ない。
      • ちなみに、rewrite ^/hoge$ /hoge/; のようにして internal redirect で処理してはいけない。相対リンクが壊れるので。

URL デコードに注意

  • nginx は URL (正確にはパスの部分) を勝手に URL デコードしてしまうことがある。
    • リクエストを /prefix をつけた URL にリダイレクトしようとして return 301 /prefix$uri; とやると嵌まる。
      • /hoge%3Fpiyo/prefix/hoge?piyo にリダイレクトされる。
    • $request_urireturn できないか検討すること。request_uri はデコードされていない URL が格納されている。
      • ちなみに $uri は引数の部分を含まないが $request_uri は引数の部分を含む。紛らわしい。
  • rewrite ^(.*)$ /prefix$1 redirect; なども似たような問題がある。
    • やっぱり /hoge%3Fpiyo/prefix/hoge?piyo にリダイレクトされる。
    • rewrite の場合は単純に URL デコードされるわけではなく、文字によってデコードされたりされなかったりする。
  • さらに言うと、URL デコードだけでなく、駆け上がり処理 (/hoge/../fuga/fuga にするようなやつ) とかも行われる。
    • 駆け上がり処理は URL デコードした後の文字列で行うので、/../ を URL エンコードしたりしても回避できない。
  • Apache は %2F をURL デコードしないという謎の仕様があるが、nginx にはこの仕様がないので微妙に互換でない。

proxy_pass

  • proxy_pass はホスト名まで書く場合とパスの部分がある場合で挙動が変わる。
    • つまり proxy_pass http://foo; と書く場合と proxy_pass http://foo/; は異なる挙動をするということ。
    • パスを指定してしまうと %2F%2B などの一部の文字が勝手にデコードされる問題が発生する。パスを指定しない場合はパスをそのままバックエンドに渡してくれる。
    • ということで基本的にパスは書くべきでない。proxy_pass http://foo; の形式を用いるのが安全。
    • /hoge.index/prefix/hoge.index にリバースプロキシしたいみたいな場合はどうしてもデコードが避けられない。
    • また、proxy_pass は URL に変数を含む場合と含まない場合で挙動が変わるが、マニアックなので省略。
    • さらに proxy_pass を含む location は挙動が微妙に変わる。これに関してはマニュアルを参照

フェイズに注意

  • returnrewrite, set などは deny とか allow より先に処理される等、ディレクティブの処理順番に注意。
    • deny all; としていても同じ location に return 200 "hello"; とか書くと 200 が返ってくる。
    • 処理順番はドキュメントに記載されていない場合が多いので、気になる場合は実験するかソースを読むしかない。
  • 基本的に set, rewrite, return などのリライト系が最初に処理され、limit_req などのリソース制限系が次に処理され、deny, allow などのアクセス制限系が次に処理され、次に proxy_pass などのレスポンス生成系が処理される。ログの出力は一番最後。
    • internal redirect があると内部的にフェイズが巻き戻り、また最初から順番に処理される。

その他

  • internal redirect なのか普通の redirect なのか。
  • redirect するときにパスだけ redirect すればいいのか、引数 (? 以降のやつ) を引き継ぐ必要があるのか?
    • 引き継ぐ必要がある場合、$is_args$args を末尾に付けないといけない。忘れやすいので注意。
  • HSTS ヘッダを付けるべきか付けないべきか。付ける場合は includeSubdomainspreload を指定するべきかしないべきか。
  • add_header をすると、それより上のスコープで add_header したやつが全部消える。 なので下の階層でヘッダを追加したい場合は、上の階層で add_header したやつを全部また add_header しないといけない。
    • error_page なども同様。
  • allow, deny を複数書く場合は上から順番にマッチされていく。
    • allow all; deny 1.2.3.4; のように書いてしまうと 1.2.3.4 は許可されてしまう。
  • レスポンスの Content-Type ヘッダの値は正しいか。
    • types ディレクティブで指定されていない拡張子のファイルがあるかチェック。
      • 現実的には、nginx の設定の管理者とコンテンツの管理者が異なるとチェックはかなり難しいけど。
    • Content-Type が間違っているとダウンロードされてほしいところでインライン表示になったり、インライン表示されてほしいことろでダウンロードされたりする。
      • 実際これでよく問題になる。
    • gzip_types など Content-Type で動作が変わるようなディレクティブもある。
    • また、charset ディレクティブで charset も指定すべき。文字化けによる XSS がありうるので。
      • 歴史的事情により文字コードが混在してたりすると辛い。
  • error_page の中でエラーが起きないか。
    • error_page ディレクティブを使うとエラー時に internal redirect を起こせるが、internal redirect の先で更にエラーが起きた場合、後に起きたエラーのエラーコードがクライアントに返されるので注意。
      • エラー処理の中で起きたエラーを更にエラー処理する設定にもできるけど、ややこしいのであまり使うべきじゃないと思う。

DNS

  • 設定ファイル内にドメイン名をベタに書いた場合、そのドメイン名は nginx 起動時 (または reload 時) に名前解決され、TTL を無視してずっと保持される。
    • この名前解決は resolver ディレクティブに指定した DNS サーバではなく、OS デフォルトの DNS サーバで行われる。(gethostbyname が使われている)
  • ドメイン名の指定に変数が指定されている場合、そのドメイン名はリクエストが来たときに名前解決され、TTL は遵守される。
    • この名前解決は resolver ディレクティブで指定した DNS サーバで行われる。
  • 設定ファイルにドメイン名をベタ書きしたいけど TTL は遵守したい場合、一旦変数にドメイン名を set して、それをディレクティブの引数に指定するなどの工夫が必要。

適用手順

  • restart すべきか reload すべきか。
    • restart すべきなのは以下のような場合のみ:
      • nginx のプログラムを更新するとき。
      • 共有メモリ(SSLのセッションキャッシュとか)のサイズを変更したいとき。
      • リスニングソケットのオプション (setsockopt で弄るようなやつ) を変更したいとき。(ポートの変更とかなら restart しなくてよい)
    • これら以外の場合は reload する。
  • 複数回 graceful restart するときは、一個前の graceful restart が完全に終わっていることを確かめる。
    • ps して古い master がいなくなったことを確かめればよい。
    • 一個前の graceful restart が完全に終わる前に新たな graceful restart を始めることは nginx の仕様上できない。
    • 例えば1時間掛けてでかいファイルをダウンロードしているクライアントがいる場合、その1時間に1回しか graceful restart はできない。

おわりに

nginx は嵌まりどころが結構多いですが、ちゃんと使うととても優秀な HTTP サーバです。 上手く利用して幸せな nginx ライフを送りましょう。