1. 記事一覧 >
  2. ブログ記事
category logo

RHEL8 Apache2.4 PHP7:php.iniのupload_tmp_dirを変更したらSELinux関係でハマった話

(更新) (公開)

はじめに

RHEL8 & Apache2.4 & PHP7 のサーバーで
php.ini

upload_tmp_dir = /usr/temporary
として、POST を受信したら、SELinux の関係で POST データが消失して、ハマりました。


/usr/temporary は、

# mkdir /usr/temporary
# chown apache /usr/temporary
# chmod 1777 /usr/temporary

として、単純なパーミッションの問題ではないです。


また、大きめの POST データを送信すると、/usr/temporary が使われて、SELinux の関係で書き込みが拒否されているようでした。小さめの POST データの場合は、問題ありませんでした。


調査すると、いくつかの要素が重なって現象が発生し、簡単な解決方法であることが分かりました。
この記事は、現象、解決方法、調査内容の記録になります。


なお、SELinux が原因というのは、

# getenforce
Enforced
# setenforce 0
→再現しなくなる。

で、すぐに分かりましたが、setenforce 0 で解決する話ではなく、setenforce 1 のまま解決します。


【すぐに解決しなかった要因】
・/var/log/audit/audit.log に何も出力されない。
・ファイルを添付しない POST のため、upload_tmp_dir は無関係だと思っていた。(PHP マニュアルでは、ファイルアップロード時にファイル保存に用いるテンポラリディレクトリ。 という説明。)
display_errors = Ondisplay_startup_errors = On のときのみ POST データ消失が再現。
・同じ内容を POST して、再現しないこともあった。(一度再現しなくなったら、しばらく再現せず、たまに再現。← この件は、最終的に謎のまま。)

PHP8 では再現しませんでした。

また、PHP7 の場合は、PHP 7.4.6、PHP 7.4.33 の2種類でしか再現は確認していません。


再現環境

今回の環境の作成方法は、過去記事「CentOS8.3 に apache2,php,postgresql,openldap-client をインストール」にありますので、そちらを参照してください。
CentOS Linux release 8.3.2011
Apache 2.4.37
PHP 7.4.6
です。


CentOS8.3 ですが、別に Red Hat Enterprise Linux 8.8 で再現を試みても同じでした。


原因・対策

現象の説明、調査内容の紹介の前に原因・対策をサクッと発表しますと、以下になります。


原因:

POST データが 16384 バイト以上の時、upload_tmp_dir が使われ、upload_tmp_dir ディレクトリへの書き込みを SELinux が阻止していた。かつ、display_errors = Ondisplay_startup_errors = On のとき、POST データが消失した状態で動作していた。


対策:

# /usr/sbin/semanage fcontext -a -t httpd_sys_rw_content_t "/usr/temporary(/.*)?"
# /sbin/restorecon -R /usr/temporary

のみです。display_errors = Ondisplay_startup_errors = On のときでもエラーがなくなって、正常に動作します。


現象

●php.ini 設定が display_errors = Ondisplay_startup_errors = On のとき
画面にエラーが表示されて、POST データが消失しました。


OKパターン:
POST OKパターン


NGパターン:
POST NGパターン


POST データが消失するタイミングは、受信時コード実行前です。1行目で $_POST をどこかに書き出しても、デバッガで1行目でブレークしても、何もないことが分かります。

POST NGパターン デバッガでブレーク

●php.ini 設定が display_errors = Ondisplay_startup_errors = On 以外のとき

www-error.log にエラーが出力されているだけで正常でした。

$ tail /var/log/php-fpm/www-error.log
[03-Oct-2023 15:57:34 UTC] PHP Notice:  Unknown: file created in the system's temporary directory in Unknown on line 0
[03-Oct-2023 15:57:34 UTC] PHP Warning:  Cannot modify header information - headers already sent in Unknown on line 0
[03-Oct-2023 15:57:34 UTC] PHP Warning:  Cannot modify header information - headers already sent in /var/www/html/receive.php on line 9

