Ubuntu 14.04でNginxとPhp-fpmを使用して複数のウェブサイトを安全にホストする方法
紹介
LEMPスタック(Linux、nginx、MySQL、PHP)がPHPサイトの実行において比類のない速度と信頼性を提供することはよく知られています。ただし、この人気のスタックの他の利点であるセキュリティや分離はそれほど人気がありません。
この記事では、異なるLinuxユーザーでLEMP上にサイトを運営することのセキュリティと分離の利点を示します。これは、各nginxサーバーブロック(サイトまたは仮想ホスト)用に異なるphp-fpmプールを作成することによって行います。
前提条件
このガイドはUbuntu 14.04でテストされています。説明されたインストールと設定は他のOSやOSバージョンでも似ていると思われますが、コマンドや設定ファイルの場所は異なる場合があります。
また、nginxとphp-fpmがすでにセットアップされていることを前提としています。もしそうでない場合は、記事「Ubuntu 14.04にLinux、nginx、MySQL、PHP(LEMP)スタックをインストールする方法」のステップ1とステップ3に従ってください。
このチュートリアルのすべてのコマンドは、非ルートユーザーとして実行する必要があります。コマンドにルートアクセスが必要な場合は、sudoの前に付けます。まだ設定していない場合は、次のチュートリアルに従ってください: Ubuntu 14.04による初期サーバー設定。
テストのためには、デフォルトのlocalhostに加えて、Dropletを指す完全修飾ドメイン名(fqdn)が必要です。手元にない場合は、site1.example.orgを使用できます。お気に入りのエディタで/etc/hostsファイルを次のように編集します:sudo vim /etc/hosts、そしてこの行を追加します(使用している場合はsite1.example.orgをあなたのfqdnに置き換えてください):
...
127.0.0.1 site1.example.org
...
LEMPを追加で確保する理由
一般的なLEMP設定では、すべてのサイトのすべてのPHPスクリプトを同じユーザーで実行するphp-fpmプールが一つだけ存在します。これには二つの主要な問題があります:
- もし1つのnginxサーバーブロック、つまりサブドメインや別のサイトのウェブアプリケーションが侵害されると、このDroplet上のすべてのサイトにも影響が及びます。攻撃者は他のサイトの設定ファイル(データベースの詳細を含む)を読み取ったり、ファイルを変更したりすることができます。
- もしユーザーにあなたのドロップレット上のサイトへのアクセスを許可したい場合、実質的に彼にすべてのサイトへのアクセスを与えることになります。たとえば、あなたの開発者がステージング環境で作業する必要がある場合です。しかし、非常に厳しいファイル権限を設定しても、同じドロップレット上のメインサイトを含むすべてのサイトへのアクセスを与えることになります。
上記の問題は、各サイトごとに異なるユーザーで実行される別のプールを作成することによって、php-fpmで解決されます。
ステップ1 — php-fpmの設定
もし必要な前提条件を満たしていれば、すでにDroplet上に1つの動作するウェブサイトがあるはずです。独自のFQDNを指定していなければ、ローカルではlocalhostで、リモートではDropletのIPアドレスでアクセスできるはずです。
次に、独自のphp-fpmプールとLinuxユーザーを持つ2つ目のサイト(site1.example.org)を作成します。
必要なユーザーを作成することから始めましょう。最良の隔離を確保するために、新しいユーザーは自分自身のグループを持つべきです。まず、ユーザーグループsite1を作成しましょう:
- sudo groupadd site1
それでは、このグループに属するユーザーサイト1を作成してください:
- sudo useradd -g site1 site1
これまでのところ、新しいユーザーサイト1にはパスワードがなく、Dropletにログインできません。このサイトのファイルに直接アクセスする必要がある場合は、sudo passwd site1コマンドを使用してこのユーザーのパスワードを作成する必要があります。新しいユーザー/パスワードの組み合わせを使用することで、ユーザーはsshまたはsftpでリモートログインできます。詳細やセキュリティに関する情報については、「制限されたディレクトリアクセスを持つ二次SSH/SFTPユーザーの設定」という記事を確認してください。
次に、site1 のために新しい php-fpm プールを作成します。php-fpm プールは、その本質において、特定のユーザー/グループの下で実行される通常の Linux プロセスであり、Linux ソケットで待機しています。また、IP:ポートの組み合わせで待機することもできますが、これはより多くの Droplet リソースを必要とし、推奨される方法ではありません。
デフォルトでは、Ubuntu 14.04のすべてのphp-fpmプールは、ディレクトリ/etc/php5/fpm/pool.d内のファイルで構成されるべきです。このディレクトリ内の.conf拡張子のファイルは、php-fpmのグローバル設定に自動的に読み込まれます。
それでは、新しいサイトのために新しいファイル /etc/php5/fpm/pool.d/site1.conf を作成しましょう。お気に入りのエディタを使って、次のように作成できます:
- sudo vim /etc/php5/fpm/pool.d/site1.conf
このファイルには次のものが含まれているべきです:
[site1]
user = site1
group = site1
listen = /var/run/php5-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = off
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chdir = /
上記の構成では、これらの特定のオプションに注意してください:
[site1]はプールの名前です。各プールにはユニークな名前を指定する必要があります。userとgroupは、新しいプールが実行される Linux のユーザーとグループを表します。listenは各プールのユニークな位置を指すべきです。- listen.ownerとlisten.groupはリスナーの所有権、つまり新しいphp-fpmプールのソケットを定義します。Nginxはこのソケットを読み取ることができなければなりません。だからこそ、ソケットはnginxが実行されるユーザーとグループ、つまりwww-dataの下で作成されます。
php_admin_valueを使用すると、カスタムのphp設定値を設定できます。私たちは、Linuxコマンドを実行できる関数 -exec,passthru,shell_exec,systemを無効にするためにそれを使用しました。php_admin_flagはphp_admin_valueに似ていますが、これは真偽値のスイッチ、すなわちオンとオフだけです。攻撃者によって使用される可能性のあるリモートファイルを開くことを許可する PHP スクリプトの PHP 関数allow_url_fopenを無効にします。
注意: 上記の php_admin_value および php_admin_flag の値は、グローバルに適用することも可能です。ただし、サイトによってはそれらが必要な場合があり、デフォルトでは設定されていません。php-fpmプールの魅力は、各サイトのセキュリティ設定を微調整できることにあります。さらに、これらのオプションはセキュリティの範囲外の他のphp設定にも使用でき、サイトの環境をさらにカスタマイズすることができます。
この pm オプションは現在のセキュリティトピックの外にありますが、プールのパフォーマンスを設定できることを知っておくべきです。
オプションchdirは、ファイルシステムのルートである/であるべきです。この設定は、別の重要なオプションchrootを使用しない限り変更されるべきではありません。
chrootオプションは、上記の設定に故意に含まれていません。これは、プールを監獄環境、すなわちディレクトリ内にロックされた状態で実行することを可能にします。これはセキュリティ上素晴らしいことで、プールをサイトのWebルート内にロックできるからです。しかし、この究極のセキュリティは、システムバイナリやImagemagickなどのアプリケーションに依存する優れたPHPアプリケーションに深刻な問題を引き起こします。これらは利用できなくなります。このトピックにさらに興味がある場合は、「監獄環境でWordPressインストールを設定するためのFirejailの使い方」という記事をお読みください。
上記の設定が完了したら、新しい設定を反映させるために次のコマンドでphp-fpmを再起動してください:
- sudo service php5-fpm restart
新しいプールが正しく動作していることを確認するには、次のようにプロセスを検索します:
- ps aux |grep site1
ここまで正確な指示に従ったのであれば、次のような出力が表示されるはずです:
site1 14042 0.0 0.8 133620 4208 ? S 14:45 0:00 php-fpm: pool site1
site1 14043 0.0 1.1 133760 5892 ? S 14:45 0:00 php-fpm: pool site1
赤色の部分は、プロセスまたはphp-fpmプールが実行されているユーザー - site1です。
さらに、opcacheによって提供されるデフォルトのphpキャッシングを無効にします。この特定のキャッシング拡張機能はパフォーマンスに優れていますが、後で見るようにセキュリティには適していません。それを無効にするには、スーパーユーザー権限でファイル/etc/php5/fpm/conf.d/05-opcache.iniを編集し、次の行を追加します:
opcache.enable=0
それから、設定を適用するために再びphp-fpmを再起動します(sudo service php5-fpm restart)。
ステップ 2 — nginxの設定
サイトのためにphp-fpmプールを設定したら、nginxでサーバーブロックを設定します。そのために、お好きなエディタを使って次のように新しいファイル/etc/nginx/sites-available/site1を作成してください:
- sudo vim /etc/nginx/sites-available/site1
このファイルには次のものが含まれているべきです:
server {
listen 80;
root /usr/share/nginx/sites/site1;
index index.php index.html index.htm;
server_name site1.example.org;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php5-fpm-site1.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
上記のコードは、nginxのサーバーブロックの一般的な設定を示しています。興味深いハイライト部分に注意してください:
- ウェブルートは
/usr/share/nginx/sites/site1です。 - サーバー名は、この記事の前提条件で言及されている完全修飾ドメイン名
site1.example.orgを使用しています。 fastcgi_passは、phpファイルのハンドラーを指定します。すべてのサイトには、/var/run/php5-fpm-site1.sockのような異なるunixソケットを使用するべきです。
ウェブルートディレクトリを作成します:
- sudo mkdir /usr/share/nginx/sites
- sudo mkdir /usr/share/nginx/sites/site1
上記のサイトを有効にするには、ディレクトリ/etc/nginx/sites-enabled/にそのシンボリックリンクを作成する必要があります。これは次のコマンドで行うことができます:
- sudo ln -s /etc/nginx/sites-available/site1 /etc/nginx/sites-enabled/site1
最後に、変更を適用するためにnginxを以下のように再起動してください:
- sudo service nginx restart
ステップ3 — テスト
テストを実行するために、PHP環境に関する詳細な情報を提供するよく知られたphpinfo関数を使用します。info.phpという名前の新しいファイルを作成し、その内容としてのみ<?php phpinfo(); ?>の行を含めます。このファイルは、デフォルトのnginxサイトとそのウェブルート/usr/share/nginx/html/に最初に必要です。この目的のために、以下のようなエディタを使用することができます:
- sudo vim /usr/share/nginx/html/info.php
その後、ファイルを他のサイト(site1.example.org)のウェブルートに次のようにコピーしてください:
- sudo cp /usr/share/nginx/html/info.php /usr/share/nginx/sites/site1/
さあ、サーバーユーザーを確認するための最も基本的なテストを実行する準備が整いました。ブラウザまたはDropletのターミナルからコマンドラインブラウザのlynxを使用してテストを実行できます。まだDropletにlynxがインストールされていない場合は、sudo apt-get install lynxというコマンドでインストールしてください。
最初に、デフォルトサイトの info.php ファイルを確認してください。これは、以下のようにローカルホストからアクセスできるはずです:
- lynx --dump http://localhost/info.php |grep 'SERVER\["USER"\]'
上記のコマンドでは、出力をgrepでフィルタリングして、サーバーユーザーを表す変数SERVER["USER"]のみにしています。デフォルトのサイトでは、出力には次のようにデフォルトのwww-dataユーザーが表示される必要があります:
_SERVER["USER"] www-data
同様に、次に site1.example.org のサーバーユーザーをチェックしてください:
- lynx --dump http://site1.example.org/info.php |grep 'SERVER\["USER"\]'
出力にはこの時にsite1ユーザーが表示されるはずです。
_SERVER["USER"] site1
PHP-FPMプールごとにカスタムPHP設定を行った場合は、上記の方法で興味のある出力をフィルタリングすることによって、それらの対応する値を確認することもできます。
これまでのところ、私たちの2つのサイトが異なるユーザーで実行されていることが分かっていますが、次に接続を保護する方法を見てみましょう。この記事で解決しようとしているセキュリティの問題を示すために、機密情報を含むファイルを作成します。通常、そのようなファイルにはデータベースへの接続文字列が含まれ、データベースユーザーのユーザー名とパスワードの詳細が記載されています。誰かがその情報を知ることができれば、その人は関連するサイトに対して何でもできてしまいます。
お気に入りのエディタを使って、メインサイトに新しいファイルを作成してください /usr/share/nginx/html/config.php。そのファイルには以下の内容を含める必要があります:
<?php
$pass = 'secret';
?>
上記のファイルでは、secretという値を保持するpassという変数を定義しています。もちろん、このファイルへのアクセスを制限したいので、ファイルの権限を400に設定します。これにより、ファイルの所有者にのみ読み取り権限が与えられます。
パーミッションを400に変更するには、次のコマンドを実行します:
- sudo chmod 400 /usr/share/nginx/html/config.php
また、私たちのメインサイトはユーザーwww-dataの下で実行されており、このファイルを読むことができるはずです。したがって、このようにファイルの所有権をそのユーザーに変更してください:
- sudo chown www-data:www-data /usr/share/nginx/html/config.php
この例では、秘密情報を読み取り、出力するために別のファイルである/usr/share/nginx/html/readfile.phpを使用します。このファイルには以下のコードが含まれている必要があります。
<?php
include('/usr/share/nginx/html/config.php');
print($pass);
?>
このファイルの所有権をwww-dataにも変更してください。
- sudo chown www-data:www-data /usr/share/nginx/html/readfile.php
ウェブルートで全ての権限と所有権が正しいことを確認するために、コマンドls -l /usr/share/nginx/html/を実行してください。以下のような出力が表示されるべきです:
-r-------- 1 www-data www-data 27 Jun 19 05:35 config.php
-rw-r--r-- 1 www-data www-data 68 Jun 21 16:31 readfile.php
今、デフォルトサイトでコマンドlynx --dump http://localhost/readfile.phpを使って後者のファイルにアクセスしてください。出力にsecretが印刷されているのが見えるはずで、これは機密情報を含むファイルが同じサイト内でアクセス可能であることを示しており、これは期待される正しい動作です。
次に、ファイル /usr/share/nginx/html/readfile.php をあなたの第二のサイト site1.example.org にこのようにコピーしてください:
- sudo cp /usr/share/nginx/html/readfile.php /usr/share/nginx/sites/site1/
サイト/ユーザーの関係を整理するために、各サイト内でファイルがそれぞれのサイトユーザーによって所有されていることを確認してください。これは、新しくコピーされたファイルの所有権を site1 に変更することで行います。コマンドは次の通りです:
- sudo chown site1:site1 /usr/share/nginx/sites/site1/readfile.php
正しい権限とファイルの所有者が設定されていることを確認するために、コマンドls -l /usr/share/nginx/sites/site1/を使ってsite1のウェブルートの内容をリストしてください。次のような出力が表示されるはずです:
-rw-r--r-- 1 site1 site1 80 Jun 21 16:44 readfile.php
次に、lynx --dump http://site1.example.org/readfile.php コマンドを使って site1.example.com から同じファイルにアクセスしてみてください。空のスペースしか表示されません。さらに、sudo grep error /var/log/nginx/error.log コマンドを使用して nginx のエラーログにエラーを検索すると、次のようになります。
2015/06/30 15:15:13 [error] 894#0: *242 FastCGI sent in stderr: "PHP message: PHP Warning: include(/usr/share/nginx/html/config.php): failed to open stream: Permission denied in /usr/share/nginx/sites/site1/readfile.php on line 2
注意: php-fpm 設定ファイル /etc/php5/fpm/php.ini で display_errors が On に設定されている場合、lynx の出力にも同様のエラーが表示されることがあります。
警告は、site1.example.org のスクリプトがメインサイトのセンシティブなファイル config.php を読み取れないことを示しています。したがって、異なるユーザーで実行されるサイトは、お互いのセキュリティを侵害することはできません。
この記事の設定部分の最後に戻ると、opcacheによって提供されるデフォルトのキャッシュを無効にしたことがわかります。なぜそうしたのか気になる場合は、スーパーユーザー権限でファイル/etc/php5/fpm/conf.d/05-opcache.iniにopcache.enable=1を設定してopcacheを再度有効にし、コマンドsudo service php5-fpm restartでphp5-fpmを再起動してみてください。
驚くべきことに、テスト手順を全く同じ順序で再実行すると、その所有権や権限に関係なく、機密ファイルを読むことができるようになります。このopcacheの問題は長い間報告されていますが、この記事の時点ではまだ修正されていません。
結論
セキュリティの観点から、同じNginxウェブサーバー上の各サイトに対して異なるユーザーを持つphp-fpmプールを使用することが不可欠です。たとえ少しパフォーマンスのペナルティがあったとしても、そのような隔離の利点は重大なセキュリティ侵害を防ぐことができるでしょう。
この記事で述べられているアイデアは独自のものではなく、SuPHPのような他の類似のPHP隔離技術にも存在します。しかし、他のすべての代替手段のパフォーマンスはphp-fpmのそれよりもはるかに劣っています。
