SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-13

SELinuxでnginxが403になる原因と直し方

nginxが403を返し、Unix権限も正しい場合はSELinuxの拒否を確認します。監査ログのdenialを読み、semanageとrestoreconでラベルを直してenforcingのまま運用する方法を解説します。

権限に問題がないファイルで nginx が 403 を返す理由

ファイルの権限ビットが正しいにもかかわらず nginx が 403 を返す場合、そのほとんどは SELinux(security-enhanced Linux)が読み取りを拒否していることが原因です。SELinux は通常の権限チェックを通過した後に、別のルールセットも確認します。Web サーバーが読み取れるのは、Web コンテンツ用のラベルが付いたファイルだけです。このファイルには別のラベルが付いているため、open に失敗し、nginx は送信する内容を取得できません。

mode だけでなく、ラベルも確認します。

ls -ldZ /data/www /data/www/index.html
drwxr-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.html

drwxr-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 -Z

nginx のワーカープロセスのコンテキストは、httpd_t で終わっています。ログインシェルには unconfined_u:unconfined_r:unconfined_t:s0 が表示されます。これは、デフォルトの targeted ポリシーがサービスを制限し、対話的なユーザーには制限を適用しないためです。この点は重要です。SELinux は、最小権限のユーザーでサービスを実行することの代わりにはなりません。サービスが侵害された後に、そのサービスがアクセスできる範囲を制限します。

3 つのモードと、SELinux が有効なイメージ

sestatus
getenforce

Enforcing はアクセスをブロックし、その内容をログに記録します。Permissive はすべてを許可し、本来ブロックするはずだったアクセスをログに記録します。Disabled はポリシーをまったく読み込みません。getenforce は現在のモードを表示します。sestatus/etc/selinux/config に設定されたモードも表示します。これは再起動後に戻るモードです。

Rocky Linux、AlmaLinux、Fedora、RHEL は targeted ポリシーで SELinux を Enforcing として提供します。Ubuntu と Debian は代わりに AppArmor を提供します。AppArmor も別の仕組みで同じ役割を果たします(最後のセクションで説明します)。そのため、同じアプリケーションでも、一方のサーバーでは正常にインストールでき、別のサーバーでは 403 を返すことがあります。

必要になる前にツールをインストールする

sudo dnf install -y policycoreutils-python-utils setroubleshoot-server

最小構成のイメージで semanage: command not found が発生する場合、policycoreutils-python-utils が不足しています。このパッケージには semanageaudit2allow が含まれています。setroubleshoot-serversealert を追加し、各拒否の概要を平易な英語で journal に記録します。新しいサーバーでは両方をインストールしてください。必要になる時点では、すでに何かが壊れている可能性が高いためです。

監査ログで SELinux の拒否を読み取る方法

各拒否は、audit daemon によって AVC (access vector cache) メッセージとして記録されます。

sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent
type=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=0

4 つのフィールドで全体を把握できます。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.log

audit2why は同じ記録を読み取り、認識した原因を示します。無効になっている boolean、ポリシーと一致しないラベル、またはルールが存在しないことなどです。sealert はログ全体を調べ、拒否ごとに推奨コマンドを表示します。推奨内容は参考情報として扱ってください。リリースによって文言が変わり、sealert が、1 行のラベル修正で解決できる場合にもカスタムポリシーモジュールを提案することがあります。

もう 1 つ把握しておく点があります。ポリシーには、無害と判断された拒否を隠す dontaudit ルールが含まれています。そのため、ログに何も残らないままプログラムが正常に動作しないことがあります。1 回のテスト中だけ、これらを表示させます。

sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -B

semanage 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、パッケージ更新、または完全な再ラベル付けで元に戻ります。ポリシーでは、そのパスに別のラベルを付けることになっているためです。永続化される方法は semanage fcontext です。記録済みの設定は sudo semanage fcontext -l | grep '^/data' で一覧表示できます。

サービスが書き込む必要のあるコンテンツには、別のタイプが必要です。アップロードディレクトリやキャッシュには httpd_sys_rw_content_t を使用してください。ただし、そのパスに限定してください。読み取り専用のサイトを書き込み可能なタイプにすると、アプリケーションに必要な範囲を超えるアクセスを許可することになります。

そもそも、なぜラベルが間違っていたのでしょうか。ほとんどの場合、ファイルの配置方法が原因です。mv はファイルの既存のラベルを保持します。そのため、サイトを /root から移動すると admin_home_t のラベルで配置され、そのまま残ります。通常の cp では、移動先ディレクトリのデフォルトラベルが新しいファイルに付きます。通常はこちらが必要な動作です。一方、cp -arsync -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 upstream

アップストリームには問題ありません。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_thttp_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 sshdBind 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 を使用する方法を取ります。

ブール値も変更可能なラベルもない場合

通常のサーバーではまれなケースですが、ここで設定を壊す事故が起きます。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 1

permissive モードではアクセスを許可し、そのアクセスをログに記録します。真の価値は、すべての拒否を把握できることにあります。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 コンテンツを読み取れる可能性はあります。一方、/etc/shadow の読み取りや systemd unit の書き込みは、Unix ユーザーが許可していてもポリシーによって拒否されます。ポリシーを読み込まない場合、同じコードはサービスアカウントに与えられた権限をすべて取得します。

無効化すると、後で別のコストも発生します。ポリシーを読み込んでいない間に作成された新しいファイルにはラベルが付かないため、ファイルシステムの状態がポリシーと合わなくなります。その後 SELinux を再び有効にすると、ファイルシステム全体の再ラベル付けが必要になります。再ラベル付けを行わないと、多数のサービスが一度に失敗することがあります。

sudo fixfiles -F onboot
sudo reboot

これにより /.autorelabel が書き込まれ、次回のブート時にすべてのファイルシステムが再ラベル付けされます。大容量ディスクでは長時間かかり、コンソールが停止したように見えるため、待機できるときに実行してください。Rocky Linux と AlmaLinux 9 では、設定ファイルを変更するだけでカーネル側の SELinux が無効になることはありません。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 に他のサービスが使用するディレクトリを指定すると、そのディレクトリが再帰的に再ラベル付けされ、サービスが動作しなくなります。そのため、コンテナ専用のパスを用意してください。その他の構成は、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-utilsaa-complain(1 つのプロファイルだけを permissive にする)を設定し、aa-enforce で元に戻せます。Ubuntu はパッケージとして提供される一部のサービスだけを制限し、それ以外は制限しません。思い込みで判断せず、実際に有効な設定を確認するには aa-status を読みます。

両方のシステムに共通する習慣があります。サービスが、正しく見える対象に対して Permission denied と報告した場合は、権限を変更する前にセキュリティログを確認します。権限ビットが 2 回続けて原因になることは、ほとんどありません。

FAQ

ファイルの権限が正しいのに nginx が 403 を返すのはなぜですか?

SELinux が読み取りを拒否しているためです。権限ビットが原因ではありません。Web サーバーは httpd_t ドメインで実行され、Web コンテンツ用のラベルが付いたファイルだけを読み取れます。そのため、default_t または admin_home_t のラベルが付いたファイルは拒否され、nginx は配信するものがありません。sudo ausearch -m AVC -ts recent で確認できます。このコマンドでは、scontexthttpd_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 にしたまま、標準以外のポートでサービスを実行するにはどうしますか?

そのサービスが bind を許可されているタイプにポートを追加します。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 になっています。