すぐに分からなかった要因詳細

■ /var/log/audit/audit.log に何も出力されない。

SELinux の /usr/temporary への書き込み拒否ルールが dontaudit ルールに該当していたため、/var/log/audit/audit.log に出力されませんでした。

SELinux の dontaudit ルール(ドントオーディット ルール)とは、ポリシーによって許可されなかったアクセスを拒否するときに、ログに記録しないようにするルールです。


■ ファイルを添付しない POST のため、upload_tmp_dir は無関係だと思っていた。(PHP マニュアルでは、ファイルアップロード時にファイル保存に用いるテンポラリディレクトリ。 という説明。)

POST データが 16384 バイト以上の時に upload_tmp_dir が使われる ようでした。
main/SAPI.h:#define SAPI_POST_BLOCK_SIZE 0x4000
という値が見つかり、10 進数にすると、16384 バイトのため、これが関係するかもしれません。


■ display_errors = Ondisplay_startup_errors = On のときのみ POST データ消失が再現。

PHP起動時エラーを表示するかどうかが POST データ消失に関係あるようでした。
PHP の起動時に発生したエラーを表示する場合のみに、POST データ消失が再現しました。
display_errors = Ondisplay_startup_errors = On:再現
display_errors = Ondisplay_startup_errors = Off:再現しない
display_errors = Offdisplay_startup_errors = Off:再現しない
display_errors = Offdisplay_startup_errors = On:再現しない
でした。

display_errors は、PHP スクリプトの実行中に発生したエラーを表示するかどうかを決めます。

display_startup_errors は、PHP の起動時に発生したエラーを表示するかどうかを決めます。

display_errors が On になっていても、display_startup_errors が Off の場合は、PHP の起動時に発生するエラーは表示されません。


■ 同じ内容を POST して、再現しないこともあった。
一度再現しなくなったら、しばらく再現せず、たまに再現する状況でした。
キャッシュ的な何かなのか、タイミングみたいなものがあるのか、この件は、最終的に謎のままです。


調査内容

過去記事「CentOS8.3 に apache2,php,postgresql,openldap-client をインストール」の通り、http://[IP アドレス]/info.php で /var/www/html/info.php の実行まで確認済みの環境とします。

以下のようなプログラムを作成します。


Web ブラウザで、http://[IPアドレス]/post.php にアクセス

post.php でダミー POST データ生成

post.php で curl_exec() により、http://localhost/receive.php へ POST

receive.php で受け取った POST データをそのまま JSON 文字列に変換

receive.php で JSON 文字列をレスポンス

post.php でブラウザに receive.php からのレスポンスを表示

POST OKパターン

簡略化のために送信側、受信側1つのサーバーで検証しますが、現象が発生するのは、受信側です。


# firewall-cmd --add-service=http --permanent
# firewall-cmd --reload
# systemctl enable httpd
# systemctl start httpd
# systemctl enable php-fpm
# systemctl start php-fpm
# vi /var/www/html/post.php

/var/www/html/post.php
<?php
// POSTするデータを作成
$data["value"] = str_repeat('x', 16378);//xxxxx....とxの連続文字列 16378文字
// データをform-urlencode形式に変換
$data = http_build_query($data);//value=xxxxx....となる
//value=を入れると、データの長さは、16384バイト

// curlセッションを初期化
$ch = curl_init();

// オプションを設定
curl_setopt($ch, CURLOPT_URL, 'http://localhost/receive.php'); // URLを指定
curl_setopt($ch, CURLOPT_POST, true); // POSTメソッドを使用
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // POSTするデータを指定
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // curl_exec() の戻り値を文字列で返す
curl_setopt($ch, CURLOPT_HEADER, false); // curl_exec() の戻り値にヘッダーは含めない

// リクエストを実行し、レスポンスを取得
$response = curl_exec($ch);

// curlセッションを終了
curl_close($ch);

// レスポンスを表示
echo '<pre>';
print_r($response);
echo '</pre>';
?>

# vi /var/www/html/receive.php

/var/www/html/receive.php
<?php
// POSTされたデータを取得
$data = $_POST;

// データをJSON文字列に変換(整形済みとする)
$data = json_encode($data, JSON_PRETTY_PRINT);

// ヘッダーを設定
header('Content-Type: application/json');

// レスポンスを出力
echo $data;
?>

なお、ここで、いきなり、
http://[IPアドレス]/post.php
にアクセスすると、別のエラーになるため、対処しておきます。


● 別のエラー

エラー確認:

# tail /var/log/audit/audit.log
type=AVC msg=audit(1696302915.809:138): avc:  denied  { name_connect } for  pid=3442 comm="php-fpm" dest=80 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:http_port_t:s0 tclass=tcp_socket permissive=0
type=SYSCALL msg=audit(1696302915.809:138): arch=c000003e syscall=42 success=no exit=-13 a0=8 a1=7ffec3703a90 a2=1c a3=2e8890b9bf4f97 items=0 ppid=3436 pid=3442 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm="php-fpm" exe="/usr/sbin/php-fpm" subj=system_u:system_r:httpd_t:s0 key=(null)ARCH=x86_64 SYSCALL=connect AUID="unset" UID="apache" GID="apache" EUID="apache" SUID="apache" FSUID="apache" EGID="apache" SGID="apache" FSGID="apache"
type=PROCTITLE msg=audit(1696302915.809:138): proctitle=7068702D66706D3A20706F6F6C20777777

対処:

# setsebool -P httpd_can_network_connect 1

この時点では、
php.ini

upload_tmp_dir =
display_errors =
display_startup_errors =
を変えていないため、何も問題なく動作します。

post.php receive.php 問題なし 動画

$ tail /var/log/audit/audit.log
(追加出力無し)
$ tail /var/log/php-fpm/www-error.log
(出力無し)

/usr/temporary ディレクトリを作成して、php.ini を書き換えます。

# mkdir /usr/temporary
# chmod 1777 /usr/temporary
# vi /etc/php.ini

upload_tmp_dir = /usr/temporary
display_errors = On
display_startup_errors = On

upload_tmp_dir を設定しない場合のデフォルトは、/tmp です。(ただし、今回の環境の場合)

最初の桁が 1 の場合、スティッキービットという特殊なパーミッションが設定されます。スティッキービットが設定されたディレクトリでは、誰でもファイルやディレクトリを作成や書き込みができますが、削除や名前変更ができるのはファイルやディレクトリの所有者か root ユーザーだけになります。

777 は、オーナー、グループ、その他のユーザーはすべて読み取り、書き込み、実行の権限を持つという意味です。

今回の環境の場合、デフォルトの /tmp も 1777 です。

# systemctl restart php-fpm

Web ブラウザで、http://[IPアドレス]/post.php にアクセスします。

post.php receive.php POST データ消失 動画


再現しました。
レスポンスが [] となっていて、POST データを消失しています。

正確には、どこで消失したか分かりませんが、receive.php の1行目で $_POST をどこかに書き出しても、デバッガで1行目でブレークしても、何もないことが分かりました。

$ tail /var/log/audit/audit.log
(追加出力無し)
$ tail /var/log/php-fpm/www-error.log
[02-Oct-2023 14:30:23 UTC] PHP Notice:  Unknown: file created in the system's temporary directory in Unknown on line 0
[02-Oct-2023 14:30:23 UTC] PHP Warning:  Cannot modify header information - headers already sent in Unknown on line 0
[02-Oct-2023 14:30:23 UTC] PHP Warning:  Cannot modify header information - headers already sent in /var/www/html/receive.php on line 9

ここで、/var/log/audit/audit.log には何も出力されません。


