こうした権限設計の悩みは、シェルスクリプトを実運用する上で避けては通れません。
この記事では、RHEL 9系/Ubuntu 24.04 LTSを想定環境として、root権限が必要なシェルスクリプトのスクリプト側の設計に絞って解説します。EUIDによるroot判定・sudoで自己再実行するパターン・必要な処理だけを昇格させる分割設計・sudoersで特定スクリプトだけを許可する最小権限設定・sudo実行時のPATH対処まで、具体的なコード例を交えてまとめます。
この記事のポイント
・$EUIDが0かどうかでroot実行かを一行で判定できる
・sudo自己再実行はreadlink -fで絶対パスを取得する形が安全
・sudoersは絶対パス・root所有・引数ワイルドカード禁止が鉄則
・sudo後のPATHはsecure_pathになるのでスクリプト内で明示設定する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
シェルスクリプトにroot権限が必要なときの典型的な設計ミス
root権限が必要なシェルスクリプトを設計するとき、次の3つのミスをよく見かけます。ミス1: スクリプト全体をsudoで実行する
「どうせroot権限が必要なのだから、最初からsudoで実行すればいい」という発想です。しかしこの方法では、ファイル読み書きや文字列処理など、権限昇格が不要な処理まですべてrootで動かすことになります。スクリプトの一部にバグやコマンドインジェクションがあった場合の影響範囲が大きくなります。
ミス2: 利用者が書き換えられる場所にスクリプトを置いてsudoersに登録する
ホームディレクトリや/tmpのような、利用者が書き込めるパスにスクリプトを置いてsudoersに登録するのは非常に危険です。利用者はスクリプトの内容を好きに書き換えられるため、sudoを通じてroot権限を自由に取得できてしまいます。
ミス3: sudo実行時の環境変数変化を無視する
sudoはデフォルトで環境変数をリセットし、PATHも専用の値(secure_path)に差し替えます。一般ユーザーの環境で動いていたスクリプトが、sudoで実行すると「command not found」になるのはこれが原因です。
権限設計を含むシェルスクリプトの実装全般を体系的に学びたい方は、シェルスクリプト実践講座も参考にしてください。以降の節では、上記3つのミスを避ける具体的な設計を解説します。
EUIDでroot実行を判定するシェルスクリプトの書き方
root権限が必要なスクリプトには、「実行時に権限があるかを確認して、なければ早めにエラー終了する」か「自動的にsudoで再実行する」かのどちらかの仕組みを入れておきます。1. $EUIDと$(id -u)の使い分け
root実行かどうかを判定するには、有効ユーザーID(EUID)が0かどうかを確認します。# bash専用: $EUIDを直接参照できる(#!/bin/bashのスクリプト向け) if [ "$EUID" -ne 0 ]; then echo "このスクリプトはroot権限で実行してください" >&2 exit 1 fi # POSIX sh互換にする場合はid -uを使う(#!/bin/sh でも動作する) if [ "$(id -u)" -ne 0 ]; then echo "このスクリプトはroot権限で実行してください" >&2 exit 1 fi
$EUIDはbash固有の変数で、シェバン行が#!/bin/bashのスクリプトでのみ使えます。$(id -u)はPOSIX準拠で、shがdashやkshの環境でも動きます。移植性が必要ない場合は$EUIDの方がサブシェルを起動しない分わずかに軽量です。2. root以外で実行されたときにエラー終了する
操作が取り返しのつかない処理(ファイル削除・パーティション操作など)では、静かに失敗するよりも早期終了の方が安全です。#!/bin/bash set -euo pipefail if [ "$EUID" -ne 0 ]; then echo "エラー: root権限が必要です。sudo で実行してください。" >&2 echo "使い方: sudo $0" >&2 exit 1 fi # --- root権限が確認できた後の処理 --- echo "バックアップを開始します..."
exit 1で終了することで、呼び出し元のスクリプトやcronがエラーを検知できます。3. sudoで自己再実行するパターン
「一般ユーザーでも起動でき、権限が必要な部分だけ自動で昇格させたい」場合は、自己再実行のパターンを使います。#!/bin/bash set -euo pipefail if [ "$EUID" -ne 0 ]; then echo "root権限が必要なため sudo で再実行します..." exec sudo -- "$(readlink -f -- "$0")" "$@" # execが成功した場合、以降の行は実行されない fi # ここに到達するのはroot権限がある場合のみ echo "root権限で実行中 (EUID=$EUID)"
・exec:現在のプロセスをsudoで置き換えます。execを使わないと、sudoから戻った後も元のシェルが動き続けます
・readlink -f:シンボリックリンクを解決した絶対パスを取得します。相対パスのまま渡すとsudoersのエントリと一致しないことがあります
・--:sudoのオプション解析終了を示します。スクリプトの引数にハイフン始まりの値が来ても誤解釈されません
・"$@":元の引数をそのまま引き継ぎます
必要な処理だけをroot昇格させる分割設計
スクリプト全体をroot実行する代わりに、root権限が必要な箇所だけを昇格させます。これが「最小権限の原則」の実践です。1. sudo単独コマンドで最小昇格する
root権限が必要なコマンドが数行程度なら、その行だけにsudoを付けます。#!/bin/bash # このスクリプト自体はroot権限不要 set -euo pipefail LOGDIR="/var/log/myapp" BACKUP_DIR="/backup/myapp" DATE=$(date +%Y%m%d) echo "ログを収集します..." # rootが所有するログファイルのコピーだけsudoを使う sudo cp "${LOGDIR}/app.log" "${BACKUP_DIR}/app-${DATE}.log" sudo chown "$USER":"$USER" "${BACKUP_DIR}/app-${DATE}.log" echo "圧縮します..." # 圧縮は一般ユーザーで可能 gzip "${BACKUP_DIR}/app-${DATE}.log" echo "完了: ${BACKUP_DIR}/app-${DATE}.log.gz"
2. root処理だけを別スクリプトに切り出してsudoersに登録する
root権限が必要な処理だけをヘルパースクリプトにまとめて、そちらをsudoersに登録します。# /usr/local/sbin/backup-log-helper.sh # (root所有・sudoersに登録するスクリプト。root権限が必要な処理のみ記述) #!/bin/bash set -euo pipefail PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export PATH LOGDIR="/var/log/myapp" BACKUP_DIR="/backup/myapp" DATE=$(date +%Y%m%d) cp "${LOGDIR}/app.log" "${BACKUP_DIR}/app-${DATE}.log" chown operator:operator "${BACKUP_DIR}/app-${DATE}.log"
# /home/operator/run-backup.sh # (一般ユーザーが実行するスクリプト。root権限不要) #!/bin/bash set -euo pipefail echo "前処理(一般権限)..." # ... 前処理 ... echo "ログをバックアップ中(sudo)..." sudo /usr/local/sbin/backup-log-helper.sh echo "後処理(一般権限)..." # ... 後処理 ...
/usr/local/sbin/backup-log-helper.shだけです。ヘルパースクリプトの内容はroot権限が必要な処理に限定されているため、万が一バグがあっても影響範囲を小さくできます。sudoersで特定のシェルスクリプトだけを許可する
1. スクリプトの安全な配置(/usr/local/sbin、root所有)
sudoersに登録するスクリプトは、利用者が書き込めない場所に置くことが大前提です。推奨する配置先と権限設定の手順:
# スクリプトをroot所有の場所にコピー sudo cp backup-log-helper.sh /usr/local/sbin/backup-log-helper.sh # 所有者をroot:rootに設定 sudo chown root:root /usr/local/sbin/backup-log-helper.sh # 権限を755に設定(root以外は読み込み・実行のみ、書き込みは不可) sudo chmod 755 /usr/local/sbin/backup-log-helper.sh # 親ディレクトリの確認(一般ユーザーに書き込み権限がないこと) ls -ld /usr/local/sbin/ # 出力例(環境により異なります) # drwxr-xr-x. 2 root root 4096 Oct 20 10:00 /usr/local/sbin/
/tmpやホームディレクトリに置いたスクリプトをsudoersに登録してはいけません。親ディレクトリも一般ユーザーが書き込めない場所であることを確認します。2. visudoでの最小権限エントリの書き方
sudoersの編集は必ずvisudoコマンドで行います。visudoは保存前に構文チェックを行うため、書き間違いによる「sudoが使えなくなる」事故を防げます。/etc/sudoers.dを使う場合は、ハイフン区切りのファイル名でエントリを分けます(ピリオドを含む名前のファイルはsudoが読み込みをスキップします)。
# /etc/sudoers.d/backup-operator を新規作成する場合 sudo visudo -f /etc/sudoers.d/backup-operator
# /etc/sudoers.d/backup-operator # operatorユーザーが指定スクリプトをパスワードなしでroot実行できる operator ALL=(root) NOPASSWD: /usr/local/sbin/backup-log-helper.sh
# backupopsグループのメンバー全員に許可する場合 %backupops ALL=(root) NOPASSWD: /usr/local/sbin/backup-log-helper.sh
/usr/local/sbin/backup.sh *のように書くと、引数を通じてroot権限を悪用される危険があります。特定の引数だけを受け付ける場合は、引数をスクリプト内で厳密にチェックします。3. 許可してはいけないスクリプトの条件
以下の条件を1つでも満たすスクリプトは、sudoersに登録してはいけません。・利用者が書き込めるパスにある:スクリプトを書き換えてroot権限を奪えます
・利用者が書き込めるファイルをsourceしている:sourceされるファイルを書き換えてroot権限を奪えます
・利用者が制御できる変数でコマンドを構築している:コマンドインジェクションの経路になります
# 危険な例1: 利用者のホームディレクトリにあるスクリプト(書き換え可能) # operator ALL=(root) NOPASSWD: /home/operator/scripts/backup.sh ← NG # 危険な例2: 利用者が書き込めるファイルをsourceするスクリプト(内部) source /home/operator/config.sh # ← operatorが任意のコードを注入できる # 危険な例3: 引数を無検証でコマンドに渡す(内部) DEST="$1" cp /var/log/app.log "$DEST" # ← 悪意ある引数で任意パスへコピーできる
sudo実行時の環境変数とPATHを制御する
1. env_resetとsecure_pathの既定動作
多くのディストリビューションのsudoersには、デフォルトで次の設定が含まれています。# /etc/sudoers のデフォルト設定(代表的な設定を抜粋) Defaults env_reset Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
・secure_path:PATHを強制的に上書きします。一般ユーザーが
~/.local/binなどに追加したパスは消えますこの動作によって、「一般ユーザー環境では動くが、sudo実行時にcommand not foundになる」という問題が発生します。
2. スクリプト内でのPATH明示設定
sudoで実行するスクリプトは、冒頭でPATHを明示的に設定します。#!/bin/bash # sudo実行時でも必要なコマンドが見つかるようにPATHを明示設定する PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export PATH # secure_path外にあるコマンドはフルパスで指定する # 例: /opt/company/bin/myapp ならそのまま絶対パスで呼ぶ /opt/company/bin/myapp --config /etc/myapp/config.conf
secure_pathに含まれていないコマンドは、フルパスで記述するかPATHに追加します。どちらのアプローチが適切かは、そのコマンドの管理責任の範囲で判断します。3. 環境変数を丸ごと引き継ぐ書き方を避ける
sudo -EやSETENVを使って環境変数を丸ごと引き継ぐことは推奨しません。# 推奨しない書き方 sudo -E ./script.sh # ← 一般ユーザーの環境をすべて引き継ぐ sudo --preserve-env ./script.sh # ← 同上 # sudoersに SETENV を書くのも同様に推奨しない # operator ALL=(root) NOPASSWD,SETENV: /usr/local/sbin/backup.sh
sudo -Eを使うと、一般ユーザーがLD_LIBRARY_PATHやPYTHONPATHなどを細工した状態でroot権限のプロセスを動かせます。スクリプトに必要な環境変数は個別に引数で渡すか、スクリプト内で設定します。トラブルシュート
「sudo: command not found」が出る
スクリプト内で呼んでいるコマンドがsecure_pathのどこにも存在しない場合に起こります。# コマンドの実際の場所を確認する which aws # 出力例(環境により異なります) # /usr/local/bin/aws # sudo実行時のPATHを確認する sudo bash -c 'echo $PATH' # 出力例(環境により異なります) # /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # /usr/local/bin はsecure_pathに含まれているため上記の場合は問題なし # /opt/xyz/bin など含まれていなければスクリプト内でPATHに追加またはフルパスで指定
「sorry, user XXX is not allowed to execute ... as root」が出る
sudoersにエントリが存在しないか、スクリプトのパスがsudoersの記述と一致していない場合に出ます。# 実際に呼ばれているパス(シンボリックリンク解決後)を確認する readlink -f /usr/local/sbin/backup-log-helper.sh # 出力例(環境により異なります) # /usr/local/sbin/backup-log-helper.sh # sudoersに書かれているパスと一致しているか確認する sudo grep backup-log-helper /etc/sudoers /etc/sudoers.d/*
/etc/sudoers.d/配下のファイル名にピリオド(.)が含まれていると、そのファイルはsudoに読み込まれません。ファイル名はハイフンのみを区切り文字として使います。本記事のまとめ
sudoで動かすシェルスクリプトの権限設計をまとめます。| やりたいこと | 設計・コード例 |
|---|---|
| root実行かを判定する | [ "$EUID" -ne 0 ]または[ "$(id -u)" -ne 0 ] |
| 非root実行時にsudoで自己再実行する | exec sudo -- "$(readlink -f -- "$0")" "$@" |
| スクリプトの安全な配置先 | /usr/local/sbin/(root:root所有・chmod 755) |
| sudoersの最小権限エントリ | user ALL=(root) NOPASSWD: /usr/local/sbin/script.sh |
| sudo後のPATH対処 | スクリプト冒頭でPATH=...を明示設定 |
| sudoersに登録してはいけないスクリプト | 利用者が書き込める場所にある・sourceするファイルが書き換え可能 |
>> シェルスクリプト実践講座の詳細を見る
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:シェルスクリプト実行権限を安全に管理する|setuidを使わない配布設計
- 前のページへ:自作シェルスクリプトを社内ツールとして配布する設計|/usr/local/binへの配置とバージョン管理の決め方
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます