Nginx
nginxは軽量で高い拡張性を持つオープンソースのHTTPサーバである。現在では全世界のwebサイトの数十%を占めるといわれているとか。
特に大規模webサービスにおいてnginxのシェアが高いことも判明している。これはC10K問題(1台のサーバが1万ものクライアントからの接続を同時処理することでサーバの処理が追い付かなくなる)の解決のためにnginxが作られたこととも関係している。イベント駆動、I/O Multiplexing,ノンブロッキングI/O,非同期IOといったテクニックを組み合わせることでC10K問題に耐えうる強固なアーキテクチャとなっている。
##1.nginx.confの基本設計
設定ファイルの基本的な記述方法としては、「ディレクティブ」を用いる。
ディレクティブにはパラメータを単純に1つ指定するタイプ、複数指定するタイプ、ブロック付きディレクティブの3つの書き方が大きく分けて存在。
特にブロック付きディレクティブの中の指定値をコンテキストと呼称する。
1パラメータ指定するディレクティブ
worker_processes 1;
複数パラメータ指定するディレクティブ
error_log /var/log/nginx/error.log error;
ブロック付きディレクティブ
server {
server_name a.example.com;
root /var/www/html;
}
■バーチャルサーバ(serverディレクティブ)
nginxではIPアドレス、ポート、ホスト名(具体的にはHTTPリクエストのホストヘッダー)ごとに別々の設定を持つ複数HTTPサーバを稼働させることが可能。
これらはバーチャルサーバと呼ばれ、serverディレクティブ内で定義される。HTTPディレクティブ内のserverコンテキストという形になる。
下記だと、www.example1.comでのアクセスの場合とwww.example2.comでHTTPサーバを分割できる。
■公開ディレクトリ(rootディレクティブ)
nginxではrootディレクティブで指定したディレクトリパスがURIのルートにマッピングされる。
■アクセスログの出力(log_formatディレクティブ,access_logディレクティブ)
代表例:
log_formatはログの書式、access_logは出力先を指定する。
log_formatにはnginxで利用できる代表的な関数を使うと便利である。
$remote_addr :リクエストの送信元アドレス
$remote_port :リクエストの送信元ポート
$time_local :現在時刻
$request :リクエストに含まれているHTTPのリクエスト行
$status :レスポンスのHTTPステータス
$server_name :リクエストを処理したサーバ名
$server_name :リクエストを処理したポート
$body_byte_sent :ヘッダを含まないリクエストボディのバイト数
■エラーログの出力(error_logディレクティブ)
第一パラメータは出力先、第二パラメータはエラーレベルを指定し指定したエラーレベル以上のエラーが出力される。
■ファイル有無に関するエラー制御(log_not_foundディレクティブ)
デフォルトでは応答すべきファイルがなかった場合、errorレベルのエラーが出力されるが、多数ファイルを配信するwebサーバのように、
存在しないファイルをリクエストされることが問題ない場合はoffにすることが個別にできる。
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'- $server_name - '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_not_found off;
server {
listen 80;
server_name www.example1.com;
root /appl/html;
access_log logs/access_example1.log;
error_log logs/error_example1.log warn;
}
}
server {
listen 80;
server_name www.example2.com;
root /appl/html2;
access_log logs/access_example2.log;
error_log logs/error_example2.log warn;
}
}
##2.プロセスの動作に関わる設計(すべてmainコンテキスト内指定)
■pidディレクティブ
nginxマスタープロセスのPIDファイルの格納先を指定する
■userディレクティブ
ワーカプロセスの実行ユーザを指定する。
■worker_processesディレクティブ
ワーカプロセスの数を指定する。一般的にはCPUのコア数と合わせるのが妥当とされている。
autoを指定することで自動的にCPUコア数の数のワーカプロセスが起動する。
■worker_rlimit_nofileディレクティブ
プロセスがオープンできるファイルディスクリプタの数にはOSで上限が敷かれている(nofile)が、
nginx側でも設定できる。設定するとしたらOSで設定した値以下で設定。
■eventsディレクティブ
ワーカのイベント駆動方式に関わるディレクティブ。省略は不可能。
具体的にはブロック付きにして下記のディレクティブを中に設定する。
・worker_connectionsディレクティブ
ワーカが処理できるコネクション数を指定する。コネクション数が不足した場合は、エラーログにworker_connections are not enoughと出力される
・useディレクティブ
利用するコネクションの処理方式を指定するが、nginxが自動的にシステムに最適なものを内部ロジックで選択するため基本指定不要。(Linux2.6以上ではepoll)
pid /run/nginx.pid;
user nginx;
worker_processes auto;
worker_rlimit_nofile 512;
events {
worker_connections 1000;
}
##3.パフォーマンスに関わる設計
■keepalive_timeoutディレクティブ
nginxに常時接続しているクライアントに対するタイムアウト時間
■sendfileディレクティブ
ファイルの読み込みとレスポンス送信にOSのsendfile()システムコールを実行。
これを使用すると、ファイルオープンしているファイルディスクリプタから直接クライアントに送信するため効率のよいファイル送信可能。
※ネットワークマウントされていたりVirtual Box環境で一部sendfile()システムコールが不具合ありのため、そういうケースの場合は無効化。
■tcp_nopushディレクティブ
sendfileディレクティブが有効な場合にLinux系ではTCP_CORKオプションが利用される。
このオプションを使用すると、最も大きなパケットサイズでレスポンスヘッダとファイルの内容を送信できるので、送信パケット数が最小にできる。
基本的に有効化したい。
■open_file_cacheディレクティブ
有効化した場合、nginxは一度オープンしたファイルディスクリプタやサイズ、更新時間といった情報をの情報を一定期間保存する。
open/closeが激しくされる環境では設定しておいた方が吉
■open_file_cache_errorsディレクティブ
open_file_cacheディレクティブを有効化した時にエラーの情報をキャッシュするためのパラメータ
keepalive_timeout 60s;
sendfile on;
tcp_nopush on;
open_file_cache max=1000 inactive=60;
open_file_cache_errors on
##4.静的webサイト構築
###1)静的コンテンツの公開
■locationディレクティブ
指定するURIごとの個別設定を定義可能。
下記の例だと、/contents1にマッチする場合はドキュメントルートが/var/www/contents1となる。
またマッチする仕組みとしては、
・条件にマッチする範囲がより厳密である(Longest Matching : より長い文字数にマッチした)設定が優先される
・^~修飾子(location ^~ /XXXXX)にマッチした場合はそれ以降のlocationディレクティブで評価されない。
・同じ優先度の場合は、上から順に評価。
■indexディレクティブ
ディレクトリにアクセスしたときに表示するページを定義する。
下記例だと/var/www/contents1のURIにアクセスした場合はmain.html、それが無ければindex.htmlが表示される。
■error_pageディレクティブ
error時に表示するwebページを定義する。error_page ステータスコード 表示するURI
下記例だとHTTPステータスコード404(リソースが存在しない)の時は、エラーページ/Error/error017.html;を表示する。
クライアントに返されるHTTPステータスコードは404。
404だがクライアントに対して例えば正常ステータスコード200で返したい時などは下記で対応できる。
error_page 404=200 /Error/error017.html;
server {
listen 80;
server_name example1 example1.fururun999.xyz;
root /appl/html1;
access_log /appl/html1/logs/access_example1.log main;
error_log /appl/html1/logs/error_example1.log warn;
location /contents1 {
root /var/www/contents1;
index main.html index.html;
}
error_page 404 /Error/error017.html;
}
###2)アクセス制御
■allow,denyディレクティブ
アクセス元IPアドレスを制御する。allowに許可するIPアドレスやCIDRを記載、denyには拒否するIPアドレスやCIDRを記載
下記例でURI /whitelistへのアクセスが10.2.0.0/24だけ許可するホワイトリスト方式(上にallow,最後にdeny all)、
URI /blacklistへのアクセスには192.168.100.0/24だけ拒否するブラックリスト方式(上にdeny,最後にallow all)。
■auth_basic,auth_basic_user_fileディレクティブ
最も簡単な認証方法であるパスワード認証を定義。ほぼ全ブラウザが対応しているので認証画面を特に作る必要なく利用可能。
auth_basic 認証領域名(任意);
auth_basic_user_file パスワードファイル名;
※パスワードファイルの生成
username:password
username:password:comment
→passwordはopenssl passwd [パスワード]で暗号化出力されたもので良いらしい
■limit_conn_zone,limit_connディレクティブ
同時接続最大数制限
特定のIPアドレスかあのnginxサーバに対する同時最大コネクション数を制限できる。
limit_conn_zoneで、カウントするキーとゾーン名、テーブルサイズを定義する。
下の例だと、リモートアドレスのテーブルをaddr_limitというゾーン名で共有メモリに10MB確保する。
そしてリモートアドレスあたり最大100コネクションまで許可する。100を超えた場合はHTTPステータスコード503(Service Unavailable)でレスポンスを返す。
■limit_req_zone,limit_reqディレクティブ
時間当たりの接続数制限
特定のIPアドレスかあのnginxサーバに対する時間当たりのコネクション数を制限できる。
limit_req_zoneで、リクエスト回数の最大レートを定義する。秒間の回数をrate=?r/sという形で指定。
limit_reqの方に、burstパラメータを指定すると指定した数のバースト許容分のリクエストをキューイングする。
キューイング許容数も超えたリクエストに対しては、HTTPステータスコード503(Service Unavailable)でレスポンスを返す。
※limit_connもlimit_reqも特定のIPアドレスに対しての制限数という形で発動するためDOSには対応できるが、
DDOSにはnginxでは対応できない。
http{
limit_conn_zone $binary_remote_addr zone=addr_limit:10m;
limit_req_zone $binary_remote_addr zone=addr_limit:10m rate=10r/s;
server {
listen 80;
server_name example1 example1.fururun999.xyz;
root /appl/html1;
access_log /appl/html1/logs/access_example1.log main;
error_log /appl/html1/logs/error_example1.log warn;
location /whitelist {
allow 10.2.0.0/24;
deny all;
}
location /blacklist {
deny 192.168.100.0/24;
allow all;
}
auth_basic test;
auth_basic_user_file /passwd/html1/.access;
location /conn_limit {
limit_conn addr_limit 100;
}
location /req_limit {
limit_req zone=addr_limit burst=50;
}
##5.通信のセキュリティ
HTTPSを利用するための設定を実施する。
HTTPSはTLSによって提供される暗号化通信でHTTP通信を行う技術である。HTTPSを利用することによって
通信内容を暗号化し、盗聴・改ざんを防止するだけでなく、正しいサーバであるかも認証することでなりすましを防ぐことができる。
-暗号化の流れは以下
1.サーバへリクエスト送信@クライアント
2.サーバ証明書をクライアントへ送付@サーバ
3.サーバ証明書からサーバ公開鍵を取得@クライアント
4.共通鍵生成@クライアント
5.サーバ公開鍵で共通鍵を暗号化@クライアント
6.サーバ公開鍵で暗号化した共通鍵をサーバへ送付@クライアント
7.暗号化された共通鍵をサーバ秘密鍵で複合@サーバ
8.クライアントへの送信データを共通鍵で暗号化して送付@サーバ
9.受信データを共通鍵で複合化@クライアント
※7で共通鍵を複合できるのはサーバの秘密鍵だけで秘密鍵はサーバしか持っていない点が重要
TLSを有効化するためにはlisternディレクティブにsslを付与する。
またSSL証明書をssl_certificateディレクティブ、SSL秘密鍵をssl_certificate_keyディレクティブへ指定する。
###2)安全なHTTPS通信のために
その他SSL/TLS関連のパラメータでチューニングすべき箇所も挙げる。
■SSL/TLSプロトコル
TLSv1.1も脆弱性が見つかっている(TLSv1.1を廃止するwebサイトも多数)ため、現在はTLSv1.2が妥当。
ssl_protocols [SSLv2] [SSLv3] [TLSv1] [TLSv1.1] [TLSv1.2]
■暗号化スイート
HTTPSには鍵認証、メッセージ認証符号、鍵交換、共通鍵暗号といった複数の要素の組み合わせがあり、
それぞれで適切なものを選択可能。どの暗号化スイートを指定するかでHTTPSの安全性は大きく変わる。
ssl_ciphers 暗号化スイートリスト(優先度が高い順に列挙。使用を拒否するものは先頭に(!));
下記Mozilla Wikiサイトでセキュリティレベルが高いものを指定する。
https://wiki.mozilla.org/Security/Server_Side_TLS
■サーバの暗号化スイート
TLSでは使用する暗号化スイートをクライアント・サーバ間で交渉し決定するが、デフォではクライアントが決定する暗号化スイートが優先される。
クライアントによって弱い強度が選択されないように、サーバの暗号化スイートを優先して選択する。
ssl_prefer_server_ciphers on;
###3)TTFB最小化によるHTTPS通信の高速化
TTFB(Time to First Byte)=最初の1byte目がクライアントが受信できるまでの時間
①HTTP/2による通信の暗号化
HTTP/2はHTTPの新しいバージョンとして策定されていて、通信経路の多重化、ヘッダ圧縮、パイプライニング技術により
TTFBを短縮している。HTTP/2はnginx1.9.5以上で利用可能である…
nginxで利用する場合、listenディレクティブにhttp2と付与。
②TLSセッション再開による高速化(セッションキャッシュ)
TLSハンドシェイクを行うとクライアント/サーバは共通のセッションIDをやりとりする。
セッションキャッシュはセッションIDをキーにしてサーバ側でセッション情報をキャッシュして次回のTLSハンドシェイクを省略する。
OPENSSL内蔵のキャッシュ(builtin)だとワーカごとにキャッシュが分裂してしまうが、
共有メモリのキャッシュ(shared)を利用することでワーカごとにセッションキャッシュを共有可能。キャッシュのサイズも指定できる。
ssl_session_cacheは下記のsampleだと共有メモリに10mのセッションキャッシュ領域を作れる。
またssl_session_timeoutでキャッシュのタイムアウト時間も指定。
③バッファサイズの最適化
nginxでは、TLS通信を行う際にレスポンスをバッファリングし一定サイズごとに暗号化を行う。
デフォルトではバッファサイズとして比較的大きなファイルサイズを意図して16kが指定されているが、
標準的なwebページではより小さくすることでTTFBを縮小できる。
ssl_buffer_sizeを変更。
④OCSPステープリング
サーバに接続する際、クライアントは証明書失効リスト(CRL)、またはOnline Certificate Status Protocol (OCSP)を使用してサーバー証明書の正当性を確認する必要がある。CRLの問題点は、リストが巨大化し、ダウンロードに非常に時間がかかることだ。OCSPは非常に軽量であり、単一のレコードのみを取得する。しかし、副作用として、サーバに接続する際、OCSP要求をOCSP responderに対して行わなければならない。
これを解決するために、TLSハンドシェイク中にキャッシュされたOCSPレコードをサーバが送信することを可能にする。これによりOCSP responderをバイパスすることになる。これにより、クライアントとOCSP responder間のラウンドトリップ時間をなくすことができ、この機能はOCSP Staplingと呼ばれる。
OCSP Stapling(ステープリング)では、クライアントがOCSP要求を行うのではなく、サーバがOCSP要求を行い、その応答をキャッシュする。サーバはキャッシュしたOCSP応答を、サーバ証明書と一緒にクライアントに送信する。
ssl_stapling on; #有効化
resolver 8.8.8.8; #OCSPレスポンダを探すためのDNS
server {
listen 443 ssl https;
server_name example2 example2.fururun999.xyz;
root /appl/html2;
location /test/ {
autoindex on;
}
access_log /appl/html2/logs/access_example2.log main;
error_log /appl/html2/logs/error_example2.log warn;
ssl_certificate /ssl/server.crt;
ssl_certificate_key /ssl/server.key;
ssl_protocols TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_buffer_size 4k;
ssl_stapling on;
resolver 8.8.8.8;
}
#サーバ秘密鍵の作成
-RSA形式、256bitのAES形式、かつ2048bitの鍵長で作成
openssl genrsa -aes256 2048 > server.key
#CSRの作成
req CSRファイル作成
-new 新規にCSR作成
-key 秘密鍵ファイル指定
openssl req -new -key server.key > server.csr
#サーバ証明書作成
x509 X.509形式証明書
-in CSRファイル CSRファイル指定
-days 証明書有効期限
-req 入力ファイルがCSRであること明示
-signkey秘密鍵 秘密鍵ファイル指定
openssl x509 -in server.csr -days 3650 -req -signkey server.key > server.crt
Tips:サーバ秘密鍵からパスフレーズ削除
cp -p server.key server.key_org
openssl rsa -in server.key_org > server.key
#プロキシが前段にある場合のアクセス元IPの取得方法
nginxをwebサーバとして運用する際によくある例で、前段にロードバランサー等を敷いて負荷分散させる(例:AWSのELB)がその際にアクセス元IPやロードバランサー自体のIPアドレスを拾うには多少の工夫がいるようなのでその方法をまとめる。