0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【その1: Apache で、リクエストごとにphpを実行できるようにする】webページへのアクセス頻度をISP単位で制限してみる【Apache + PHP + SQLite3】

Last updated at Posted at 2025-10-09

前回(目次):

次回:


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-1.png
図2-1. Apache連携用プログラムのフローチャート

2-2. RewriteMap で呼ぶプログラムについて

Apache公式ドキュメントの RewriteMap の説明ページのprg見出し1には、次のように書かれている。

When a MapType of prg is 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回起動するだけであることに注意が必要である。
つまりプログラムは「ずっと起動しっぱなし」にしておく必要があり、

  1. Apacheから 標準入力されるまで待つ
  2. 標準入力を受け取ったら、任意の処理をして、標準出力をApacheに返す
  3. (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. 最初に結論

httpd.confの末尾
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:groupname format.
(引用者訳: 特に指定がない限り、 外部プログラムは Apacheを起動した ユーザ:グループ で起動します。 RewriteMap の3番目の引数に ユーザ名:グループ名 の形式で指定することで、起動する ユーザとグループを変更できます。 )

と明記されている。

しかし、この方法では、実行ユーザのセカンダリグループが読み込まれない問題が起きることが分かっている。

詳しくは こちら の記事を読んでほしい

(蛇足 ここまで)

2-3-3. RewriteCond ${(マップ名):} dummy について

RewriteCond ${(マップ名):} dummy については、
(マップ名)に対応するプログラムの標準出力が、正規表現 dummy にマッチするかを確認している。
つまり、ここでプログラムを呼んでいる。

マッチした場合、直後の ディレクティブ RewriteRule が実行され、
マッチしない場合は実行されない。

2-3-4. RewriteRule ^ - について

RewriteRule ^ - は、「何も行わない」ことを指定している。
要はダミー。
RewriteCondRewriteRule とセットで利用しないと動作が不安定になるので、
ダミーの RewriteRule を入れておく必要がある。

蛇足: 動作が不安定になる例

例えば、

httpd.conf
Include /etc/httpd/conf/test.conf
RewriteEngine On
RewriteRule ^(.*)$ ./index.php?id=$1
/etc/httpd/conf/test.conf
RewriteEngine On
RewriteCond ${(マップ名):} dummy

として、

ドキュメントルートに index.php だけおいて、
https://localhost/1234 にアクセスした場合。

(1). (マップ名)に対応するプログラムの標準出力が、正規表現 dummy にマッチする場合
httpd.confRewriteRule ^(.*)$ ./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~ の設定は、グローバルに定義しても、
バーチャルホストには引き継がれないので注意が必要である。

例えば、

httpd.conf
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 を使ってまとめることが出来る。

httpd.conf
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>

/etc/httpd/conf/map.conf
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。この場合、今後は httpdapache2 と読み替える事) にて 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" を追記する仕組みを作ってみる。

  1. 3章のことをまず最初に必ず実行する
     

  2. 次のコマンドを実行し、 グループ bbasn を作って、そこに ユーザ apache を登録する。

    sudo groupadd bbasn
    sudo usermod -aG bbasn apache
    
    • (apache の部分は、3章で確認した Apacheの動作ユーザに応じて書き換えること)
       
  3. 次のコマンドを実行し、ディレクトリ /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 以下の全てのファイルやディレクトリにアクセスできない。
       
  4. 次のコマンドを実行し、ディレクトリ /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 以下の全てのファイルやディレクトリにアクセスできない。
      • (こちらは③の問題は発生しない。実行不可能なディレクトリ内部のすべてのファイルやディレクトリにはそもそも如何なるアクセスもできないからだ)  
         
  5. 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);
     }
    

     

  6. 次のコマンドを実行し、/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 側の設定をする。
       
  7. コマンド which php を実行し、 php のパスを調べる。

    • (今回は which php の結果が /usr/bin/php であったという前提で話を進める。)
       
  8. コマンド which sudo を実行し、 sudo のパスを調べる。

    • (今回は which sudo の結果が /usr/bin/sudo であったという前提で話しを進める。)
       
  9. 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 という名前ですらない場合もある)
  10. sudo nano /etc/httpd/conf/block_by_ASN.conf を実行し、
    次のような /etc/httpd/conf/block_by_ASN.conf を作る。

    /etc/httpd/conf/block_by_ASN.conf
    RewriteEngine 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 と同ディレクトリに配置するのが分かりやすいとは個人的には思う )
       
  11. 次のコマンドを実行し、/etc/httpd/conf/block_by_ASN.conf の所有者とパーミッションを変更する。
    所有ユーザ root, 所有グループ rootroot以外の 一切のアクセスを不能(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 権限で読まれる。)
       
  12. 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 を見てみると、
アクセスした回数だけ

/var/lib/BBASN/test.txt
Hello!
Hello!
Hello!

のように書き込まれていれば成功である。


前回(目次):

次回:

  1. https://httpd.apache.org/docs/2.4/rewrite/rewritemap.html#prg 2 3

  2. https://httpd.apache.org/docs/2.4/en/mpm.html

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?