ほとんどの開発者がはまる落とし穴
Fedora ServerにWebアプリをデプロイしたばかりだとしよう。Nginxは起動済み、ファイアウォールでポート80を開放、設定ファイルも正常に見える。それなのにアクセスすると…502 Bad Gateway。Nginxのログにはconnect() to unix:/run/myapp.sock failed (13: Permission denied)と出ている。ls -laを実行すれば権限は755、念のためchmod 777にしても——一向に解決しない。
本当の犯人は?SELinux——Fedora/RHELを初めて使う人の90%が、最初に遭遇した瞬間に無効化したくなるものだ。
私はFedoraをメインの開発マシンとして2年間使ってきた。パッケージの更新が速く、カーネルも常に新しい——そこが気に入っている。ただ、SELinuxだけは新しい環境をセットアップするたびに「戦わなければならない」相手だ。この記事では、何度も詰まった経験から学んだことをまとめた。
SELinuxはなぜこれほど厄介なのか
従来のLinuxはDAC(任意アクセス制御)モデルでアクセス権を管理している——user/group/パーミッションビットに基づく方式だ。ファイルのオーナーであれば、何でも自由にできる。
SELinuxはその上にMAC(強制アクセス制御)の層を追加する。核心的な違いは、rootかどうかを問わない——プロセスとファイルに付与されたセキュリティコンテキスト(セキュリティラベル)を見るという点だ。
すべてのファイル、プロセス、ポートは次の形式のコンテキストを持つ:
user:role:type:level
ls -Zを実行して実際のコンテキストを確認しよう:
ls -Z /var/www/html/
# 出力:
system_u:object_r:httpd_sys_content_t:s0 index.html
Nginxプロセスはhttpd_tタイプで実行される。読み取りが許可されているのはhttpd_sys_content_tタイプのファイルのみだ。/homeから/var/www/htmlにファイルをコピーすると、そのファイルは元のコンテキスト——通常はuser_home_tまたはtmp_t——を保持したままになる。パーミッションビットが777であってもNginxはアクセスを拒否される。これがWebアプリデプロイ時のSELinux permission deniedエラーで最もよくある原因だ。
3つの対処法——悪い順から良い順へ
方法1:SELinuxを無効化する(本番環境では絶対にやめること)
「SELinux permission denied」でGoogle検索すると、最初に表示される回答はたいていこれだ:
setenforce 0
# さらに悪い方法——完全に無効化する:
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
手っ取り早い。すぐ効く。そして完全に間違っている。
SELinuxを無効化することは、うるさいからと火災報知器を切るようなものだ。FedoraをはじめRHEL系のディストリビューションがSELinuxをデフォルトで有効にしているのは、privilege escalation攻撃——攻撃者がNginxやPHPの脆弱性を悪用してプロセス外に権限昇格する手法——を防ぐためだ。無効化すれば、その保護層が完全に失われる。
方法2:デバッグ用のPermissiveモード
Permissiveモードはすべての動作を許可しつつ、違反をログに記録し続ける——ポリシーを書く前にアプリに必要な権限を把握するのに非常に役立つ:
# 一時的にPermissiveに切り替える(reboot後はリセットされる)
setenforce 0
# 現在のモードを確認する
getenforce
# 結果: Permissive または Enforcing
# denialログを確認する
ausearch -m avc -ts recent
# またはリアルタイムで監視する:
tail -f /var/log/audit/audit.log | grep denied
Permissiveモードはデバッグ段階だけに使うこと。本番サーバーで放置するのは、不必要なセキュリティリスクを受け入れることになる。
方法3:正しくコンテキストを修正する——まずこれを試すべき
ファイルのコンテキストが間違っている場合——最もよくあるのはhomeディレクトリから/var/wwwにコピーしたケース:
# ファイルの現在のコンテキストを確認する
ls -Z /var/www/html/myapp/
# そのディレクトリのデフォルトコンテキストに復元する
restorecon -Rv /var/www/html/myapp/
# または手動で設定する
chcon -R -t httpd_sys_content_t /var/www/html/myapp/
よくある落とし穴:chconは一時的な変更しか行わない——SELinuxがファイルシステム全体をrelabelした後(例:touch /.autorelabelしてrebootした後)にリセットされる。変更を永続化するにはsemanage fcontextを使う:
# /var/www外のディレクトリに永続ルールを追加する
semanage fcontext -a -t httpd_sys_content_t "/srv/myapp(/.*)?"
# 追加したルールを適用する
restorecon -Rv /srv/myapp/
複雑なアプリケーション向けカスタムポリシーの作成
この部分はほとんどのチュートリアルが省略している——アプリが具体的に何をする必要があるかを理解していないと書けないからだ。しかし実際のアプリをデプロイする際には必ず必要になる:標準外ポートへの接続、慣例外ディレクトリへの書き込み、複雑なシステムコールを伴うバックグラウンドジョブの実行などのケースだ。
ステップ1:十分なdenialログを収集する
Permissiveに切り替え、アプリを実行してすべての機能をテストする。重要なのはすべてテストすること——ファイルアップロード、API呼び出し、cronジョブ——SELinuxがブロックしているすべての権限をログに記録させるためだ。一つでも機能のテストを漏らすと、ポリシーをロードした後でまたブロックされる。
setenforce 0
# アプリを実行し、すべての機能をテストする...
ausearch -m avc -ts today > /tmp/denials.log
ステップ2:audit2allowでポリシーを生成する
# ツールが未インストールの場合はインストールする
dnf install -y policycoreutils-python-utils
# denialログからポリシーモジュールを作成する
ausearch -m avc -ts today | audit2allow -M myapp_policy
# 2つのファイルが生成される:
# myapp_policy.te — Type Enforcement ソース(人間が読める形式)
# myapp_policy.pp — コンパイル済みPolicy Package
.teファイルを次の作業の前に読んでおこう:
cat myapp_policy.te
# 出力例:
# allow httpd_t unreserved_port_t:tcp_socket name_connect;
# allow httpd_t var_t:file { read write create };
2行目——allow httpd_t var_t:file { read write create }——は警告サインだ。var_tタイプは範囲が広すぎて、システム全体の/var/libや/var/logまで含んでしまう。より具体的なタイプに絞り込み、そのままロードするのではなくcheckmoduleとsemodule_packageで再コンパイルすべきだ。
ステップ3:ポリシーをロードしてEnforcingに戻す
semodule -i myapp_policy.pp
setenforce 1
# ロードされたか確認する
semodule -l | grep myapp
アプリがカスタムポートをバインドできるようにする
アプリがポート8080をバインドしようとしてブロックされている?SELinuxはポートも管理している——各ポートには特定のタイプが割り当てられており、対応するタイプのプロセスのみがバインドできる:
# どのポートにどのタイプが割り当てられているかを確認する
semanage port -l | grep http
# ポート8080をhttp_port_tに追加する
semanage port -a -t http_port_t -p tcp 8080
# 確認する
semanage port -l | grep 8080
Boolean——ポリシーを書かずに機能を有効化・無効化する
Fedoraには一般的なシナリオに対応した300以上のbooleanスイッチが付属している。ポリシーをゼロから書き始める前に、まずbooleanを確認しよう——たいてい必要なものがすでに用意されている:
# httpd/nginxに関連するbooleanを確認する
getsebool -a | grep httpd
# nginxが外部に接続できるようにする(例:バックエンドへのプロキシ)
setsebool -P httpd_can_network_connect on
# httpdがhomeディレクトリを読み取れるようにする
setsebool -P httpd_enable_homedirs on
# -P でreboot後も設定を維持する
SELinux問題のデバッグワークフロー
私がよく使う手順を、シンプルなものから複雑なものへの順に紹介する:
sestatusを実行する——Enforcingモードであることを確認するausearch -m avc -ts recentを実行する——最新のdenialを見つけ、どのタイプがブロックされているか確認する- まずシンプルな修正を試す:コンテキストの問題なら
restorecon、よくある状況ならboolean、ポートの問題ならsemanage port - それでも不十分な場合 → Permissiveモード → 十分なdenialを収集 →
audit2allow - .teファイルを注意深く読み、過度に広い権限を絞り込んでからコンパイルする
- ポリシーをロードし、すべての機能をテストし、Enforcingに戻す
便利なクイックリファレンスコマンド
# 実行中のプロセスのコンテキストを確認する
ps auxZ | grep nginx
# マップされているすべてのポートを確認する
semanage port -l
# 特定のプロセス名でdenialを検索する
ausearch -m avc -c nginx
# ロード済みのカスタムポリシーを一覧表示する
semodule -l | grep -v ^base
SELinuxが「やっぱり必要だ」と心から実感したのは、Node.jsアプリの脆弱性が悪用された時だった——攻撃者はプロセス内でシェルを手に入れたが、SELinuxがポリシー外のシステムコールをすべてブロックしたため、外部に権限昇格できなかった。サーバーは正常に動き続け、auditログに数十件のdenialの試みが記録されただけだ。それ以来、SELinuxを作業の邪魔者として見なくなった。

