Linux パーミッションの仕組み - chmod・chown・umask と Permission denied の調べ方

Linux パーミッションの仕組み - chmod・chown・umask と Permission denied の調べ方

作成日:
読了:約50分
更新日:

当サイトは 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文字は、次のように区切って読みます。

ls -lの出力例
-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つです。

数値文字主な用途
644rw-r--r--通常のファイル。所有者だけ書けて、全員が読める
755rwxr-xr-xディレクトリ、実行ファイル、スクリプト
600rw-------秘密鍵、パスワードを含む設定ファイルなど、本人しか読めないファイル
700rwx------~/.sshなど、本人しか入れないディレクトリ

迷ったときは「ファイルは644、ディレクトリは755、見せたくないものは600と700」と覚えておけば、たいていの場面で足ります。

所有者・グループ・その他とrwx

3つの区分は「上から順に1つだけ」選ばれる

ファイルには所有者(UID)とグループ(GID)が1つずつ記録されています。これらはファイル名ではなくinodeに入っている情報で、権限の9ビットも同じ場所にあります。

プロセスがファイルにアクセスするとき、カーネルはpath_resolution(7)に書かれている通り、次の順番で使う区分を1つだけ決めます。

  1. プロセスの実効ユーザーIDがファイルの所有者と同じなら、所有者の3ビットを使う
  2. そうでなく、ファイルのグループがプロセスの実効グループIDか補助グループのどれかと同じなら、グループの3ビットを使う
  3. どちらでもなければ、その他の3ビットを使う

正確には、Linuxは実効ユーザーIDの代わりにfsuid(ファイルシステムユーザーID)を使います。ただし同じmanページに、fsuidは通常は実効ユーザーIDと同じ値で、setfsuid(2)はもう使うべきではないと書かれているので、普段は実効ユーザーIDと考えて構いません。

ここで大事なのは、1で所有者に一致したら、グループやその他のビットは見ないことです。所有者のビットで拒否されたら、その他にrが付いていても読めません。

所有者には権限がなく、グループとその他にだけ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コマンドで確認できます。

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だけを付けたディレクトリでは、名前の一覧は見えますが、中のファイルには触れません。

rだけのディレクトリ
mkdir d
echo x > d/a.txt
echo y > d/b.txt
chmod 400 d
ls d
cat d/a.txt
ls -l d
実行結果
a.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だけを付けると、一覧は見えませんが、名前を知っていれば中のファイルを読めます。

xだけのディレクトリ
chmod 100 d
ls d
cat d/a.txt
実行結果
ls: 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、グループとその他は権限なし
各コマンドの直後にstat -c '%A %a'で確認した実行結果
-rw-r--r-- 644
-rwxr--r-- 744
-rwx------ 700
-r--r--r-- 444
-rw------- 600

chmod +x fのように「誰の」を省くと全員が対象になりますが、umaskで落とされるビットは変更されません。意図をはっきりさせたいときはu+xやa+xと書きます。

大文字のXは「ディレクトリか、すでに実行可能なものだけ」

大文字のXは、chmod(1)の説明では「対象がディレクトリであるか、すでに誰かの実行ビットが立っている場合にだけ、実行・検索の権限を付ける」ものです。-Rと組み合わせて、ディレクトリには入れるようにしつつ、普通のファイルには実行ビットを付けない、という使い方をします。

go+rXを再帰的に適用する
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で種類ごとに分けます。

ディレクトリは755、ファイルは644に揃える
find t -type d -exec chmod 755 {} +
find t -type f -exec chmod 644 {} +
実行結果(すべて777にした状態から実行)
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(すべて表示)を付けます。

chmod -c 600 own.txtの実行結果
mode of 'own.txt' changed from 0644 (rw-r--r--) to 0600 (rw-------)

chownとchgrp: 所有者とグループを変える

所有者とグループはchownで変えます。書式は所有者:グループです。

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 root own.txtを実行した結果
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なら次のようになります。

umask 022のときの計算
ファイル:       0666 & ~022 = 0644 (rw-r--r--)
ディレクトリ:   0777 & ~022 = 0755 (rwxr-xr-x)

umaskの値を変えて、実際に作られるモードを確かめます。

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)")
done
実行結果
umask 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
umask -S
実行結果
0022
u=rwx,g=rx,o=rx

