
Linux パーミッションの仕組み - chmod・chown・umask と Permission denied の調べ方
当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。
Permission deniedが出たとき、とりあえずchmod 777やsudoで通してしまった経験は多くの人にあるはずです。動きはしますが、なぜ拒否されたのかは分からないままで、同じ場面でまた同じことを繰り返します。
Linuxのパーミッションは、9個のビットと所有者・グループの組み合わせという小さな仕組みです。ただし「ファイルを消せるかどうかはファイル自身の権限では決まらない」「所有者は、その他のユーザーより権限が弱くなることがある」など、直感と違う点がいくつかあります。この記事では、カーネルがどの順番で何を見て判定しているかを軸に、chmod・chown・umaskの使い方と、Permission deniedの切り分け手順を整理します。
NOTE
仕様は2026年11月12日に、man7.orgのpath_resolution(7)、inode(7)、chmod(1)、umask(2)、chown(2)、acl(5)と、OpenBSDのssh(1)で確認しました。「実行結果」と書いたものは、macOS 27.2にHomebrewで入れたGNU coreutils 9.9(gchmod・gstatなど)とbash 5.3.9で実行したものです。Linuxの実機では実行できなかったため、Linux専用のコマンド(namei・getfacl・lsattr)と、macOSで挙動が異なる部分の出力は「出力例」と明記し、manページの記述をもとに書いています。
先に結論: ls -lの読み方と数値の早見表
ls -lの先頭の10文字は、次のように区切って読みます。
-rw-r--r-- 1 alice staff 1024 Nov 12 09:00 notes.txt
drwxr-x--- 2 alice staff 4096 Nov 12 09:00 private| 位置 | 例 | 意味 |
|---|---|---|
| 1文字目 | - / d / l | ファイルの種類(通常ファイル、ディレクトリ、シンボリックリンク) |
| 2〜4文字目 | rw- | 所有者(user)の権限 |
| 5〜7文字目 | r-- | グループ(group)の権限 |
| 8〜10文字目 | r-- | その他(other)の権限 |
その後ろのaliceが所有者、staffがグループです。rは4、wは2、xは1として、3文字ごとに足した数字を並べたものが数値表記です。rw-r--r--は6・4・4で644になります。
よく使う値は次の4つです。
| 数値 | 文字 | 主な用途 |
|---|---|---|
644 | rw-r--r-- | 通常のファイル。所有者だけ書けて、全員が読める |
755 | rwxr-xr-x | ディレクトリ、実行ファイル、スクリプト |
600 | rw------- | 秘密鍵、パスワードを含む設定ファイルなど、本人しか読めないファイル |
700 | rwx------ | ~/.sshなど、本人しか入れないディレクトリ |
迷ったときは「ファイルは644、ディレクトリは755、見せたくないものは600と700」と覚えておけば、たいていの場面で足ります。
所有者・グループ・その他とrwx
3つの区分は「上から順に1つだけ」選ばれる
ファイルには所有者(UID)とグループ(GID)が1つずつ記録されています。これらはファイル名ではなくinodeに入っている情報で、権限の9ビットも同じ場所にあります。
プロセスがファイルにアクセスするとき、カーネルはpath_resolution(7)に書かれている通り、次の順番で使う区分を1つだけ決めます。
- プロセスの実効ユーザーIDがファイルの所有者と同じなら、所有者の3ビットを使う
- そうでなく、ファイルのグループがプロセスの実効グループIDか補助グループのどれかと同じなら、グループの3ビットを使う
- どちらでもなければ、その他の3ビットを使う
正確には、Linuxは実効ユーザーIDの代わりにfsuid(ファイルシステムユーザーID)を使います。ただし同じmanページに、fsuidは通常は実効ユーザーIDと同じ値で、setfsuid(2)はもう使うべきではないと書かれているので、普段は実効ユーザーIDと考えて構いません。
ここで大事なのは、1で所有者に一致したら、グループやその他のビットは見ないことです。所有者のビットで拒否されたら、その他にrが付いていても読めません。
echo hello > own.txt
chmod 044 own.txt
stat -c '%A %a %n' own.txt
cat own.txt----r--r-- 44 own.txt
cat: own.txt: Permission denied他のユーザーは読めるのに、所有者本人だけが読めないファイルになりました。「権限は足し合わせではなく、該当する区分のビットだけで決まる」と覚えておくと、グループの権限を絞ったのに効かない、といった場面で混乱しません。なお、所有者はchmodで自分のファイルのモードを変えられるので、この状態から元に戻すことはできます。
自分が誰なのかはidで確認する
判定に使われるのはプロセスのユーザーIDとグループIDです。idコマンドで確認できます。
uid=1000(alice) gid=1000(alice) groups=1000(alice),27(sudo),33(www-data)groups=に並んでいるのが補助グループです。usermod -aGなどでグループに追加した直後は、すでに動いているシェルやデーモンのプロセスには反映されていません。グループの一覧はプロセスが持っている情報なので、ログインし直すか、サービスを再起動する必要があります。cronやsystemdから動かすジョブは、対話シェルとは別のユーザーで動いていることも多いので、cronの記事で扱ったように「誰として動いているか」を最初に確認します。
rootとCAP_DAC_OVERRIDE
rootはパーミッションを無視できる、とよく言われます。Linuxではこの特権がケーパビリティという単位に分割されていて、ファイルの権限チェックに関係するのは次の2つです。
| ケーパビリティ | できること |
|---|---|
CAP_DAC_OVERRIDE | ファイルの読み・書き・実行の権限チェックを迂回する |
CAP_DAC_READ_SEARCH | ファイルの読み取りと、ディレクトリの読み取り・検索の権限チェックを迂回する |
DACは「discretionary access control(任意アクセス制御)」の略です。path_resolution(7)には注意点が1つ書かれています。CAP_DAC_OVERRIDEがあっても、実行の権限は、3つの実行ビットのうち少なくとも1つが立っているときにしか与えられません。rootでも、xがどこにも付いていないファイルは直接実行できないということです。
コンテナの中のrootがホストのrootほど強くないのも、このケーパビリティが絞られているためです。コンテナ内でrootなのにPermission deniedやOperation not permittedになる場合は、ケーパビリティやユーザー名前空間を疑います。
ファイルとディレクトリでrwxの意味が違う
同じrwxでも、通常ファイルとディレクトリでは意味が違います。パーミッションで一番つまずきやすいのはここです。
| ビット | 通常ファイル | ディレクトリ |
|---|---|---|
r | 中身を読める | 中にある名前の一覧を取得できる |
w | 中身を書き換えられる | 中にエントリ(名前)を作成・削除・改名できる |
x | プログラムとして実行できる | 中のエントリへたどり着ける(検索・通過できる) |
ディレクトリは「名前とinode番号の対応表」を持つファイルです。rはその表を読む権限、wは表を書き換える権限、xは表を使って名前からinodeを引く権限と考えると整理できます。path_resolution(7)でも、ディレクトリの3ビット目は実行ではなく検索(search)の権限と説明されています。
rだけ、xだけのディレクトリ
rだけを付けたディレクトリでは、名前の一覧は見えますが、中のファイルには触れません。
mkdir d
echo x > d/a.txt
echo y > d/b.txt
chmod 400 d
ls d
cat d/a.txt
ls -l da.txt
b.txt
cat: d/a.txt: Permission denied
ls: cannot access 'd/b.txt': Permission denied
ls: cannot access 'd/a.txt': Permission denied
total 0
-????????? ? ? ? ? ? a.txt
-????????? ? ? ? ? ? b.txt名前は分かるのに、サイズや所有者が?になっています。ls -lは名前ごとにstatでinodeの情報を取りに行きますが、xがないので名前からinodeへたどり着けないためです。
逆にxだけを付けると、一覧は見えませんが、名前を知っていれば中のファイルを読めます。
chmod 100 d
ls d
cat d/a.txtls: cannot open directory 'd': Permission denied
xファイルの削除は親ディレクトリのwで決まる
ファイルを削除する操作は、ファイルの中身を書き換えるのではなく、親ディレクトリの対応表から名前を消す操作です。そのため、削除できるかどうかはファイル自身のパーミッションではなく、親ディレクトリのw(とx)で決まります。unlink(2)のエラーの説明でも、EACCESになる条件は「対象を含むディレクトリへの書き込みが許可されていない」ことです。
chmod 500 d
rm d/a.txt
touch d/new
echo z >> d/a.txt && echo "追記はできた"
chmod 700 d
chmod 444 d/b.txt
rm -f d/b.txt && echo "読み取り専用のファイルを削除できた"rm: cannot remove 'd/a.txt': Permission denied
touch: d/new: Permission denied
追記はできた
読み取り専用のファイルを削除できたディレクトリにwがなければ、中のファイルが自分のもので書き込み可能でも、削除も新規作成もできません。ただしファイルの中身への追記はできます。反対に、ディレクトリにwがあれば、読み取り専用(444)のファイルでも削除できます。改名も同じで、モードが000のファイルでも、親ディレクトリに書き込めればmvで名前を変えられることを確認しました。
「消されたくないファイルを444にしておく」だけでは守れない、という点は覚えておく価値があります。rmが確認を求めてくることはありますが、それはコマンドの親切であって、カーネルが止めているわけではありません。
chmod: モードを変える
モードを変えられるのは、ファイルの所有者か特権を持つプロセスだけです(chmod(2)では、Linuxの場合はCAP_FOWNERと書かれています)。グループにwが付いていても、グループのメンバーはchmodできません。
数値(8進)表記
数値表記は、モード全体を一度に置き換えます。chmod(1)によると、1〜4桁の8進数で、省略した上の桁は0として扱われます。
| 桁 | 対象 | 値 |
|---|---|---|
| 4桁目(先頭、省略可) | 特殊ビット | setuid=4、setgid=2、sticky=1 |
| 3桁目 | 所有者 | r=4、w=2、x=1 |
| 2桁目 | グループ | 同上 |
| 1桁目 | その他 | 同上 |
chmod 640 fileなら、所有者がrw-(4+2)、グループがr--(4)、その他が---(0)です。
記号表記
記号表記は「誰の」「どうする」「どの権限」を組み合わせて書きます。今のモードの一部だけを変えられるのが利点です。
| 部分 | 記号 | 意味 |
|---|---|---|
| 誰の | u / g / o / a | 所有者、グループ、その他、全員 |
| どうする | + / - / = | 追加、削除、指定したものだけにする |
| どの権限 | r / w / x / X / s / t | 読み、書き、実行、条件付き実行、setuid・setgid、sticky |
touch f
chmod 644 f
chmod u+x f # 所有者に実行を追加
chmod go-r f # グループとその他から読み取りを削除
chmod a=r f # 全員を読み取りだけにする
chmod u=rw,go= f # 所有者はrw、グループとその他は権限なし-rw-r--r-- 644
-rwxr--r-- 744
-rwx------ 700
-r--r--r-- 444
-rw------- 600chmod +x fのように「誰の」を省くと全員が対象になりますが、umaskで落とされるビットは変更されません。意図をはっきりさせたいときはu+xやa+xと書きます。
大文字のXは「ディレクトリか、すでに実行可能なものだけ」
大文字のXは、chmod(1)の説明では「対象がディレクトリであるか、すでに誰かの実行ビットが立っている場合にだけ、実行・検索の権限を付ける」ものです。-Rと組み合わせて、ディレクトリには入れるようにしつつ、普通のファイルには実行ビットを付けない、という使い方をします。
chmod -R go+rX t適用前
drwx------ 700 t
-rw------- 600 t/a.txt
drwx------ 700 t/sub
-rwx------ 700 t/sub/run.sh
適用後
drwxr-xr-x 755 t
-rw-r--r-- 644 t/a.txt
drwxr-xr-x 755 t/sub
-rwxr-xr-x 755 t/sub/run.shディレクトリと、もともと実行可能だったrun.shにはxが付き、a.txtには付いていません。
chmod -Rの注意と、findでの使い分け
chmod -R 755 dirは、ディレクトリだけでなく中のすべてのファイルも755にします。テキストファイルや画像に実行ビットが付いてしまい、あとで「どれが本当に実行ファイルなのか」が分からなくなります。chmod -R 644 dirはもっと困ったことになり、ディレクトリからxが消えて中に入れなくなります。
ファイルとディレクトリで別のモードにしたいときは、findで種類ごとに分けます。
find t -type d -exec chmod 755 {} +
find t -type f -exec chmod 644 {} +drwxr-xr-x 755 t
-rw-r--r-- 644 t/a.txt
drwxr-xr-x 755 t/sub
-rw-r--r-- 644 t/sub/run.shこの方法ではrun.shの実行ビットも落ちます。実行ファイルが混ざっているツリーでは、先ほどのXを使うか、実行ファイルだけあとからchmod u+xします。
シンボリックリンクについては、chmod(1)に次のように書かれています。コマンドラインで指定したシンボリックリンクは、リンク自体ではなくリンク先のファイルのモードが変わります。-Rでたどっている途中に見つかったシンボリックリンクは、既定では無視されます。
何が変わったかを確認したいときは-c(変更があったものだけ表示)か-v(すべて表示)を付けます。
mode of 'own.txt' changed from 0644 (rw-r--r--) to 0600 (rw-------)chownとchgrp: 所有者とグループを変える
所有者とグループはchownで変えます。書式は所有者:グループです。
sudo chown alice file # 所有者だけ変える
sudo chown alice:www-data file # 所有者とグループを変える
sudo chown :www-data file # グループだけ変える(chgrpと同じ)
sudo chown -R alice:alice dir # 再帰的に変える
chgrp www-data file # グループだけ変える誰が変更できるかはchown(2)に書かれています。
- 所有者を変えられるのは、特権を持つプロセス(Linuxでは
CAP_CHOWNを持つもの)だけ - ファイルの所有者は、グループを自分が所属しているグループのどれかに変えられる
- 特権を持つプロセスは、グループを任意のものに変えられる
一般ユーザーは、自分のファイルを他人のものにすることができません。実行すると次のようになります。
chown: changing ownership of 'own.txt': Operation not permittedエラーがPermission denied(EACCES)ではなくOperation not permitted(EPERM)である点に注目してください。この2つは別のエラーです。おおまかに言うと、Permission deniedは「ファイルのパーミッションで拒否された」、Operation not permittedは「その操作をする資格(所有者であること、特権があること)がない」ときに出ます。メッセージの違いは切り分けの手がかりになります。
もう1つ、chown(2)には、実行ファイルの所有者やグループを変えるとsetuidビットとsetgidビットが落とされると書かれています。権限を持つ別のユーザーのファイルに見せかけることを防ぐためです。
umask: 新しく作るファイルのモードを決める
引き算ではなくビットのマスク
新しく作ったファイルが644になり、ディレクトリが755になるのは、umaskの働きです。umask(2)によると、umaskはプロセスごとの「ファイルモード作成マスク」で、open(2)やmkdir(2)に渡されたモードから、umaskで立っているビットを落とします。
プログラムはファイルを作るとき、通常ファイルなら0666、ディレクトリなら0777を要求するのが普通です。そこからumaskのビットが消されます。manページの例の通り、umaskが022なら次のようになります。
ファイル: 0666 & ~022 = 0644 (rw-r--r--)
ディレクトリ: 0777 & ~022 = 0755 (rwxr-xr-x)umaskの値を変えて、実際に作られるモードを確かめます。
for m in 022 002 027 077; do
(umask $m; touch u$m; mkdir ud$m
echo "umask $m: $(stat -c '%a %A' u$m) $(stat -c '%a %A' ud$m)")
doneumask 022: 644 -rw-r--r-- 755 drwxr-xr-x
umask 002: 664 -rw-rw-r-- 775 drwxrwxr-x
umask 027: 640 -rw-r----- 750 drwxr-x---
umask 077: 600 -rw------- 700 drwx------「666からumaskを引く」と説明されることがありますが、正しくはビットを消すマスクです。違いが出るのは、基点で立っていないビットをumaskが指定しているときです。umaskが033の場合、引き算なら633になりそうですが、実際に作られたファイルは644でした。666の時点で実行ビットは立っていないので、umaskの1のビットは消すものがなく、2のビットだけが効きます。
基点が666であることから、umaskをどう設定しても、touchやリダイレクトで作ったファイルに実行ビットは付きません。実行ビットはchmodで明示的に付けるものです。
確認と設定
シェルのumaskコマンドで現在の値を確認できます。-Sを付けると、落とすビットではなく残るビットが記号で表示されます。
umask
umask -S0022
u=rwx,g=rx,o=rxumaskはプロセスの属性で、umask(2)によるとfork(2)で子プロセスに引き継がれ、execve(2)でも変わりません。シェルでumask 077を実行すると、そのシェルと、そこから起動するコマンドすべてに効きます。スクリプトの中で一時的に変えたいときは、上の例のように丸括弧のサブシェルで囲むと、外側のumaskを変えずに済みます。
注意が1つあります。umask(2)には、親ディレクトリにデフォルトACLが設定されている場合、umaskは無視され、デフォルトACLが引き継がれると書かれています。umaskを変えたのに新規ファイルのモードが思った通りにならないときは、後述のgetfaclで親ディレクトリを確認します。
既定値はディストリビューションで違う
umask(2)のNOTESには、典型的な既定値は022と書かれています。ただし実際の値は、ログイン時の設定で決まります。確認できた範囲は次の通りです。
- login.defs(5):
/etc/login.defsのUMASKで初期値を指定する。指定がなければ022になる。この値はpam_umaskの既定値としても使われる - pam_umask(8): ユーザーのGECOSフィールドの
umask=、モジュールの引数のumask=、/etc/login.defsのUMASK、/etc/default/loginのUMASK=の順に値を探す - 同じくpam_umask(8)の
usergroupsオプション: rootでないユーザーで、ユーザー名とプライマリグループ名が同じ場合、umaskのグループのビットを所有者のビットと同じにする(manページの例では022が002に、077が007になる)
つまり、ユーザーごとに専用のグループを作る方式のディストリビューションでは、一般ユーザーのumaskが002になり、新しいファイルが664で作られることがあります。DebianやRed Hat Enterprise Linuxの各バージョンで、実際にどの値が既定になっているかは、この記事では未確認です。使っている環境でumaskを実行して確かめてください。また、systemdのサービスやcronのジョブは、ログインシェルとは別の経路で起動されるので、対話シェルと同じumaskとは限りません。
特殊ビット: setuid・setgid・sticky bit
9ビットの上に、さらに3つのビットがあります。inode(7)での定義は次の通りです。
| ビット | 8進 | 実行ファイルでの意味 | ディレクトリでの意味 |
|---|---|---|---|
| setuid | 4000 | 実行したプロセスの実効ユーザーIDが、ファイルの所有者になる | (inode(7)には記載なし) |
| setgid | 2000 | 実行したプロセスの実効グループIDが、ファイルのグループになる | 中に作られるファイルが、ディレクトリのグループを引き継ぐ |
| sticky | 1000 | (chmod(1)によると、古いシステムでの用途) | 中のファイルを削除・改名できる人が限定される |
setuidとsetgid(実行ファイル)
execve(2)によると、setuidビットが立ったプログラムを実行すると、プロセスの実効ユーザーIDがそのファイルの所有者に変わります。一般ユーザーが、root所有のプログラムを通じて、普段は書き込めないファイルを更新できるようにするための仕組みです。
強力な分、危険でもあります。execve(2)には、次の場合はsetuid・setgidビットが無視されると書かれています。
- 呼び出したスレッドに
no_new_privs属性が設定されている - ファイルシステムが
nosuidオプションでマウントされている - 呼び出したプロセスがptraceされている
自作のスクリプトやプログラムにsetuidを付けて権限の問題を解決しようとするのは避けてください。必要な権限はsudoの設定やグループで与えるほうが安全です。
setgid(ディレクトリ)
ディレクトリのsetgidは、共有ディレクトリでよく使います。inode(7)によると、setgidが立ったディレクトリの中に作られたファイルは、作成したプロセスの実効グループIDではなくディレクトリのグループを引き継ぎ、中に作られたディレクトリにもsetgidが付きます。
sudo mkdir /srv/shared
sudo chgrp developers /srv/shared
sudo chmod 2775 /srv/shareddrwxrwsr-x 2 root developers 4096 Nov 12 09:00 /srv/sharedこれで、developersグループのメンバーが作ったファイルは、作成者のプライマリグループではなくdevelopersグループになります。ただし、ファイルのモードはumaskで決まるので、umaskが022だと、グループは引き継がれてもwが付かず、他のメンバーが編集できません。共有ディレクトリでは、umaskを002にするか、デフォルトACLを併用します。
なお、chmod(2)には、ファイルのグループが自分の所属グループと一致しない場合、特権のないプロセスがchmodするとsetgidビットが黙って落とされる(エラーにはならない)と書かれています。手元のmacOSで試したときも、所属していないグループのファイルにchmod 2755を実行したところ、エラーなしで755になりました。
sticky bit(/tmpの1777)
/tmpは誰でも書き込めるディレクトリです。前の章で見た通り、ディレクトリにwがあれば中のファイルを誰のものでも削除できてしまうので、そのままでは他人の一時ファイルを消し放題になります。これを防ぐのがsticky bitです。
stat -c '%A %a %n' /tmp/drwxrwxrwt 1777 /tmp/inode(7)によると、sticky bitが立ったディレクトリの中のファイルを改名・削除できるのは、そのファイルの所有者、ディレクトリの所有者、特権を持つプロセスだけです。chmod(1)ではこれを「restricted deletion flag(削除制限フラグ)」と呼んでいます。
大文字のSとTの意味
特殊ビットは、ls -lでは実行ビットの位置に重ねて表示されます。
chmod 4755 s; stat -c '%A %a' s
chmod 4644 s; stat -c '%A %a' s
chmod 1777 sd; stat -c '%A %a' sd
chmod 1770 sd; stat -c '%A %a' sd-rwsr-xr-x 4755
-rwSr--r-- 4644
drwxrwxrwt 1777
drwxrwx--T 1770| 表示 | 意味 |
|---|---|
小文字のs / t | 特殊ビットが立っていて、その位置の実行ビットxも立っている |
大文字のS / T | 特殊ビットは立っているが、その位置の実行ビットxは立っていない |
大文字のSは、たいてい設定の間違いです。実行できないファイルにsetuidを付けても意味がありません。chmod 4644のように数値で指定したときに起きやすいので、大文字を見かけたら意図した設定かを確認します。
パーミッションは正しく見えるのにPermission deniedになる要因
ls -lで見る限り権限はあるのに拒否される、という場面では、9ビット以外の要因を順に確認します。
親ディレクトリのx
最も多い原因です。ファイルを開くには、そのファイルだけでなく、パスの途中にあるすべてのディレクトリにx(検索の権限)が必要です。path_resolution(7)にも、パスを解決する各段階で、プロセスがそのディレクトリの検索権限を持っていなければEACCESになると書かれています。
mkdir -p a/b/c
echo hi > a/b/c/f.txt
chmod 600 a/b
cat a/b/c/f.txtcat: a/b/c/f.txt: Permission deniedf.txtは644、cは755ですが、途中のa/bにxがないのでたどり着けません。エラーメッセージには末尾のファイル名しか出ないので、ファイルだけを見ていると原因が分かりません。ホームディレクトリが700や750のときに、その下に置いたファイルをWebサーバーが読めない、というのが典型的な例です。
ACL
ACL(アクセス制御リスト)は、所有者・グループ・その他の3区分に加えて、特定のユーザーやグループごとに権限を設定できる仕組みです。ACLが付いているファイルは、ls -lでモードの直後に+が表示されます。POSIXのlsの仕様では、この位置は「追加のアクセス制御方式があるときに印字可能な1文字を表示する」場所で、方式が1つだけの実装には+を使うことが推奨されています。
-rw-rw-r--+ 1 alice staff 1024 Nov 12 09:00 report.txt中身はgetfacl(1)で確認し、setfacl(1)で設定します。
getfacl report.txt
setfacl -m u:bob:rw report.txt # bobに読み書きを許可する
setfacl -x u:bob report.txt # bobのエントリを削除する
setfacl -d -m g:developers:rwX dir # ディレクトリにデフォルトACLを設定する# file: report.txt
# owner: alice
# group: staff
user::rw-
user:bob:rw-
group::r--
mask::rw-
other::r--acl(5)のアクセス判定の手順は、通常のパーミッションと同じ考え方です。所有者なら所有者のエントリ、次に名前付きユーザーのエントリ、次にグループのエントリ、最後にその他のエントリの順に見て、最初に該当したところで許可か拒否かが決まります。名前付きユーザーとグループのエントリにはmaskが上限としてかかります。
ACLがあるとき、ls -lのグループの3文字は、グループの権限ではなくmaskの値を表示しています(acl(5)の「ACLのエントリとパーミッションビットの対応」の節)。そのため、chmod g-wを実行するとmaskが変わり、名前付きユーザーの実効権限まで下がることがあります。+が付いているファイルは、ls -lではなくgetfaclで判断してください。
noexecマウント
実行ビットが付いているのに実行できない場合は、ファイルシステムのマウントオプションを確認します。mount(8)によると、noexecは、そのファイルシステム上のバイナリを直接実行することを許可しないオプションです。/tmpや外付けのディスク、共有フォルダに付いていることがあります。
findmnt -T /path/to/fileOPTIONSの列にnoexecがあれば、それが原因です。sh script.shのようにインタプリタに読ませる形なら、スクリプトのファイル自体を実行するわけではないので動きます。
immutable属性
rootでも書き込みや削除ができない場合は、ファイル属性を確認します。chattr(1)によると、i(immutable)属性が付いたファイルは、変更も削除も改名もできず、リンクも作れず、書き込みモードで開くこともできません。この属性を付け外しできるのは、スーパーユーザーかCAP_LINUX_IMMUTABLEを持つプロセスだけです。
lsattr file # 属性を表示する
sudo chattr +i file # immutableにする
sudo chattr -i file # immutableを外す----i---------e------- fileこの場合のエラーは、unlink(2)やchown(2)の説明にある通りEPERMなので、メッセージはOperation not permittedになります。
SELinuxとAppArmor
ここまでの仕組みは、所有者が自分の判断で権限を決められる「任意アクセス制御(DAC)」です。これとは別に、システム管理者が決めたポリシーをカーネルが強制する「強制アクセス制御(MAC)」があり、LinuxではSELinuxとAppArmorが代表的です。
MACは、パーミッションで許可された操作をさらに絞り込みます。rootで動いていても、パーミッションが777でも、ポリシーで許可されていなければ拒否されます。Webサーバーが、決められた場所以外に置かれたファイルを読めない、といった形で現れます。
詳しい設定はこの記事の範囲を超えるので、確認の入口だけ挙げます。
getenforce # SELinuxのモード(Enforcing / Permissive / Disabled)
ls -Z file # SELinuxのコンテキストを表示する
sudo aa-status # AppArmorのプロファイルの状態
sudo dmesg | tail # 拒否のログが出ていないか拒否された記録は、監査ログやカーネルのログに残ります。原因がMACだと分かった場合は、無効化するのではなく、ポリシーやファイルのラベルを直すのが本来の対応です。
Permission deniedの切り分け手順
実際に調べるときは、次の順番で確認すると抜けがありません。
- 誰として実行しているか:
idでユーザーと所属グループを確認する。サービスなら、設定ファイルの実行ユーザーを確認する - 対象のファイルやディレクトリ:
ls -ldかstatでモード・所有者・グループを見る - パスの途中:
namei -lで、ルートから対象までのすべてのディレクトリを見る - ACL:
ls -lに+があればgetfaclで見る - マウントオプションと属性:
findmnt -Tとlsattrで見る - MAC: SELinuxやAppArmorのログを見る
statで1つのファイルを詳しく見る
ls -ldの-dは、ディレクトリを指定したときに中身ではなくディレクトリ自体を表示するオプションです。statは、モードを8進と文字の両方で表示してくれます。
stat own.txt
stat -c '%A %a %U:%G %n' own.txt File: own.txt
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 1,15 Inode: 396538831 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 501/alice) Gid: ( 0/ wheel)
Access: 2026-11-12 05:06:14.583084568 +0900
Modify: 2026-11-12 05:06:14.583330443 +0900
Change: 2026-11-12 05:06:14.636117403 +0900
Birth: 2026-11-12 05:06:14.583084568 +0900-cに指定できる書式のうち、よく使うのは%A(文字のモード)、%a(8進のモード)、%Uと%G(所有者名とグループ名)、%n(ファイル名)です。Changeの行は、モードや所有者などinodeの情報を最後に変えた時刻なので、「いつ権限が変わったのか」を調べる手がかりになります。
namei -lでパス全体を見る
親ディレクトリのxが原因のときは、namei(1)が便利です。-lを付けると、パスを構成する要素を1つずつ、モード・所有者・グループと一緒に表示します。util-linuxに含まれるコマンドで、macOSには入っていないため、次の出力は書式を示す例です。
namei -l /home/alice/site/index.htmlf: /home/alice/site/index.html
drwxr-xr-x root root /
drwxr-xr-x root root home
drwx------ alice alice alice
drwxr-xr-x alice alice site
-rw-r--r-- alice alice index.htmlこの例では、/home/aliceが700なので、alice以外のユーザー(たとえばWebサーバーのwww-data)は、その下のファイルが644でもたどり着けません。上から順に見て、対象のユーザーにxがない最初のディレクトリが原因です。
対象のユーザーになって試す
設定を直したら、実際にそのユーザーとしてアクセスできるかを確かめます。
sudo -u www-data cat /home/alice/site/index.html
sudo -u www-data test -r /home/alice/site/index.html && echo readable自分のシェルで読めても意味がありません。判定に使われるのは、アクセスするプロセスのユーザーIDとグループIDです。システムコールのレベルで何が拒否されたかを知りたいときは、straceでどの呼び出しがEACCESを返したかを見る方法もあります(システムコールの記事を参照)。
よくある場面
SSHの秘密鍵と~/.ssh
SSHは、鍵や設定ファイルのパーミッションが緩いと、そのファイルを使いません。ssh(1)のFILESの節には、次のように書かれています。
| パス | manページの記述 |
|---|---|
~/.ssh/ | 推奨は、本人が読み・書き・実行でき、他人はアクセスできないこと |
~/.ssh/config | 厳格な権限が必要。本人が読み書きでき、他人が書き込めないこと |
秘密鍵(~/.ssh/id_ed25519など) | 本人だけが読めること。他人がアクセスできる秘密鍵を、sshは単に無視する |
~/.ssh/authorized_keys | 推奨は、本人が読み書きでき、他人はアクセスできないこと |
秘密鍵を644にして試すと、次のメッセージが出ます。
chmod 644 k
ssh-keygen -y -f k@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'k' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "k": bad permissionsssh-keygenで作った直後の鍵は、秘密鍵が600、公開鍵が644でした。鍵を別のマシンからコピーしたり、アーカイブから展開したりするとモードが変わることがあるので、次のように揃えます。
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config ~/.ssh/authorized_keys
chmod 644 ~/.ssh/id_ed25519.pubサーバー側にも同様のチェックがあります。sshd(8)によると、authorized_keys、~/.sshディレクトリ、ユーザーのホームディレクトリのいずれかが他のユーザーから書き込み可能な場合、sshd_config(5)のStrictModesがnoでない限り、sshdはそのファイルを使いません(StrictModesの既定値はyes)。鍵は合っているのに公開鍵認証が通らないときは、ホームディレクトリにchmod 777やg+wをしていないかを確認します。公開鍵認証の流れはSSHの記事で説明しています。
シェルスクリプトが実行できない
./run.sh
echo "終了ステータス: $?"
sh run.sh
chmod u+x run.sh
./run.shbash: ./run.sh: Permission denied
終了ステータス: 126
ok
ok./run.shの形で起動するには、ファイルにxが必要です。終了ステータスの126は、コマンドは見つかったが実行できなかったことを表します(見つからない場合は127です)。sh run.shは、shがファイルを読んで解釈するだけなので、rがあれば動きます。
xを付けても動かないときは、次の点を確認します。
- 実行するユーザーに
rもあるか(スクリプトは、インタプリタが中身を読めなければ実行できません) - 1行目のシバン(
#!/bin/bashなど)に書いたインタプリタが存在し、実行できるか - ファイルシステムが
noexecでマウントされていないか
スクリプトの書き方そのものは、set -euo pipefailの記事も参考になります。また、sudo echo text > /etc/fileがPermission deniedになるのは、パーミッションの設定ではなくリダイレクトを処理するのが誰か、という問題です。パイプとリダイレクトの記事で説明しています。
Webサーバーの公開ディレクトリ
Webサーバーのプロセスは、www-dataやnginxなど専用のユーザーで動いています。そのユーザーから見て、次の2つが満たされている必要があります。
- 公開するファイルに
rがある - ルートからそのファイルまでのすべてのディレクトリに
xがある
基本の形は、ファイルを置くユーザー(デプロイ用のユーザー)を所有者にして、ディレクトリを755、ファイルを644にすることです。Webサーバーは「その他」として読むだけになります。
sudo chown -R alice:alice /var/www/site
find /var/www/site -type d -exec chmod 755 {} +
find /var/www/site -type f -exec chmod 644 {} +アップロードやキャッシュのために、Webサーバーが書き込む必要のあるディレクトリがある場合は、そのディレクトリだけグループをwww-dataにして、グループにwを付けます。
sudo chgrp -R www-data /var/www/site/storage
sudo chmod -R g+rwX /var/www/site/storage
sudo chmod g+s /var/www/site/storageサイト全体をWebサーバーのユーザーの所有にして書き込み可能にする設定は避けます。アプリケーションに脆弱性があったときに、プログラムのファイルまで書き換えられてしまうためです。データベースのパスワードなどを含む設定ファイルは、640にしてグループをwww-dataにすると、Webサーバーは読めて、他のユーザーは読めない状態にできます。
chmod 777を避ける理由
chmod 777は「誰でも読める・書ける・実行できる」にする設定です。動かない原因が分からないときに使われがちですが、次の問題があります。
- 原因が分からないままになる: 足りなかったのが親ディレクトリの
xなのか、所有者の違いなのかが分からず、次に同じ問題が起きたときも同じ対応をすることになる - そのマシンの全ユーザーと全プロセスが書き換えられる: Webアプリケーションや別のサービスが侵害されたとき、そのプロセスからファイルを改ざんしたり、ディレクトリに実行ファイルを置いたりできる
- かえって動かなくなるものがある: 前述の通り、SSHは他人がアクセスできる秘密鍵を無視し、sshdは他人が書き込めるホームディレクトリや
authorized_keysを既定で拒否する - 効かないことがある: 原因がACL、
noexec、immutable属性、SELinuxやAppArmorの場合、777にしても拒否されたままになる
「誰が」「何をする必要があるのか」を先に決めて、所有者とグループで表現するのが基本です。所有者にrw、特定のグループにrかrw、その他は必要ならr、という3段階でほとんどの要件は表せます。それで足りない場合に、ACLを検討します。
まとめ
- モードは所有者・グループ・その他の3区分にそれぞれ
rwxの3ビット。数値はr=4、w=2、x=1の合計 - カーネルは、所有者、グループ、その他の順に該当する区分を1つだけ選ぶ。所有者に一致したら、グループやその他のビットにはフォールバックしない
- ディレクトリの
rは一覧、wはエントリの作成・削除・改名、xは通過。ファイルの削除は、ファイル自身ではなく親ディレクトリのwで決まる chmodは数値で全体を置き換えるか、u+xやgo-wで一部だけ変える。-Rでは大文字のXか、findでファイルとディレクトリを分ける- 所有者を変えられるのは特権を持つプロセスだけ。所有者自身は、グループを自分の所属グループに変えられる
- umaskは引き算ではなくビットのマスク。基点はファイルが666、ディレクトリが777。既定値は環境によって違うので
umaskで確認する - setgidを付けたディレクトリでは、新しいファイルがディレクトリのグループを引き継ぐ。sticky bitを付けたディレクトリでは、他人のファイルを削除できない
ls -lの大文字のSやTは、特殊ビットはあるが実行ビットがない状態- パーミッションが正しく見えるのに拒否されるときは、親ディレクトリの
x、ACL、noexec、immutable属性、SELinuxやAppArmorを順に疑う - 切り分けは
id、ls -ld、stat、namei -l、getfaclの順。直したら、対象のユーザーになって確かめる chmod 777は原因を隠すだけで、SSHのようにかえって拒否されるものもある
参考リンク
- path_resolution(7)(man7.org)
- inode(7)(man7.org)
- capabilities(7)(man7.org)
- chmod(1)(man7.org、GNU coreutils)
- chmod(2)(man7.org)
- chown(2)(man7.org)
- umask(2)(man7.org)
- execve(2)(man7.org)
- unlink(2)(man7.org)
- acl(5)(man7.org)
- getfacl(1)(man7.org)
- setfacl(1)(man7.org)
- login.defs(5)(man7.org)
- pam_umask(8)(man7.org)
- mount(8)(man7.org)
- chattr(1)(man7.org)
- namei(1)(man7.org)
- chmod(POSIX.1-2024)
- ls(POSIX.1-2024)
- ssh(1)(OpenBSD manual pages)
- sshd(8)(OpenBSD manual pages)
- sshd_config(5)(OpenBSD manual pages)


