こんにちは。 インフラチームの野島(@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 ライフを送りましょう。