Cloudflare Pages / fictional postmortem

世界が書き込める本番サーバーを作った日 —— sudo chmod -R 777 /

この記事には、Linux システム全体を破壊し得るコマンドが登場します。手元の端末でも試さないでください。検証したい場合も、使い捨ての隔離環境と復旧手段を用意してください。

14時07分、端末には、ちゃんと / と表示されていました。

sudo chmod -R 777 /

私はそれを見て、/srv/example/shared/uploads だと思い込み、Enter を押しました。

この記事は「本番環境などでやらかしちゃった人 Advent Calendar 2025」にタイトルだけ登録され、公開されなかった sudo chmod -R 777 / から着想を得た、架空の事故対応記録です。

登録した方や特定の組織の体験を推測・再現したものではありません。以下は架空の担当者による一人称で進みます。時刻、システム構成、被害時間、復旧経過は創作ですが、コマンドの挙動と対策は公開資料に基づいています。

過去の「やらかし」記事にならい、笑えるところは笑いつつも、最後は次の2点まで持って帰れるように書きます。

TL;DR

14時02分、ただのリリースだった

舞台は、長く働いている1台の Linux サーバーです。

Web アプリ、バッチ、画像アップロード、管理画面。創業期には身軽だった構成も、年月とともに「あのサーバーを再起動するなら全員に声をかけて」に育っていました。Infrastructure as Code は次期基盤から導入予定。この言葉は、古い基盤にはだいたい「今回は入らない」という意味です。

その日のリリース内容は小さなものでした。画像アップロード機能の保存先を変更するだけ。デプロイ後の疎通確認で、管理画面に赤いエラーが出ました。

Permission denied: /srv/example/shared/uploads/2025/12

アプリは動いている。閲覧もできる。書き込みだけが失敗する。

こういうとき、人類は2つの派閥に分かれます。

  1. 実行ユーザー、所有者、グループ、umask、親ディレクトリ、ACL を順番に調べる派
  2. 777 にして通ったら権限が原因と分かる派

その日の私は、判断が速いほうでした。

速すぎました。

14時07分、3つの7がそろった

手順書には、リリース先の一部を一時的に書き込み可能にする古いコマンドが残っていました。実際のコマンドは変数を使う形でした。

TARGET="${APP_ROOT}/${WRITABLE_DIR}"
sudo chmod -R 777 "$TARGET"

この日、新しいデプロイ方式へ移した際に、APP_ROOTWRITABLE_DIR はどちらも設定されていませんでした。

シェルは空気を読みません。

"" + "/" + "" = "/"

本番サーバーも空気を読みません。

画面に表示された実行予定のコマンドは、これでした。

sudo chmod -R 777 /

私は見ました。

見て、実行しました。

「見落とした」なら、まだ言い訳ができたかもしれません。見たうえで、頭の中では /srv/example/shared/uploads と読んでいました。人間の脳には、差分レビューの自動補完機能があります。精度はあまりよくありません。

Enter キーはいつもと同じ深さでした。

14時07分30秒、サーバーが雄弁になる

すぐに大量のメッセージが流れ始めました。

chmod: changing permissions of '/proc/...': Operation not permitted
chmod: changing permissions of '/sys/...': Read-only file system
...

一瞬、「エラーになって止まった」と思いました。

止まっていません。

chmod は、変更できないファイルでエラーを出しても、変更できる次のファイルへ進みます。目立つ赤信号だけが失敗し、普通のファイルは無言で成功していました。端末に見えていたエラーは防波堤ではなく、浸水していない数軒からの報告でした。

そして -R は再帰です。/ の下を順番に歩きます。別のファイルシステムが /mnt などにマウントされていても、それはパスの途中にある普通のディレクトリとして到達し得ます。

慌てて Ctrl+C を押しました。

プロンプトは戻りました。

心拍数は戻りませんでした。

14時08分、「動いた」が最悪の知らせになる

恐る恐る管理画面から画像をアップロードすると、成功しました。

権限エラーは直ったのです。

家の鍵をなくしたので、玄関と窓と壁を全部取り外したような直り方でした。

直後、監視からアラートが上がり始めます。

既に動いているプロセスの一部は平然とリクエストを返していました。プロセスは起動時に設定やライブラリを読み込み、その後はメモリ上で働けるからです。

ロードバランサーから見ると正常。しかし、新しくログインできず、再起動すれば戻ってこないかもしれない。サービスは動いているのに、運用する手段だけが壊れていました。

何が壊れたのか

777 は「強い権限」ではありません。所有者、グループ、その他の全員に、読み取り・書き込み・実行を与える指定です。

対象 7 の意味
1桁目 所有者 read + write + execute
2桁目 グループ read + write + execute
3桁目 その他全員 read + write + execute

ディレクトリの write と execute が全員にあれば、その中の名前を作成・削除・置換できる場合があります。設定ファイルが読めるだけでなく、改ざんできる。実行ファイルも置き換えられる。当該ホスト上の変更可能なログも、証拠として信用できなくなります。

一方で、777 を指定しても、システムが「何でもできるようになる」わけではありません。通常ファイルでは set-user-ID と set-group-ID のような特別なビットが指定から漏れて消えます。GNU chmod のディレクトリには set-user-ID と set-group-ID を保存する規則がある一方、sticky bit は消えます。いずれにせよ、パスごとに異なっていた期待状態は失われます。

sudo が動かない

sudo は通常、一般ユーザーが実行しても、所有者である root の権限で動き始めるための set-user-ID ビットを持ちます。通常のファイルに数値モード 777 を設定すると、その特別なビットは指定に含まれないため失われます。

既に root 権限で走り始めた今回の chmod は最後まで root のままです。しかし、次に実行する sudo は、もう同じ梯子を持っていません。

自分が登ったあと、梯子を燃やしながら屋根を修理していたことになります。

SSH が入れてくれない

OpenSSH の sshd は、ユーザーのホームディレクトリ、.sshauthorized_keys などが他人から書き換え可能だと、既定の StrictModes yes により公開鍵認証で使うことを拒否します。

これは障害ではなく、正常な防御です。誰でも公開鍵を差し替えられる家に、鍵の本人確認を任せるわけにはいきません。

「chmodをもう一度」で元に戻らない

一番つらい点です。

元は 600 だったのか、640 だったのか、755 だったのか。setuid、setgid、sticky bit は付いていたのか。ディレクトリごとに誰が書けるべきだったのか。chmod は変更前の値を記録しません。

全部を 755 にすればよい、という話でもありません。

一括で 644 にすれば、今度はディレクトリをたどれず、実行ファイルも起動できません。

パッケージ管理システムが知っているファイルなら、RPM 系でパッケージ既定値を戻すなど、復旧の手掛かりはあります。しかし、それで戻るのはパッケージが管理する範囲だけです。アプリが作ったファイル、ユーザーデータ、秘密情報、マウント先、個別調整、アクセス ACL まで、昨日の状態へ魔法のようには戻りません。

chmod には undo がありません。必要なのは別の chmod ではなく、正しい状態を知っている別の情報源です。

14時12分、二次災害のコマンドを飲み込む

頭に浮かんだのは、これでした。

全部755にすれば、とりあえず安全になるのでは?

ならない。

この瞬間に一番役立ったのは Linux の知識ではなく、「事故中の作業者は、自分の事故を自分で大きくしがち」という、過去の障害訓練で聞いた一文でした。

私はコマンドを打たず、隣の席のメンバーへ言いました。

すみません。本番で / に再帰 chmod を実行しました。止めましたが、影響範囲は不明です。私をキーボードから離してください。

伝えたくない情報ほど、最初に伝える。

この申告によって、個人の失敗はチームのインシデントになりました。ここからようやく復旧が始まります。

復旧でやったこと

1. 生きているセッションを閉じなかった

既存の SSH セッションは、残っている唯一の接点かもしれません。反射的なログアウトと再起動を禁止しました。

ただし、sudo chmod を中断して戻ったシェルは、元の非特権ユーザーのままです。root なのは子プロセスだった chmod だけで、プロンプトに管理者権限が残るわけではありません。再起動やログアウトより先に、ハイパーバイザーのコンソールやレスキュー環境など、帯域外の管理経路を確保しました。

同時に、確保した経路で思いつきの修復を続けることも禁止しました。「触れる」と「触ってよい」は別です。

2. サーバーをサービスとネットワークから隔離した

帯域外の管理経路を確保したうえでロードバランサーから切り離し、セキュリティグループとファイアウォールで、調査に必要な通信を除く受信・送信・内部ネットワーク間の経路を制限しました。

777 だけで外部の攻撃者にログイン権限が生まれるわけではありません。しかし、既にホスト上で動くサービスやローカルユーザーには、通常のアクセス制御上、秘密情報の読み取りや実行ファイルの改ざん機会が生じます。SELinux や AppArmor など別の制御が阻止する場合はあっても、悪用を否定できません。このため、可用性だけの事故ではなく、侵害された可能性があるホストとして扱いました。

正しいパーミッションを戻せたとしても、「変更されなかった」という証明にはなりません。

3. ディスクを保全し、修理ではなく再構築を選んだ

ディスクのスナップショットを初期の調査資料として取得しました。通常のスナップショットだけで、揮発性情報、完全な整合性、取得後の証拠管理まで保証できるわけではありません。必要な証跡の取得と記録はインシデント対応の手順に従い、復旧用とは別に扱いました。そのうえで、クリーンなイメージから新しいサーバーを起動しました。

古いサーバーと正常な同世代サーバー、パッケージ情報、構成管理リポジトリを比較すれば、かなりのファイルを直せたかもしれません。しかし「たぶん全部直った本番サーバー」を再びサービスへ戻すより、「由来を説明できる新しいサーバー」を作るほうが安全でした。

データ領域はバックアップとトランザクションログから復元し、アップロードファイルは内容を検査して移送。アプリケーションは成果物から再デプロイしました。

4. 認証情報をローテーションした

そのホストから読めた可能性のあるものを洗い出しました。

「実際に盗まれた形跡がない」は、ローテーションしない理由にはしませんでした。パーミッション変更後のホストでは、そのホスト上にある変更可能なログの完全性も疑う必要があるからです。中央集約したログなど、ホスト外の証跡は別途確認しました。

この架空のケースでは、ユーザー影響は画像アップロードを含む一部機能の停止が41分。データ消失はありませんでした。

惨劇はなぜ起こったのか

ポストモーテムで「作業者が確認を怠った」と書くのは簡単です。そして、ほとんど役に立ちません。

確認を怠る人間を別の人間へ交換しても、いつか別の形で同じ条件がそろうからです。

直接原因

空の2変数を / で結合し、対象パスがルートディレクトリになりました。その値を検証せず、管理者権限で再帰的に処理しました。

背景要因

  1. 空変数を異常として扱っていなかった シェルは未設定でも処理を続け、妥当な文字列 / を作りました。

  2. 危険な操作にガードがなかった 対象が空、/、想定ディレクトリ外でも実行できました。GNU chmod-R / に対して既定では止まりません。

  3. アプリの権限設計を運用コマンドで補っていた 本来は所有者・グループ・限定的なモードを設計すべきところを、「動かなければ777」で先送りしていました。

  4. 本番の root 権限が日常の道具だった 調査と変更の境界がなく、1回の Enter にホスト全体を変更できる権限がありました。

  5. 古い手順を自動化と呼んでいた 変数を使うだけでは自動化ではありません。入力検証、失敗時停止、冪等性、変更対象の可視化まであって初めて、安全性を上げる自動化になります。

なぜ「777」だったのか

Permission denied は原因ではなく結果です。

それでも 777 が好まれるのは、所有者もグループもプロセスの実行ユーザーも調べずに、目の前のエラーだけを高確率で消せるからです。問題を解決するのではなく、アクセス制御を問題ごと撤去しているのですが、成功体験だけはすぐ得られます。

777は技術的負債の消費者金融です。審査が速い。

二度と惨劇を起こさないために変えたこと

1. パスを信用せず、許可リストで検証する

危険な処理の直前で、対象が具体的な許可範囲に入ることを確認します。空と / だけを拒否するのでは不十分です。/etc/srv も、ルートではありませんが十分に壊せます。

set -euo pipefail

: "${APP_ROOT:?APP_ROOT is required}"
: "${WRITABLE_DIR:?WRITABLE_DIR is required}"

target="$(realpath -- "${APP_ROOT}/${WRITABLE_DIR}")"

case "$target" in
  /srv/example/shared/uploads|/srv/example/shared/uploads/*) ;;
  *)
    printf 'Refusing unsafe target: %s\n' "$target" >&2
    exit 1
    ;;
esac

printf 'Target: %s\n' "$target"

これはあくまで考え方の例です。実運用では、途中にあるシンボリックリンク、マウントポイント、競合する変更、実行ユーザーも考慮する必要があります。

2. 777 を手順からなくす

必要なのがアプリユーザーからの書き込みなら、所有者または専用グループを正しく設定し、必要な主体だけへ最小限の権限を与えます。

ディレクトリと通常ファイルでは execute の意味も違います。一律の再帰モード変更ではなく、デプロイ時に作成される各パスの期待状態を宣言し、構成管理で収束させます。

3. --preserve-root を付ける

GNU coreutils の chmod には、/ に対する再帰操作を拒否する --preserve-root があります。既定は反対の --no-preserve-root です。

防げる事故は限定的です。/var/srv を壊す操作までは止めません。対象ツリー内のマウントポイントを越える処理も防がず、GNU chmod 自体には --one-file-system 相当の指定もありません。再帰処理の前にはマウント構成を確認し、可能なら対象を明示的に列挙する必要があります。

それでも、最後の安全網は1枚でも多いほうがよいので、運用スクリプトでは明示するようにしました。

4. 本番変更をシェル履歴から追い出す

権限、所有者、ディレクトリ作成をデプロイ定義へ移しました。変更内容はレビューされ、CI で検証され、まずカナリアへ適用されます。

緊急時にシェルを使う場合も、次を必須にしました。

5. root を「便利なログイン先」から「例外」に戻す

通常の調査は非特権ユーザーで行い、sudo は役割ごとに必要な操作へ限定しました。万能な権限を配って注意力へ期待するのではなく、間違えても被害範囲が狭い権限を配ります。

6. バックアップに「メタデータ」と「復元試験」を含める

ファイルの中身だけでなく、期待する所有者、モード、ACL、ラベルなどを復元できるか確認します。

そして最も重要なのは、バックアップがあることではなく、クリーンな環境へ戻す手順が実際に動くことです。今回すぐ再構築を選べたのは、前月の復旧訓練だけが理由でした。訓練していなければ、おそらく本番サーバー上で chmod ガチャを続けていました。

事故のあとで分かったこと

私は長いあいだ、この事故を「スラッシュを間違えた話」だと思っていました。

違いました。

スラッシュは正しく生成されていました。シェルも chmod も、指定どおり正確に働きました。間違っていたのは、危険な値が生成されても、危険な権限で、危険な範囲へ、そのまま届く設計です。

人間が最後の防波堤だったのではありません。

防波堤が人間1人しかありませんでした。

おわりに

Linux のパーミッションで 7 は、read、write、execute を足した最大値です。

3つ並ぶと縁起がよさそうに見えますが、スロットで777が出たときに増えるのは権限であって、残高ではありません。

Permission denied に出会ったら、それはシステムが意地悪をしているのではなく、境界線がまだ働いているという知らせです。境界線を消す前に、誰が、何へ、なぜアクセスすべきなのかを調べましょう。

そして、もし本番で取り返しのつかない Enter を押したら、最初の仕事は天才的な修復コマンドを思いつくことではありません。

手を止めて、事実を伝えて、チームの事故にすることです。

障害は1人で起こせます。

復旧まで1人でやる必要はありません。

参考資料