sudoで動かすシェルスクリプトの権限設計|root判定と自己再実行・sudoersで特定スクリプトだけ許可する方法

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > sudoで動かすシェルスクリプトの権限設計|root判定と自己再実行・sudoersで特定スクリプトだけ許可する方法
「root権限が必要な処理をシェルスクリプトにまとめたいが、スクリプト全体をsudoで実行するのはリスクが高い…」「sudoersに登録したいが、誰でも書き換えられる場所に置いて問題ないか不安…」
こうした権限設計の悩みは、シェルスクリプトを実運用する上で避けては通れません。

この記事では、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になるのでスクリプト内で明示設定する


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

シェルスクリプトに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"

ただし、この方法ではsudoのパスワード入力が処理の途中で発生します。パスワードなしで実行させたい場合は、次の「別スクリプトに切り出す」方法を使います。

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 "後処理(一般権限)..." # ... 後処理 ...

この設計では、sudoersに登録するのは/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/

/usr/local/sbin/はrootが所有し、一般ユーザーは書き込めません。/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"

・env_reset:sudo実行時に環境変数をリセットします。HOME・LOGNAME・TERM・USER以外はクリアされます
・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/*

sudoersに書くパスは実体のフルパスで記述します。また、/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するファイルが書き換え可能
EUIDによる判定・自己再実行・最小権限の分割・安全なsudoers登録の4点を押さえることで、root権限が必要なシェルスクリプトを安全に運用できます。sudoコマンド自体の基本操作やvisudoの詳細は別記事で扱っているため、本記事はスクリプト設計の観点に絞りました。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、シェルスクリプトの権限設計から安全な実装パターンまで、実務で使えるスキルをステップ式で習得できる講座を用意しています。
>> シェルスクリプト実践講座の詳細を見る

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録

運営

リナックスマスター.JP(代表 宮崎智広)

株式会社イーネットマーキュリーが運営するLinux教育サイトです。代表の宮崎智広は現役のLinuxサーバー管理者として20年以上の実務経験を持ち、累計3,100名以上のエンジニアを指導してきました。実践型のLinuxセミナーや無料マニュアルの配布を通じてLinux人材の育成に取り組んでいます。