前回(目次):
次回:
1. 今回の目標
リクエストごとに phpを実行し、 /var/lib/BBASN/test.txt に "Hello!\n" を追記する。
(ちなみに、BBASN は Block By AS Number の略である。)
2. 設計
2-1. 最初に結論
外部からのリクエストごとにプログラムを動かしたい場合、 Apache の RewriteMap という仕組みが利用できる。
RewriteMap を利用して動かすプログラムは、 Apache 起動時から常駐して動き、標準入出力を通じて Apacheとやり取りをするので、図2-1 のようなフローチャートになっている必要がある。
2-2. RewriteMap で呼ぶプログラムについて
Apache公式ドキュメントの RewriteMap の説明ページのprg見出し1には、次のように書かれている。
When a MapType of
prgis used, the MapSource is a filesystem path to an executable program which will providing the mapping behavior. This can be a compiled binary file, or a program in an interpreted language such as Perl or Python.
(引用者訳: (RewriteMapで、)マップタイプprgを利用すると、マップソースに実行可能なプログラムのファイルパスを指定することが出来ます。 マップソースにはバイナリファイルか、またはPerl や Python のようなインタプリタ言語のプログラムを指定できます)
phpはインタプリタ言語なので、 RewriteMap マップタイプ prg から呼び出すことが可能である。
ドキュメント1には、次のようにも書かれている。
This program is started once, when the Apache HTTP Server is started, and then communicates with the rewriting engine via STDIN and STDOUT. That is, for each map function lookup, it expects one argument via STDIN, and should return one new-line terminated response string on STDOUT.
(引用者やや意訳: マップソースに指定されたプログラムは、Apacheが起動したタイミングで一度だけ開始されます。プログラムは、標準入出力を通じて Apache と通信します。つまり Apacheは、プログラムを呼び出したいタイミングでプログラムに標準入力を渡し、改行で終わる標準出力をプログラムから受け取ろうとします。)
プログラムは「毎回呼び出される」のではなく、Apache が起動したタイミングで1回起動するだけであることに注意が必要である。
つまりプログラムは「ずっと起動しっぱなし」にしておく必要があり、
- Apacheから 標準入力されるまで待つ
- 標準入力を受け取ったら、任意の処理をして、標準出力をApacheに返す
- (1に戻る)
という仕組みにする必要がある。(図2-1のとおり)
例えば
<?php
file_put_contents("/var/lib/BBASN/test.txt", "Hello!\n", FILE_APPEND);
というプログラムを RewriteMap に登録すると、Apache(再)起動時に一回 Hello! が書き込まれるだけである。
つまり、外部からのリクエストごとに Hello! がどんどん追記されていくわけではない。
外部からのリクエストごとに Hello! を追記したいなら、次のようにする必要がある。
<?php
while (($dummy = fgets(STDIN)) !== false){
    file_put_contents("/var/lib/BBASN/test.txt", "Hello!\n", FILE_APPEND);
    echo "OK\n";
    fflush(STDOUT);
}
phpにはバッファリングの仕組みがあり、デフォルトでは直ちに標準出力を返さないので、fflush(STDOUT); することで強制的に標準出力を返させている。
2-3. RewriteMap での呼び出し方について
2-3-1. 最初に結論
RewriteEngine On
RewriteMap (マップ名) "prg:/usr/bin/sudo -u apache -g apache (プログラムの起動コマンド)"
RewriteCond ${(マップ名):} dummy
RewriteRule ^ -
のようにすれば、apacheユーザ、apacheグループで、プログラムを呼ぶことが出来る。
マップ名は任意である。
2-3-2. プログラムの実行者 について
sudo -u apache -g apache を指定しないと Apache を起動した ユーザ:グループでプログラムを呼んでしまう。
なので、 sudo systemctl (re)start httpd により Apache を(再)起動している場合、プログラムが root権限で実行されることになる。
その場合、プログラムに脆弱性があって 第三者が勝手に OSコマンドを実行できるような場合に、 root権限を掌握されてしまい、大変危険だ。なので apache:apache権限で実行するように指定したほうが良いだろう。
(また、3章で Apacheの動作ユーザ:グループを確認するので、 これが apache:apache でない場合は正しいものに置き換えること。)
蛇足: 実行者を変更するもう一つの方法
RewriteMap (マップ名) "prg:/usr/bin/sudo -u apache -g apache (プログラムの起動コマンド)"
では、 sudo により実行者を変更しているが、 RewriteMap 自体にも実行者を変更する方法がある。それは
RewriteMap (マップ名) "prg:(プログラムの起動コマンド)" apache:apache
のようにして、 RewriteMap の第3引数に 動作ユーザ:グループ を指定することだ。
この仕様は 公式ドキュメント1 にも
By default, external rewriting programs are run as the user:group who started httpd. This can be changed on UNIX systems by passing user name and group name as third argument to RewriteMap in the
username:groupnameformat.
(引用者訳: 特に指定がない限り、 外部プログラムは Apacheを起動した ユーザ:グループ で起動します。RewriteMapの3番目の引数にユーザ名:グループ名の形式で指定することで、起動する ユーザとグループを変更できます。 )
と明記されている。
しかし、この方法では、実行ユーザのセカンダリグループが読み込まれない問題が起きることが分かっている。
詳しくは こちら の記事を読んでほしい
(蛇足 ここまで)
2-3-3. RewriteCond ${(マップ名):} dummy について
RewriteCond ${(マップ名):} dummy については、
(マップ名)に対応するプログラムの標準出力が、正規表現 dummy にマッチするかを確認している。
つまり、ここでプログラムを呼んでいる。
マッチした場合、直後の ディレクティブ RewriteRule が実行され、
マッチしない場合は実行されない。
2-3-4. RewriteRule ^ - について
RewriteRule ^ - は、「何も行わない」ことを指定している。
要はダミー。
RewriteCond は RewriteRule とセットで利用しないと動作が不安定になるので、
ダミーの RewriteRule を入れておく必要がある。
蛇足: 動作が不安定になる例
例えば、
Include /etc/httpd/conf/test.conf
RewriteEngine On
RewriteRule ^(.*)$ ./index.php?id=$1
RewriteEngine On
RewriteCond ${(マップ名):} dummy
として、
ドキュメントルートに index.php だけおいて、
https://localhost/1234 にアクセスした場合。
(1). (マップ名)に対応するプログラムの標準出力が、正規表現 dummy にマッチする場合
→ httpd.conf の RewriteRule ^(.*)$ ./index.php?id=$1 が実行される。
→ https://localhost/index.php?id=1234 へアクセスできる
(2). マッチしない場合
→ RewriteRule ^(.*)$ ./index.php?id=$1 が実行されない
→ 404 Not Found エラーになってしまう
test.conf で、 外側の httpd.conf の設定まで意識することはほとんどないだろうから、
RewriteCond の単独指定は、「予期しないところで、別のファイルで定義した RewriteRule を一つ消してしまう」危険性がある。
(蛇足 ここまで)
対応する RewriteRule がマッチしない場合、RewriteCond が評価されない ので注意する必要がある。
例えば
RewriteCond ${(プログラム名):(標準入力)} (パターン)
RewriteRule ^マッチしないパターン$ /dummy
として、 リクエストが ^マッチしないパターン$ にマッチしない場合、そもそも (プログラム)に (標準入力) が渡されない。
詳しくは こちら の記事を読んでほしい
2-3-5. バーチャルホストを定義している場合の、Rewrite~ の設定についての注意
Rewrite~ の設定は、グローバルに定義しても、
バーチャルホストには引き継がれないので注意が必要である。
例えば、
LISTEN 80
LISTEN 443
LISTEN 8080
RewriteEngine On
RewriteMap (マップ名) "prg:/usr/bin/sudo -u apache -g apache (プログラムの起動コマンド)"
RewriteCond ${(マップ名):} dummy
RewriteRule ^ -
<VirtualHost *:80>
Header always set X-port "80"
</VirtualHost>
<VirtualHost *:443>
Include /path/to/SSL設定.conf
Header always set X-port "443"
</VirtualHost>
のようにした場合、通常のhttp通信(80番ポート)やhttps通信(443番ポート)では Rewrite 周りの設定が効かない。
明示的に curl -I http://localhost:8080/ のように 8080番ポートを指定した場合にのみ、どの <VirtualHost> 設定にもマッチしないため、グローバルに定義された Rewrite 周りの設定が効く。
どのポートでも Rewriteの設定を有効にしたい場合は、
- 各 <VirtualHost>の中でRewriteOptions Inherit宣言をする
- 各 <VirtualHost>の中で毎回Rewriteの設定を明示する- こちらの場合、別ファイルにまとめて Includeする方法を取ると可読性が向上する
 
- こちらの場合、別ファイルにまとめて 
の2つの方法がある。
但し RewriteOptions Inherit 宣言をしても、その <VirtualHost> 内で別の Rewrite がある場合、<VirtualHost> 内の Rewrite だけが有効になり、 グローバルに定義された Rewrite は結局のところ無視されるようであった。(ちょっと複雑すぎて正確なことはわからなかったが)
なので、各 <VirtualHost> 内で毎回 Rewrite の設定を明示したほうが安全そうだ。
これは Include を使ってまとめることが出来る。
LISTEN 80
LISTEN 443
LISTEN 8080
Include /etc/httpd/conf/map.conf
<VirtualHost *:80>
Include /etc/httpd/conf/map.conf
Header always set X-port "80"
</VirtualHost>
<VirtualHost *:443>
Include /etc/httpd/conf/map.conf
Include /path/to/SSL設定.conf
Header always set X-port "443"
</VirtualHost>
RewriteEngine On
RewriteMap (マップ名) "prg:/usr/bin/sudo -u apache -g apache (プログラムの起動コマンド)"
RewriteCond ${(マップ名):} dummy
RewriteRule ^ -
3. Apache のユーザ、グループを確認する
万が一Apache が root ユーザで動いていたりしたら非常に危険である。
Webアプリに脆弱性があり、第三者が勝手に OSコマンドを実行できるような場合に、このコマンドは Apacheの動作している権限で実行されるからだ。これが root である場合、システム権限をすべて乗っ取られてしまう。
そこで、まずは sudo systemctl start httpd (環境によっては sudo systemctl start apache2。この場合、今後は httpd を apache2 と読み替える事) にて Apacheを起動してほしい。
次に、Apacheを起動した状態で次のコマンドを実行して、Apacheがどのユーザ:グループで動いているかを確認する。
sudo ps -eo pid,ppid,user,group,comm | { head -n 1; grep httpd; }
このコマンドの結果は
 PID  PPID USER   GROUP  COMMAND
  ◯     1 root   root   httpd
  △    ◯   ☆      ★   httpd
  ◇    ◯   ☆      ★   httpd
   :