SELinux の /var/log/audit/audit.log に出力されない dontaudit ルールに抵触しています。
dontaudit ルールであっても、ログ出力するようにします。

# semodule -DB

オプションの DB は以下の意味になります。

-D,--disable_dontaudit Remove dontaudits from policy

-B, --build build and reload policy


もう一度実行すると、audit.log が出力されます。(POST データ消失状況は変わりません。)

$ tail /var/log/audit/audit.log
type=AVC msg=audit(1696229122.100:208): avc:  denied  { write } for  pid=4603 comm="php-fpm" name="temporary" dev="sda3" ino=2106106 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:usr_t:s0 tclass=dir permissive=0
type=SYSCALL msg=audit(1696229122.100:208): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=7ffe7318f450 a2=c2 a3=180 items=0 ppid=4602 pid=4603 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm="php-fpm" exe="/usr/sbin/php-fpm" subj=system_u:system_r:httpd_t:s0 key=(null)ARCH=x86_64 SYSCALL=openat AUID="unset" UID="apache" GID="apache" EUID="apache" SUID="apache" FSUID="apache" EGID="apache" SGID="apache" FSGID="apache"
type=PROCTITLE msg=audit(1696229122.100:208): proctitle=7068702D66706D3A20706F6F6C20777777
$ tail /var/log/php-fpm/www-error.log
[02-Oct-2023 14:45:22 UTC] PHP Notice:  Unknown: file created in the system's temporary directory in Unknown on line 0
[02-Oct-2023 14:45:22 UTC] PHP Warning:  Cannot modify header information - headers already sent in Unknown on line 0
[02-Oct-2023 14:45:22 UTC] PHP Warning:  Cannot modify header information - headers already sent in /var/www/html/receive.php on line 9

audit.log の内容が分かったので、

# semodule -B

で元に戻します。


type=AVC msg=audit(1696229122.100:208): avc: denied { write } for pid=4603 comm="php-fpm" name="temporary" dev="sda3" ino=2106106 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:usr_t:s0 tclass=dir permissive=0
を深堀すると、やるべきことが見えてきます。(ぱっと見で既になんとなく分かると言えば分かりますが。)


type=AVC
type=AVC の場合、SELinux によるアクセス制御の結果を記録するレコードを意味します。


msg=audit(1696229122.100:208)
レコードのタイムスタンプと一意の ID です。


: avc: denied { write }
SELinux のアクセスベクターキャッシュ(AVC、SELinux がアクセスを許可するか拒否するかを判断するために使用するキャッシュシステム)にファイルやディレクトリへの書き込み権限が拒否されたことを意味します。


for pid=4603
SELinux が拒否したプロセスの ID です。


comm="php-fpm"
SELinux が拒否したプロセスの名前です。


name="temporary"
SELinux が拒否したオブジェクトの名前です。(/usr/temporary ディレクトリのこと)


dev="sda3"
SELinux が拒否したオブジェクトのデバイス番号です。(Filesystem=/dev/sda3 のこと)


ino=2106106
SELinux が拒否したオブジェクトの inode 番号です。(/usr/temporary ディレクトリの inode 番号)


scontext=system_u:system_r:httpd_t:s0
SELinux が拒否したプロセスのセキュリティコンテキストです。セキュリティコンテキストとは、SELinux がアクセス制御を行うために使用するラベルです。
セキュリティコンテキストは、ユーザー (user) 、ロール (role) 、タイプ (type) 、レベル (level) の 4 つの要素から構成されます。
この例では、以下のようになります。

  • ユーザーは system_u です。これは、システム用のユーザーです。
  • ロールは system_r です。これは、システム用のロールです。
  • タイプは httpd_t です。これは、Apache HTTP Server 用のタイプです。
  • セキュリティレベルは s0 です。s0 から s15 に向かってだんだんと機密レベルが上昇します。

