1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

監視スクリプトに root を1コマンドだけ渡す ― visudoで壊さないsudoers設計

1
Posted at

前回、「孤児Chrome」だけを見分けて刈る話を書きました。今回はその隣の監視スクリプト、mem-hog-guard.sh検知はできるのにkillする権限がなくて通知止まりだった話と、その権限を「壊れたら詰む」形ではなく検証込みで渡した設計の話です。

困りごと:検知はできるのに手が出せない

mem-hog-guard.shの冒頭コメントに、事の発端が書いてあります。

# なぜ要るか(2026-08-09):
# macOSの dasd(Duet Activity Scheduler) が RSS 264MB のまま MEM 47GB / CMPRS 46GB を
# 抱え、swapを37GBまで押し上げて Chrome の launchPersistentContext が180秒で
# タイムアウト -> SNS全レーンが1日分まるごと0件で失敗した。稼働21時間で発生。
# RSS上位監視には一度も映らないため、誰も気づけなかった。

dasdはmacOSの標準デーモンで、本来は落ちてもlaunchdが即座に再起動してくれる安全な対象です。だから検知ロジックはAUTO_RESTART_NAMES="dasd" / AUTO_RESTART_THRESHOLD_MB=5120(5GB)としてちゃんと組んでありました。問題は、検知した後の一手でした。

if in_list "$command" "$AUTO_RESTART_NAMES" && [ "$mem_mb" -ge "$AUTO_RESTART_THRESHOLD_MB" ] && ! cooldown_active "$command"; then
  ...
  else
    # dasd 等はroot所有。NOPASSWDルールが無い環境では -n が失敗するので通知に落とす。
    cooldown_mark "$command"
    if sudo -n /usr/bin/killall "$command" 2>/dev/null; then
      log "restarted $command pid=$pid mem=$mem cmprs=$cmprs"
      findings="${findings}${msg}"$'\n'
      restarted=$((restarted + 1))
    else
      log "restart-failed(no-sudo) $command pid=$pid mem=$mem"
      findings="${findings}⚠️ ${msg} — sudo権限が無く再起動できず。手動: sudo killall ${command}"$'\n'
    fi
  fi

dasdはroot所有なので、sudo -n(パスワード入力なしのsudo)でしか無人ジョブからは殺せません。NOPASSWDルールを設定していなかったのでsudo -nは毎回失敗し、ログにはrestart-failed(no-sudo)が積み上がり、通知には「⚠️ 手動: sudo killall dasd」が延々と出続けるだけでした。

さらに地味に効いていたのが、cooldown_mark "$command"sudoの成否を確認する前に呼ばれている点です。つまりNOPASSWDが無い間は、検知して失敗するたびに30分(COOLDOWN_SEC=1800)のクールダウンだけがきっちり立ち、dasdがそのままさらに膨らんでも次の巡回では黙って再検知をスキップしていました。検知ロジックは正しいのに、実行力がないせいで「静かに見送り続ける」状態に落ちていたわけです。

設計:3段構えで最小スコープのrootだけ渡す

これを直すのがenable-dasd-autorestart.shです。単にvisudoでNOPASSWDを足すだけでも動きますが、それだと「打ち間違えたらsudo自体が死ぬ」「入れたつもりが実は効いていない」の2つの事故を踏みます。なのでこのスクリプトは3段構えにしてあります。

1. 渡すルールをコマンド1本・引数まで固定する

RULE='matsubara ALL=(root) NOPASSWD: /usr/bin/killall dasd'

NOPASSWD: /usr/bin/killallではなく、/usr/bin/killall dasdまで書いています。sudoersはコマンド行を引数まで含めて照合するので、この1行が許可するのは文字通り「dasdという名前のプロセスをkillallすること」だけです。ALLにせず、フルパスにせず、任意プロセスも渡さない。無人スクリプトに与えるrootは「壊れたら被害が全部」ではなく「この1コマンドだけ」に絞っています。

2. 設置前にvisudo -cfで文法検証する

# 壊れたsudoersを入れるとsudo自体が死ぬので、必ず検証してから設置する
visudo -cf "$TMP"
install -m 440 -o root -g wheel "$TMP" /etc/sudoers.d/lily-dasd

/etc/sudoers.d/配下は文法が壊れているファイルが1個でもあると、それだけでsudoコマンド自体が全体で動かなくなります。一度壊すとroot権限が要る作業(sudoersの修正含む)が全滅するので、一時ファイルに書いた内容をvisudo -cfで文法チェックしてからinstall -m 440 -o root -g wheelで本設置しています。パーミッションも440(root所有・読み取り専用)にして、後から誰かがviで直接編集して壊すルートも塞いでいます。実際に設置されたファイルも、この通りの権限で入っています。

