nginxの403をSELinuxで直す方法
nginxが403を返し、通常の権限に問題がない場合はSELinuxの拒否を確認します。semanageとrestoreconでラベルを修正し、enforcingのまま安全に解決する手順を紹介します。
権限が正常なファイルに対して nginx が 403 を返す理由
ファイルの権限ビットが正しいのに nginx が 403 を返す場合、その原因のほとんどは SELinux(security-enhanced Linux)が読み取りを拒否していることです。SELinux は通常の権限チェックを通過した後に、別のルールセットも確認します。Web サーバーは、Web コンテンツ用のラベルが付いたファイルだけを読み取れます。対象ファイルには別のラベルが付いているため、open に失敗し、nginx は送信する内容を取得できません。
mode だけでなく、ラベルを確認します。
ls -ldZ /data/www /data/www/index.htmldrwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.htmldrwxr-xr-x の後に表示されるドットは、ファイルに SELinux ラベルが付いていることを示します。default_t は、ポリシーがそのパスを認識していない場合に付く値です。Web サーバーのルールには、そのタイプの読み取りを許可する設定がありません。エラーログには通常の Unix エラーが表示されるため、権限の問題に見えます。
2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"カーネルは、通常の拒否と SELinux による拒否の両方に対して 13: Permission denied を返します。そのため、最初にどの層が拒否したのかを特定します。最初から setenforce 0 を実行しないでください。
必要なモデルの部分
SELinux は強制アクセス制御で、通常は MAC と表記します。すべてのプロセスはドメイン内で実行されます。Web サーバーでは、たとえば httpd_t です。すべてのファイルとネットワークポートには、たとえば httpd_sys_content_t のようなタイプが付与されます。ポリシーは、ドメイン、タイプ、操作の組み合わせごとに許可・不許可を定めた一覧です。一覧にない操作は拒否されます。SELinux による判定は従来の Unix のチェック後に行われるため、drwxr-xr-x のアクセス権ビットでも先にアクセスが許可されていなければなりません。両方のレイヤーで許可される必要があります。
完全なコンテキストには、コロンで区切られた4つのフィールドがあります。形式は system_u:system_r:httpd_t:s0 です。SELinux ユーザー、ロール、タイプ、レベルの順です。サーバーでほとんど確認するのは、3番目のフィールドであるタイプです。実行中の値は、次の2つのコマンドで確認できます。
ps -eZ | grep nginx
id -Znginx のワーカープロセスのコンテキストは、httpd_t で終わります。ログインシェルには unconfined_u:unconfined_r:unconfined_t:s0 が表示されます。これは、デフォルトの targeted ポリシーがサービスを制限し、対話型ユーザーには制限を適用しないためです。この点は重要です。SELinux は、最小権限のユーザーでサービスを実行することの代わりにはなりません。サービスが侵害された後に、サービスからアクセスできる対象を制限します。
3 つのモードと、SELinux を搭載するイメージ
sestatus
getenforceEnforcing はアクセスをブロックし、その内容をログに記録します。Permissive はすべてのアクセスを許可し、ブロックするはずだった内容をログに記録します。Disabled はポリシーをまったく読み込みません。getenforce は現在のモードを表示します。sestatus は /etc/selinux/config に設定されているモードも表示します。これは再起動後に戻るモードです。
Rocky Linux、AlmaLinux、Fedora、RHEL は、targeted ポリシーを使用する SELinux の Enforcing モードでリリースされています。この共通のデフォルト設定は偶然ではありません。4 つのディストリビューションはいずれも、Rocky Linux と AlmaLinux が登場する前に CentOS へつながっていた同じ Red Hat 系譜から発展したものだからです。このページの内容に関しては、どちらを使用しても違いはありません。両方とも同じポリシーとツールを搭載しています。そのため、Rocky Linux と AlmaLinux のどちらを選ぶかは、セキュリティのデフォルト設定ではなく、互換性に関する方針と古い CPU のサポートで決まります。一方、Ubuntu と Debian は AppArmor を搭載しています。目的は同じですが、仕組みが異なります(最後のセクションで説明します)。そのため、同じアプリケーションでも、一方のサーバーには正常にインストールできるのに、別のサーバーでは 403 が返ることがあります。
必要になる前にツールをインストールする
sudo dnf install -y policycoreutils-python-utils setroubleshoot-server最小構成のイメージで semanage: command not found が表示される場合、policycoreutils-python-utils が不足しています。このパッケージには semanage と audit2allow が含まれます。setroubleshoot-server は sealert を追加し、各拒否の概要を平易な英語で journal に記録します。新しいサーバーでは両方をインストールしてください。必要になった時点で、すでに何かが壊れている可能性があるためです。
監査ログで SELinux の拒否を読み取る方法
各拒否は、audit daemon により AVC (access vector cache) メッセージとして記録されます。
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=AVC msg=audit(1754896442.881:412): avc: denied { read } for pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=04 つのフィールドで全体像を把握できます。comm はブロックされたプログラムです。scontext はソースコンテキストで、プロセスが実行されていたドメインです。tcontext はターゲットコンテキストで、アクセスしようとした対象に付けられたラベルです。tclass はオブジェクトの種類で、ここではファイルです。まとめると、httpd_t のプロセスが default_t のラベルが付いたファイルを読み取ろうとし、permissive=0 により、その要求が記録されただけでなく実際にブロックされたことが分かります。
ausearch が何も出力しない場合、audit daemon が実行されていない可能性があります。その場合、拒否はカーネルのリングバッファに記録されます。
sudo journalctl -k | grep -i avc次に、記録を英文の文に変換します。
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why は同じ記録を読み取り、認識した原因を示します。無効になっている boolean、ポリシーと一致しないラベル、またはルールが存在しないことなどです。sealert はログ全体を調べ、拒否ごとに推奨コマンドを出力します。推奨内容は参考情報として扱ってください。リリースによって文言が変わります。また、sealert は、1 行のラベル修正で解決できる場合でも、カスタムポリシーモジュールを提案することがあります。
もう1つ知っておくべき点があります。ポリシーには、無害と見なされた拒否を隠す dontaudit ルールが含まれています。そのため、ログに何も残らないままプログラムが正常に動作しないことがあります。1 回のテスト中だけ、これらの拒否を表示します。
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -Bsemanage fcontext と restorecon で誤ったラベルのパスを修正する
2 つのコマンドを使います。実行順序が重要です。semanage fcontext -a は、パスに付けるべきラベルを記録します。restorecon は、記録したデフォルトをディスク上のファイルに適用します。
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlパスには正規表現を指定します。(/.*)? はディレクトリ自体とその配下すべてに適用されます。これはドキュメントルートに必要な設定です。変更前に、何が変わるかを確認します。sudo restorecon -Rvn /data/www は予定されている再ラベル付けを表示します。-n は実際には何も実行しないことを意味します。実際の restorecon の後は、ラベルが httpd_sys_content_t と表示され、サービスを再起動しなくても 403 が解消されます。
chcon はテストにだけ使用します。chcon -t httpd_sys_content_t index.html はラベルを直接設定しますが、次回の restorecon、パッケージ更新、または完全な再ラベル付けでリセットされます。ポリシーでは、そのパスに別のラベルを付けることになっているためです。タイマーでセキュリティ更新を適用する dnf-automatic が動作する環境では、そのリセットはマシンの前に座っている間ではなく、スケジュールに従って実行されます。そのため、最後に触れてから数時間後にサイトが停止することがあります。semanage fcontext は再起動後も維持される方法です。記録済みの設定は sudo semanage fcontext -l | grep '^/data' で一覧表示します。
サービスが書き込む必要があるコンテンツには、別のタイプが必要です。アップロードディレクトリやキャッシュには httpd_sys_rw_content_t を使用し、対象パスだけに限定してください。読み取り専用のサイトに書き込み可能なタイプを設定すると、アプリケーションに必要な範囲を超えるアクセスを許可することになります。
そもそも、なぜラベルが誤っていたのでしょうか。ほとんどの場合、ファイルの移動方法が原因です。mv はファイルの既存ラベルを保持します。そのため、サイトを /root から移動すると admin_home_t のラベルが付いたままになり、その状態が維持されます。通常の cp では、移動先ディレクトリのデフォルトラベルが新しいファイルに付くため、通常はこちらが適切です。一方、cp -a と rsync -X は、ファイルと一緒に移動元のラベルもコピーします。新しいトップレベルディレクトリへの git clone では、default_t が生成されます。/usr/share/nginx/html からはページが正常に読み込めるのに、自分のディレクトリからは失敗する場合、その原因はこれです。
ブール値で特定の動作を修正する
失敗の原因がラベルの問題とは限りません。Rocky または AlmaLinux を新規に構築した環境でリバースプロキシが 502 を返し、エラーログに次の内容が出ることがあります。
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamupstream は正常です。httpd_t ドメインでは、デフォルトで外向きのネットワーク接続を開くことが許可されていません。そのため、connect() の呼び出しは loopback インターフェースに到達する前に拒否されます。この動作全体を制御するスイッチは 1 つです。
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on重要なのは -P フラグです。このフラグを付けると、値がディスクに書き込まれます。-P がない場合、変更は次回の再起動時に失われます。その場合、マシンを再起動するまでしかサービスが動作しません。semanage boolean -l | grep httpd_can_network_connect で確認すると、実行中の値と保存された値が並んで表示されます。
利用できる場合は、手書きのルールよりもブール値を優先してください。ブール値はディストリビューションのポリシーに含まれているため、保守され、文書化されており、次の担当者も簡単に見つけられます。getsebool -a では、システム上のすべてのブール値を一覧表示できます。
非標準ポートでサービスを待ち受けさせる
ポートにもラベルが付いています。nginx を 8081 に移すと、起動に失敗します。
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t はラベルが付いた http_port_t ポートで待ち受けできますが、8081 はその中にありません。追加します。
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081先に一覧を確認してください。8008 や 8443 など、すでに許可されている高位ポートが複数あります。また、同じポートを 2 回追加すると ValueError: Port tcp/8081 already defined で失敗します。ポートがすでに別のタイプに割り当てられている場合は、追加ではなく semanage port -m -t http_port_t -p tcp 8081 で変更してください。
移動した SSH ポートを機能させる場合も同じコマンドを使います。journalctl -u sshd の Bind to port 2222 on 0.0.0.0 failed: Permission denied は、2222 が ssh_port_t に登録されていないことを示します。そのため、デーモンを再起動してセッションを閉じる前に sudo semanage port -a -t ssh_port_t -p tcp 2222 を実行します。Red Hat 系のイメージで VPS 上の SSH を強化する一般的な手順に従う際、この手順を省略する人が多くいます。SELinux はファイアウォールでもないため、ポートは別途開放する必要があります。ここでは sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload を使うか、Debian または Ubuntu イメージでの ufwを使います。この --permanent フラグにも、ブール値での -P と同じ再起動時の注意点があります。また、ルールを適用するインターフェースを決めるゾーンについては、Rocky または AlmaLinux VPS 向け firewalld の基本で一度確認しておくとよいでしょう。
ブール値も変更できるラベルもない場合
通常のサーバーではまれですが、ここでシステムに被害を与えやすくなります。audit2allow は、ログに記録された拒否からポリシーモジュールを作成できます。
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppインストールする前に nginx_local.te を確認してください。安全に運用するには、2 つの点を守ります。-c で入力を対処中の 1 つのプログラムに絞り込んでください。無関係な拒否を 1 週間分 audit2allow に渡すと、それらすべてが一度に許可されるためです。また、内容を説明できない拒否から作成したモジュールは、決してインストールしないでください。サーバー上のすべてのファイルを httpd_t が読み取れるようにするルールは簡単に生成できますが、数か月後に見つけるのは困難です。sudo semodule -r nginx_local でモジュールを削除します。
Permissive は診断モードであり、修正方法ではありません
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1Permissive モードではアクセスを許可し、その内容をログに記録します。本当の価値は、拒否を漏れなく把握できることです。enforcing では最初の拒否が発生した時点でサービスが停止するため、その拒否を修正して再起動すると、次の拒否に遭遇します。Permissive では処理が継続し、1 回の実行ですべての拒否をログに収集できます。その後 enforcing に戻し、まとめて修正します。
setenforce は /etc/selinux/config に影響しないため、再起動するとシステムは enforcing に戻ります。これは安全策であり、setenforce 0 だけで構成した「修正」が最悪のタイミングで再発する理由でもあります。作業中に 1 つのサービスだけに一時的な許可が必要な場合は、システム全体ではなく、そのドメインを指定します。sudo semanage permissive -a httpd_t なら、それ以外はすべて enforcing のままになり、sudo semanage permissive -d httpd_t で元に戻せます。
SELinux を無効化するとラベル修正よりコストがかかる
SELINUX=disabled を /etc/selinux/config に設定すると、1 行のラベル修正で済む問題を、サーバーの防御力が恒久的に低下した状態へ置き換えることになります。この違いが現れるのは、Web アプリケーションが侵害されたときです。enforcing では、攻撃者のコードは httpd_t で実行されるため、Web コンテンツを読み取れる可能性はあります。しかし、Unix ユーザーであれば許可される操作であっても、/etc/shadow の読み取りや systemd unit の書き込みはポリシーによって拒否されます。ポリシーを読み込まなければ、同じコードがサービスアカウントに許可されたすべての操作を実行できます。
無効化すると、後で別のコストも発生します。ポリシーが読み込まれていない間に作成された新しいファイルにはラベルが付かないため、ファイルシステムの状態がポリシーと合わなくなります。その後 SELinux を再び有効にすると、完全な再ラベル付けが必要になります。そうしないと、多数のサービスが同時に失敗します。
sudo fixfiles -F onboot
sudo rebootこれにより /.autorelabel が書き込まれ、次回のブート時にすべてのファイルシステムが再ラベル付けされます。大容量ディスクでは長時間かかり、コンソールが停止したように見えます。そのため、待機できるタイミングで実行してください。いずれにしてもマシンを停止するため、その前に再起動が必要なものがほかにキューへ追加されていないか確認する価値があります。これは、dnf update の後に古いカーネルやライブラリがメモリに残っている場合に needs-restarting が報告する内容です。Rocky Linux と AlmaLinux 9 では、設定ファイルだけでカーネル側の機能を無効にすることはできなくなりました。SELinux を完全に無効化する公式の方法は、カーネル引数(sudo grubby --update-kernel ALL --args selinux=0)を使用することです。このコマンドを知っておくと、他者から引き継いだサーバーを扱う際に役立ちます。これは 403 の修正方法ではありません。
コンテナにもう 1 つラベルを追加する
Red Hat 系のホストでは、コンテナプロセスは container_t で実行され、container_file_t のラベルが付いたファイルだけを読み取れます。ホストからの bind mount は、ホスト上では ls -l が正常に見えても、コンテナ内では Permission denied で失敗します。:Z サフィックスを付けると、ランタイムがマウントのラベルを変更します。
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z は、そのコンテナ専用としてディレクトリにラベルを付けます。:z は、複数のコンテナ間で共有するディレクトリとしてラベルを付けます。:Z に他のサービスが使用するディレクトリを指定すると、そのディレクトリが再帰的に再ラベル付けされ、サービスが壊れます。そのため、コンテナ専用のパスを用意してください。まだエンジンをインストールしていない場合は、これらのディストリビューションでは docker コマンドが podman を指していることが多い点に注意してください。Rocky と AlmaLinux のインストール手順で、こうした問題に遭遇する前に対応できます。その他のセットアップは通常のイメージと同じで、VPS で Docker を実行するで説明しています。
Ubuntu と Debian では AppArmor を使用します
役割は同じですが、設計は異なります。AppArmor は、ディスク上のファイルにラベルを付けるのではなく、/etc/apparmor.d/ 配下のプロファイルを使用し、実行ファイルのパスに基づいてプログラムを制限します。再ラベル付けは不要で、restorecon もありません。まず次を実行します。
sudo aa-status
sudo journalctl -k | grep -i apparmor拒否は apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r" として表示されます。手順の流れは同じです。拒否内容を読み、プロファイルを特定し、ルールを変更します。sudo apt install apparmor-utils で aa-complain(1 つのプロファイルだけを permissive にする)を設定し、aa-enforce で元に戻せます。Ubuntu では、パッケージ化された一部のサービスだけを制限し、それ以外は制限しません。思い込みで判断せず、aa-status を読んで実際に有効な設定を確認してください。
両方のシステムに共通する習慣があります。サービスが、正しく見える対象について Permission denied と報告した場合は、権限を変更する前にセキュリティログを確認してください。権限ビットが同じ問題の原因になることは、ほとんどありません。
FAQ
nginx がファイル権限に問題がないのに 403 を返すのはなぜですか?
SELinux が読み取りを拒否したためです。権限ビットが原因ではありません。Web サーバーは httpd_t ドメインで実行され、Web コンテンツ用のラベルが付いたファイルだけを読み取れます。そのため、default_t または admin_home_t のラベルが付いたファイルは拒否され、nginx は提供するファイルを見つけられません。sudo ausearch -m AVC -ts recent で確認できます。このコマンドでは、scontext が httpd_t で終わり、tcontext が誤ったタイプを保持していることが示されます。正しいラベルを確認して記録し、適用します。sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" に続けて sudo restorecon -Rv /data/www を実行します。
setenforce 0 を実行してサービスを動作させても安全ですか?
setenforce 0 は診断手順であり、修正ではありません。問題を 1 回再現するために使用し、ログにすべての拒否をまとめて記録させます。その後、sudo ausearch -m AVC -ts recent でログを確認し、sudo setenforce 1 を実行して原因を修正します。permissive のままにしたサーバーはすべての拒否を記録するだけで、何もブロックしません。そのため、大量のログだけが残り、保護機能は失われます。作業中に 1 つのサービスだけに余地を与える必要がある場合は、sudo semanage permissive -a httpd_t を実行してください。マシンの他の部分は enforcing のまま維持できます。
SELinux を enforcing にしたまま、標準以外のポートでサービスを実行するにはどうすればよいですか?
そのサービスがバインドを許可されているタイプにポートを追加します。8081 で Web サーバーを実行する場合は、sudo semanage port -a -t http_port_t -p tcp 8081 です。2222 で SSH を実行する場合は、sudo semanage port -a -t ssh_port_t -p tcp 2222 です。まず sudo semanage port -l | grep -w http_port_t で現在の一覧を確認してください。すでに登録されているポートを追加すると、ValueError: Port tcp/8081 already defined で失敗します。この手順を実行しないと、他のプロセスがポートを使用していなくても、デーモンは起動時に bind() ... Permission denied で終了します。
Ubuntu に SELinux はありますか?
ありません。Ubuntu と Debian には AppArmor が搭載されています。AppArmor はファイルのラベルではなく、実行ファイルのパスに関連付けられたプロファイルを適用します。sudo aa-status で状態を確認し、sudo journalctl -k 内の apparmor="DENIED" 行を探してください。Ubuntu では、パッケージで提供される一部のサービスだけが制限されるため、多くのプログラムはデフォルトで制限なしに実行されます。Rocky Linux と AlmaLinux では、Fedora および RHEL と同様に、最初から SELinux が enforcing で動作しています。