のようになっているはずであり、
☆や★が Apacheの動作しているユーザ:グループである。
Apacheの動作しているユーザやグループ が root の場合、Apache用のユーザ:グループ apache:apache を作成して、このユーザ:グループ で Apacheを起動するように修正する必要がある。
(今回、その方法は解説しない。)
また、今回 Apacheを動かすユーザ:グループは apache:apache であることを前提として解説を進めるので、
それ以外の場合 (例えば www-data:www-dataなど) は適宜読み替えること。
Apache 動作ユーザ確認コマンドの解説
sudo ps -eo pid,ppid,user,group,comm | { head -n 1; grep httpd; }
の結果の具体例は次のとおり。
    PID    PPID USER     GROUP    COMMAND
 363043       1 root     root     httpd
 363048  363043 apache   apache   httpd
 363049  363043 apache   apache   httpd
 363050  363043 apache   apache   httpd
 363051  363043 apache   apache   httpd
 363231  363043 apache   apache   httpd
PPID (Parent Process ID ― 親プロセスのID)が 1である行は、親プロセスが存在しない事を意味する。
このようなプロセスを 「initプロセス」と言ったりする。
今回の場合は、sudo systemctl start httpd が直接作ったプロセスである。
sudo なので、このプロセスが USER=root, GROUP=root なのは当然だ。
このプロセスの PID (Process ID) である 363043 が、 initプロセス以外の全ての結果の PPID となっている。
これは、 initプロセス 以外の全ての httpd のプロセスが、 initプロセス の子プロセスであることを意味している。
initプロセス の子プロセスは、すべて USER=apache, GROUP=apache となっている。・・・①
一方、 Apache は必ずマルチプロセスで動いている2。
よって、 Apacheが起動すると、Apacheは子プロセスを複数作り、子プロセスで実際の処理を行う。・・・②
①②よりApacheの実際の処理を行っているユーザ:グループは apache:apache であることが分かる。
(Apache 動作ユーザ確認コマンドの解説 ここまで)
4. 作ってみる
リクエストごとに phpを実行し、 /var/lib/BBASN/test.txt に "Hello!\n" を追記する仕組みを作ってみる。
- 
3章のことをまず最初に必ず実行する 
 