umaskはプロセスの属性で、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進実行ファイルでの意味ディレクトリでの意味
setuid4000実行したプロセスの実効ユーザーIDが、ファイルの所有者になる(inode(7)には記載なし)
setgid2000実行したプロセスの実効グループIDが、ファイルのグループになる中に作られるファイルが、ディレクトリのグループを引き継ぐ
sticky1000(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/shared
ls -ld /srv/sharedの出力例
drwxrwsr-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です。

/tmpのモードを確認する
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になると書かれています。

途中のディレクトリのxを外す
mkdir -p a/b/c
echo hi > a/b/c/f.txt
chmod 600 a/b
cat a/b/c/f.txt
実行結果
cat: a/b/c/f.txt: Permission denied

f.txtは644、cは755ですが、途中のa/bにxがないのでたどり着けません。エラーメッセージには末尾のファイル名しか出ないので、ファイルだけを見ていると原因が分かりません。ホームディレクトリが700や750のときに、その下に置いたファイルをWebサーバーが読めない、というのが典型的な例です。

ACL

ACL(アクセス制御リスト)は、所有者・グループ・その他の3区分に加えて、特定のユーザーやグループごとに権限を設定できる仕組みです。ACLが付いているファイルは、ls -lでモードの直後に+が表示されます。POSIXのlsの仕様では、この位置は「追加のアクセス制御方式があるときに印字可能な1文字を表示する」場所で、方式が1つだけの実装には+を使うことが推奨されています。

ACLが付いたファイルのls -lの出力例
-rw-rw-r--+ 1 alice staff 1024 Nov 12 09:00 report.txt

中身はgetfacl(1)で確認し、setfacl(1)で設定します。

ACLの確認と設定
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を設定する
getfacl report.txtの出力例
# 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/file

OPTIONSの列に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を外す
lsattr fileの出力例(iが付いている)
----i---------e------- file

この場合のエラーは、unlink(2)やchown(2)の説明にある通りEPERMなので、メッセージはOperation not permittedになります。

SELinuxとAppArmor

ここまでの仕組みは、所有者が自分の判断で権限を決められる「任意アクセス制御(DAC)」です。これとは別に、システム管理者が決めたポリシーをカーネルが強制する「強制アクセス制御(MAC)」があり、LinuxではSELinuxとAppArmorが代表的です。

MACは、パーミッションで許可された操作をさらに絞り込みます。rootで動いていても、パーミッションが777でも、ポリシーで許可されていなければ拒否されます。Webサーバーが、決められた場所以外に置かれたファイルを読めない、といった形で現れます。

詳しい設定はこの記事の範囲を超えるので、確認の入口だけ挙げます。

MACが関係しているかを確認する入口
getenforce            # SELinuxのモード(Enforcing / Permissive / Disabled)
ls -Z file            # SELinuxのコンテキストを表示する
sudo aa-status        # AppArmorのプロファイルの状態
sudo dmesg | tail     # 拒否のログが出ていないか

拒否された記録は、監査ログやカーネルのログに残ります。原因がMACだと分かった場合は、無効化するのではなく、ポリシーやファイルのラベルを直すのが本来の対応です。

Permission deniedの切り分け手順

実際に調べるときは、次の順番で確認すると抜けがありません。

  1. 誰として実行しているか: idでユーザーと所属グループを確認する。サービスなら、設定ファイルの実行ユーザーを確認する
  2. 対象のファイルやディレクトリ: ls -ldかstatでモード・所有者・グループを見る
  3. パスの途中: namei -lで、ルートから対象までのすべてのディレクトリを見る
  4. ACL: ls -lに+があればgetfaclで見る
  5. マウントオプションと属性: findmnt -Tとlsattrで見る
  6. MAC: SELinuxやAppArmorのログを見る

statで1つのファイルを詳しく見る

ls -ldの-dは、ディレクトリを指定したときに中身ではなくディレクトリ自体を表示するオプションです。statは、モードを8進と文字の両方で表示してくれます。

statでモードと所有者を確認する
stat own.txt
stat -c '%A %a %U:%G %n' own.txt
実行結果(GNU coreutils 9.9のstat。ユーザー名はaliceに置き換えています)
  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.html
出力例
f: /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がない最初のディレクトリが原因です。

対象のユーザーになって試す

設定を直したら、実際にそのユーザーとしてアクセスできるかを確かめます。

www-dataとして読めるかを確認する
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にして試すと、次のメッセージが出ます。

秘密鍵のモードを644にして読み込ませる
chmod 644 k
ssh-keygen -y -f k
実行結果(macOS 27.2のOpenSSH 10.5p1)
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         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 permissions

ssh-keygenで作った直後の鍵は、秘密鍵が600、公開鍵が644でした。鍵を別のマシンからコピーしたり、アーカイブから展開したりするとモードが変わることがあるので、次のように揃えます。

~/.sshのパーミッションを揃える
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.sh
実行結果(bash 5.3.9)
bash: ./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のようにかえって拒否されるものもある

参考リンク

パイプとリダイレクトの仕組み - 2>&1の意味をファイルディスクリプタから理解する

パイプとリダイレクトの仕組み - 2>&1の意味をファイルディスクリプタから理解する

約46分

2>&1は「標準エラー出力を、今の標準出力の行き先に複製する」という意味です。ファイルディスクリプタとopen file descriptionの関係、シェルがforkとexecの間でopenとdup2を行う流れ、> file 2>&1と2>&1 > fileの結果が違う理由を段階ごとの表で説明します。パイプの容量やSIGPIPE、PIPESTATUSとpipefail、stdioのバッファリングで出力順が入れ替わる現象、sudo echoやsort file > fileの失敗、bashとzshとPOSIXの違いまで、実行結果付きで整理します。

ファイルシステムの仕組み 徹底解説 - inode・ジャーナリング・ページキャッシュ

ファイルシステムの仕組み 徹底解説 - inode・ジャーナリング・ページキャッシュ

約63分

ファイルを開いて書き込んで閉じるまでにカーネルとディスクで何が起きているのかを、VFS・inode・ディレクトリエントリ・エクステント・ページキャッシュ・ext4のジャーナリング・CoWファイルシステムまで順に追います。man7.orgとkernel.orgの一次情報に加え、実機のext4とmacOSで測った出力例を添えて解説します。

jq の使い方入門 - JSON をコマンドラインで整形・抽出・変換する基本とハマりどころ

jq の使い方入門 - JSON をコマンドラインで整形・抽出・変換する基本とハマりどころ

約59分

jqはJSONを読み込み、フィルタで加工した結果を出力するコマンドです。整形の.、フィールド参照と.[]、パイプとカンマ、select・map・to_entries・group_by、--argと--argjson、-r・-c・-s・-n・-e、@csvや@base64、文字列補間、//と|=とreduceまでを、jq 1.8.1の実行結果付きで説明します。ファイルの上書きで中身が空になる失敗、NDJSONと--stream、終了ステータス、数値の精度、gojqやjaqなどの代替ツールも扱います。

楽天市場のおすすめPR / 楽天アフィリエイト