$ ls -la /etc/sudoers.d/lily-dasd
.r--r----- 53 root 12 Aug 13:47 /etc/sudoers.d/lily-dasd

3. 設置直後に「本当に効くか」を実行前に検証する

ここが一番肝心なところで、ルールを設置しただけで「完了」と言っていません。

# 設置後の検証(matsubaraとしてNOPASSWDが効くか)
sudo -u matsubara sudo -n -l /usr/bin/killall dasd >/dev/null 2>&1 \
  && echo "[2/3] NOPASSWD 検証OK" \
  || { echo "[2/3] 🔴NOPASSWD検証NG"; exit 1; }

sudo -n -lはパスワードなしで「このコマンドを実行する権限があるか」だけを問い合わせるモードです。ここで失敗したら即exit 1して、設置したルールが実際には効いていないことをその場で検出します。visudoの文法チェックは「壊れていないか」しか見ておらず、「意図通り効くか」は別に確かめないと分かりません。

検証が通ったところで、ようやく初回のkillallを実行し、効果を実測します。

echo "[3/3] dasdを今すぐ回収(launchdが即座に再起動する)"
BEFORE="$(vm_stat | awk '/occupied by compressor/{gsub(/\./,"");printf "%.1f", $5*16384/1073741824}')"
killall dasd || true
sleep 5
AFTER="$(vm_stat | awk '/occupied by compressor/{gsub(/\./,"");printf "%.1f", $5*16384/1073741824}')"
echo "圧縮メモリ: ${BEFORE}GB -> ${AFTER}GB"
echo "swap: $(sysctl -n vm.swapusage)"

vm_statoccupied by compressor(ページ数)をGBに換算してbefore/afterを並べているので、「殺したつもり」ではなく圧縮メモリが実際に落ちたかどうかがその場のログに残ります。これで以降はmem-hog-guard.sh側のsudo -n /usr/bin/killall dasdが無人のまま成功するようになり、restart-failed(no-sudo)だった行はrestarted $commandに変わります。

NOPASSWDを設計するときの分岐点は「ALLにするか、1コマンドに絞るか」です。ALLにすれば設定は一瞬で終わりますが、無人スクリプトがroot権限で何でもできる状態になります。ここでは検知ロジックが最初から「dasdという名前が、閾値を超えたときだけ」としか判定しないので、渡す権限もそれに合わせて/usr/bin/killall dasdの1本だけに絞りました。監視スクリプトの判定範囲と、渡すsudo権限の範囲を一致させるのがポイントです。

踏んだ落とし穴

  • cooldown_markがsudoの成否より先に立つ → NOPASSWD未設置の間、検知に失敗するたびに30分のクールダウンだけが静かに積み上がり、dasdが膨らみ続けても次の巡回では黙ってスキップされていた
  • visudo -cfは文法チェックであって効力チェックではない → 文法が正しくても、ユーザー名や対象コマンドのタイポでルールが一致しないことがある。設置直後にsudo -n -lで実効性を確認しないと「入れたつもり」で終わる
  • NOPASSWD: /usr/bin/killallだけだと対象が広すぎる → 引数のdasdまで書いてsudoersの照合対象を1コマンドに固定する。ALLはもちろん、コマンド名だけの許可でも任意プロセスをroot権限でkillできてしまう
  • /etc/sudoers.d/配下は1ファイルの文法崩壊が全体に波及する → 一時ファイルへ書いてからvisudo -cfを通し、検証済みのものだけinstallで本設置する。直接編集はしない

まとめ

  • 監視ロジック(mem-hog-guard.sh)は最初から正しく動いていたが、実行力(sudo -n)が無いために「検知して通知するだけ」に縮退していた
  • 権限を渡すときはコマンドと引数まで絞ったNOPASSWDルールにし、ALLや広い許可を避ける
  • 設置はvisudo -cfで文法検証 → install -m 440で本設置 → sudo -n -lで実効性検証 → 初回実行してbefore/afterを実測の順で、どの段階も「動くはず」で止めない
  • 無人スクリプトにrootを渡すのは、検知ロジックが判定する範囲と権限のスコープを一致させてから

次は、mem-hog-guard.shのコメントに残っていたクールダウンがプロセス単位ではなく全体で1つだったせいで、dasdを直した30分の間だけ別プロセスが野放しになっていた話を書く予定です。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?