- 
次のコマンドを実行し、 グループ bbasnを作って、そこに ユーザapacheを登録する。sudo groupadd bbasn sudo usermod -aG bbasn apache- (apacheの部分は、3章で確認した Apacheの動作ユーザに応じて書き換えること)
 
 
- (
- 
次のコマンドを実行し、ディレクトリ /var/lib/BBASNを作る。
 所有ユーザapache, 所有グループbbasnで 所有ユーザはフルアクセス可能(7)、所有グループは読み取りと実行可能(5)、 それ以外の (rootを除く) ユーザは一切のアクセスを不能(0)にする。sudo mkdir /var/lib/BBASN sudo chown apache:bbasn /var/lib/BBASN sudo chmod 750 /var/lib/BBASN- (apacheの部分は、3章で確認した Apacheの動作ユーザに応じて書き換えること)
- 
/var/lib/(アプリ名)は一般に可変データを置く場所。 アプリが自由に利用できるべきなので、今回/var/lib/BBASNに対する Apache のフルアクセスを認めた。
- 将来、Apache 以外にも、BBASN に関わる他のユーザが データを利用する運用になるかもしれない。そこで グループ bbasnに属する (apache以外の) ユーザにも読み取りと実行を認めている。
 彼らは ディレクトリ/var/lib/BBASNを:- 読み取り可能である。 (lsコマンドなどで中にあるファイルの一覧を見れる)
- 書き込み可能でない。 ( /var/lib/BBASN直下にファイルやディレクトリを作成したり、/var/lib/BBASN直下のファイルやディレクトリに対して 名前変更したり、削除したりすることはできない。
- 実行可能である。(/var/lib/BBASN直下にあるファイルやディレクトリにアクセスできる。 それら自身のパーミッションが許せば。)
 
- 読み取り可能である。 (
- グループ bbasnに所属しない一般ユーザは、/var/lib/BBASN以下の全てのファイルやディレクトリにアクセスできない。
 
 
- (
- 
次のコマンドを実行し、ディレクトリ /usr/local/bin/BBASNを作る。
 所有ユーザroot, 所有グループbbasnで 所有ユーザはフルアクセス可能(7)、所有グループは読み取りと実行可能(5)、 それ以外のユーザは一切のアクセスを不能(0)にする。sudo mkdir /usr/local/bin/BBASN sudo chown root:bbasn /usr/local/bin/BBASN sudo chmod 750 /usr/local/bin/BBASN- 
/usr/local/bin/(アプリ名)は一般に実行ファイルを置く場所。基本的に誰にも変更されたくないため、今回/usr/local/bin/BBASNの 所有ユーザをrootとし、root以外の書き込みを禁止した。- (まあ、/usr/local/bin/BBASNが書き込み禁止でも、/usr/local/bin/BBASN/ファイルが書き込み可能なら、/usr/local/bin/BBASN/ファイルの変更ができてしまうのではあるが…③)
 
- (まあ、
- BBASN を実際に動かすユーザは /usr/local/bin/BBASN以下にアクセスできる必要がある。そこで、 所有グループbbasnにはディレクトリの読み取り権限と実行権限を認めた。- (③のことより、 グループ bbasnに所属するユーザが/usr/local/bin/BBASNの中にあるファイルを変更でき得ることには注意が必要だ。)
 
- (③のことより、 グループ 
- グループ bbasnに所属しない一般ユーザは、/usr/local/bin/BBASN以下の全てのファイルやディレクトリにアクセスできない。- (こちらは③の問題は発生しない。実行不可能なディレクトリ内部のすべてのファイルやディレクトリにはそもそも如何なるアクセスもできないからだ)    
 
 
- (こちらは③の問題は発生しない。実行不可能なディレクトリ内部のすべてのファイルやディレクトリにはそもそも如何なるアクセスもできないからだ)    
 
- 
- 
sudo nano /usr/local/bin/BBASN/ipcheck.phpを実行し、
 次のような/usr/local/bin/BBASN/ipcheck.phpを作る。/usr/local/bin/ipcheck.php<?php while (($dummy = fgets(STDIN)) !== false){ file_put_contents("/var/lib/BBASN/test.txt", "Hello!\n", FILE_APPEND); echo "OK\n"; fflush(STDOUT); }
- 
次のコマンドを実行し、 /usr/local/bin/BBASN/ipcheck.phpの所有者とパーミッションを変更する。
 所有ユーザapache, 所有グループapacheで 所有ユーザは読み取り可能 (4)、それ以外は一切のアクセスを不能(0)にする。sudo chown apache:apache /usr/local/bin/BBASN/ipcheck.php sudo chmod 400 /usr/local/bin/BBASN/ipcheck.php- (apacheの部分は、3章で確認した Apacheの動作ユーザに応じて書き換えること)
- この phpプログラム は apacheだけが実行するので、apache以外によるアクセスは不要。
- ややこしいが、phpファイルを phpから実行する場合、 読み取り権限さえあれば十分で、実行権限は必要ない。
- 以上で phpプログラムを用意し、 "Hello!\n"の書き込み先である/var/lib/BBASN/test.txtの作成、追記を可能にしたので、手順7以降は Apache 側の設定をする。
 
 
- (
- 
コマンド which phpを実行し、phpのパスを調べる。- (今回は which phpの結果が/usr/bin/phpであったという前提で話を進める。)
 
 
- (今回は 
- 
コマンド which sudoを実行し、sudoのパスを調べる。- (今回は which sudoの結果が/usr/bin/sudoであったという前提で話しを進める。)
 
 
- (今回は 
- 
apachectl -V | grep -E "HTTPD_ROOT|SERVER_CONFIG_FILE"を実行し、サーバ設定ファイル(通称httpd.conf) のパスを調べる。- 例えば次のような結果になる。
この場合は、 サーバ設定ファイルは-D HTTPD_ROOT="/etc/httpd" -D SERVER_CONFIG_FILE="conf/httpd.conf"/etc/httpd/conf/httpd.confにあると分かる。
 (今回はその前提で話しを進める。)
- (余談だが、Debian / Ubuntu 系では apache2.confのように、そもそもhttpd.confという名前ですらない場合もある)
 
- 例えば次のような結果になる。
- 
sudo nano /etc/httpd/conf/block_by_ASN.confを実行し、
 次のような/etc/httpd/conf/block_by_ASN.confを作る。/etc/httpd/conf/block_by_ASN.confRewriteEngine On RewriteMap ipcheck "prg:/usr/bin/sudo -u apache -g apache /usr/bin/php /usr/local/bin/BBASN/ipcheck.php" RewriteCond ${ipcheck:} dummy RewriteRule ^ -- Apache の設定ファイルである。
- (/usr/bin/phpの部分は、 手順7 のwhich phpの結果に合わせて調整すること)
- (/usr/bin/sudoの部分は、 手順8 のwhich sudoの結果に合わせて調整すること)
- (apache:apacheの部分は、3章で確認した Apacheの動作ユーザ:グループ に応じて書き換えること)
- (ファイルパス /etc/httpd/conf/block_by_ASN.confは、手順9 の結果に合わせて調整すること。
 httpd.confと同ディレクトリに配置するのが分かりやすいとは個人的には思う )
 
 
- 
次のコマンドを実行し、 /etc/httpd/conf/block_by_ASN.confの所有者とパーミッションを変更する。
 所有ユーザroot, 所有グループrootでroot以外の 一切のアクセスを不能(0)にする。sudo chown root:root /etc/httpd/conf/block_by_ASN.conf sudo chmod 600 /etc/httpd/conf/block_by_ASN.conf- ( sudo systemctl restart httpdすれば、Apacheはroot権限で再起動する。この場合、設定ファイルもroot権限で読まれる。)
 
 
- ( 
- 
httpd.confのグローバル、及び各<VirtualHost>内に
 Include /etc/httpd/conf/block_by_ASN.confを追加する。- (/etc/httpd/conf/block_by_ASN.confは、手順10に合わせて調整すること)
- 例えば、追加前が
のような場合、次のようにする。httpd.conf 追加前Listen 80 Listen 443 ...(略)... IncludeOptional conf.d/*.conf <VirtualHost *:80> ServerAdmin root@wikinebula.org DocumentRoot /var/www/html ServerName wikinebula.org RewriteEngine On RewriteCond %{SERVER_NAME} =wikinebula.org RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] </VirtualHost> <VirtualHost *:443> ServerAdmin root@wikinebula.org DocumentRoot /var/www/html ServerName wikinebula.org Include /path/to/SSL.conf RewriteEngine On RewriteCond %{REQUEST_URI} !^/w/rest\.php RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-d RewriteRule ^(.*)$ %{DOCUMENT_ROOT}/w/index.php [L] RewriteRule ^/*$ %{DOCUMENT_ROOT}/w/index.php [L] </VirtualHost>httpd.conf 追加後Listen 80 Listen 443 ...(略)... # 追加行 Include /etc/httpd/conf/block_by_ASN.conf # 1/3 IncludeOptional conf.d/*.conf <VirtualHost *:80> ServerAdmin root@wikinebula.org DocumentRoot /var/www/html ServerName wikinebula.org # 追加行 Include /etc/httpd/conf/block_by_ASN.conf # 2/3 RewriteEngine On RewriteCond %{SERVER_NAME} =wikinebula.org RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] </VirtualHost> <VirtualHost *:443> ServerAdmin root@wikinebula.org DocumentRoot /var/www/html ServerName wikinebula.org # 追加行 Include /etc/httpd/conf/block_by_ASN.conf # 3/3 Include /path/to/SSL.conf RewriteEngine On RewriteCond %{REQUEST_URI} !^/w/rest\.php RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-d RewriteRule ^(.*)$ %{DOCUMENT_ROOT}/w/index.php [L] RewriteRule ^/*$ %{DOCUMENT_ROOT}/w/index.php [L] </VirtualHost>
 
- (
 
12. sudo systemctl restart httpd を実行し、Apache を再起動する
5. 使って見る
ブラウザや curl などでサーバにアクセスしてから、
/var/lib/BBASN/test.txt を見てみると、
アクセスした回数だけ
Hello!
Hello!
Hello!
のように書き込まれていれば成功である。
前回(目次):
次回:

