こんにちは。 インフラチームの野島(@nojima)です。
チームのメンバーに nginx の設定について気をつけるべき点を共有するために、レビュー観点を書きました。 せっかくなのでここで公開します。
ほとんどの項目は自分やチームのメンバーの実体験に基いています。
レビュー観点
server
server_name
が他のやつと被っていないか。- listen する IP アドレスが同じ場合、
server_name
で区別できないといけない。 - TLS を使う場合、SNI をサポートしないクライアントでは TLS 用の設定が
default_server
のものが使われる点にも注意。
- listen する IP アドレスが同じ場合、
- 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_uri
をreturn
できないか検討すること。request_uri
はデコードされていない URL が格納されている。- ちなみに
$uri
は引数の部分を含まないが$request_uri
は引数の部分を含む。紛らわしい。
- ちなみに
- リクエストを
rewrite ^(.*)$ /prefix$1 redirect;
なども似たような問題がある。- やっぱり
/hoge%3Fpiyo
が/prefix/hoge?piyo
にリダイレクトされる。 rewrite
の場合は単純に URL デコードされるわけではなく、文字によってデコードされたりされなかったりする。
- やっぱり
- さらに言うと、URL デコードだけでなく、駆け上がり処理 (
/hoge/../fuga
を/fuga
にするようなやつ) とかも行われる。- 駆け上がり処理は URL デコードした後の文字列で行うので、
/../
を 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
は挙動が微妙に変わる。これに関してはマニュアルを参照。
- つまり
フェイズに注意
return
やrewrite
,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 ヘッダを付けるべきか付けないべきか。付ける場合は
includeSubdomains
やpreload
を指定するべきかしないべきか。 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 する。
- restart すべきなのは以下のような場合のみ:
- 複数回 graceful restart するときは、一個前の graceful restart が完全に終わっていることを確かめる。
ps
して古い master がいなくなったことを確かめればよい。- 一個前の graceful restart が完全に終わる前に新たな graceful restart を始めることは nginx の仕様上できない。
- 例えば1時間掛けてでかいファイルをダウンロードしているクライアントがいる場合、その1時間に1回しか graceful restart はできない。
おわりに
nginx は嵌まりどころが結構多いですが、ちゃんと使うととても優秀な HTTP サーバです。 上手く利用して幸せな nginx ライフを送りましょう。