tcontext=unconfined_u:object_r:usr_t:s0
SELinux が拒否したオブジェクト(今回の場合、/usr/temporary ディレクトリ)のセキュリティコンテキストです。
この例では、以下のようになります。

  • ユーザーは unconfined_u です。これは、制限されていないユーザーです。
  • ロールは object_r です。これは、オブジェクト用のロールです。
  • タイプは usr_t です。これは、/usr ディレクトリ以下のファイルやディレクトリ用のタイプです。
  • セキュリティレベルは s0 です。s0 から s15 に向かってだんだんと機密レベルが上昇します。

tclass=dir
SELinux が拒否したオブジェクトのクラスです。クラスとは、SELinux がアクセス制御を行う対象となるオブジェクトの種類です。dir の場合、ディレクトリクラスです。


permissive=0
enforcing か permissive かの値です。enforcing とは、SELinux がアクセスを拒否するときに実際にアクションを停止することを意味します。 permissive とは、SELinux がアクセスを拒否するときに実際にアクションを停止しないことを意味します。0 の場合、enforcing です。


...ここまで調べると、「httpd → php → /usr/temporary に何か書き込もうとして SELinux に拒否された」となんとなくわかります。


「httpd セキュリティコンテキスト 書き込み」とかでググって(最近では AI に聞いて)、httpd_sys_rw_content_t というワードが見つかり、これが当たりです。


以下のコマンドでセキュリティコンテキストを適用します。

# /usr/sbin/semanage fcontext -a -t httpd_sys_rw_content_t "/usr/temporary(/.*)?"
# /sbin/restorecon -R /usr/temporary

/usr/sbin/semanage fcontext は、SELinux のファイルやディレクトリのコンテキストを永続的に変更するために使用されるコマンドです。 コンテキストとは、SELinux がアクセス制御に利用するラベルのことで、ファイルやディレクトリの役割や用途を表します。

-a SELinux のコンテキストを追加

-t SELinux のコンテキストタイプを指定

"/usr/temporary(/.*)?" 正規表現で、/usr/temporary 自身または、配下全ての意味

/sbin/restorecon は、コンテキストを修正するコマンドです。

-R 指定パス配下を再帰的に変更


type=PROCTITLE msg=audit(1696229122.100:208): proctitle=7068702D66706D3A20706F6F6C20777777

7068702D66706D3A20706F6F6C20777777 を ASCII コードとして解釈し、文字列に変換すると、php-fpm: pool www です。


もう一度動作確認します。

post.php receive.php 問題なし 動画

$ tail /var/log/audit/audit.log
(追加出力無し)
$ tail /var/log/php-fpm/www-error.log
(出力無し)

問題なし!


ちなみに、前述の通り、display_errors = Ondisplay_startup_errors = On のときのみ POST データ消失が再現します。
display_errors = Ondisplay_startup_errors = Off
display_errors = Offdisplay_startup_errors = Off
display_errors = Offdisplay_startup_errors = On
の場合、

$ tail /var/log/audit/audit.log
(追加出力無し)
$ tail /var/log/php-fpm/www-error.log
[02-Oct-2023 14:30:23 UTC] PHP Notice:  Unknown: file created in the system's temporary directory in Unknown on line 0
[02-Oct-2023 14:30:23 UTC] PHP Warning:  Cannot modify header information - headers already sent in Unknown on line 0
[02-Oct-2023 14:30:23 UTC] PHP Warning:  Cannot modify header information - headers already sent in /var/www/html/receive.php on line 9

と www-error.log に密かにエラー出力されるだけで、特に何も起きません。画面にエラーは表示されず、POST データは消失せず、正常に動作します。


実害が無く、「この www-error.log に出力されているエラーは、なんだろうなー。」で終わってたかもしれません。


このエラー出力を止める方法は、やはり、SELinux セキュリティコンテキスト調整のコマンドになります。

# /usr/sbin/semanage fcontext -a -t httpd_sys_rw_content_t "/usr/temporary(/.*)?"
# /sbin/restorecon -R /usr/temporary
loading...