最小権限実行とsudoの適切な運用
権限を降格し、sudoルールの範囲を厳密に限定して、危険な操作の前に実効UIDを検証します。
「最小権限実行とsudoの適切な運用」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。
シェルスクリプトで最小権限が重要な理由
自動化におけるセキュリティ侵害の多くは、特殊な攻撃によるものではなく、スクリプトが必要以上の権限で実行されることが原因です。ログファイルのローテーションだけが必要な cron ジョブを root で実行するのは、事故を招くような構成です。
最小権限の原則では、すべてのプロセスは、仕事に必要な権限だけを使って動作し、それ以上の権限は持たないようにします。Bash スクリプトでは、次のことを意味します。
- 可能な限り、権限のないユーザーとして実行する
- 必要なコマンドに限って root へ権限を昇格する
- 権限が必要な作業を終えたら、すぐに権限を落とす
- 認証情報をその適用範囲を超えて保存したり継承したりしない
このレッスンでは、sudo の適用範囲、su による権限降格、UID 検証ガード、sudoers の強化という具体的な手法を通じて、本番スクリプトのための規律ある権限モデルを構築します。
危険な操作の前に実効 UID を確認する
本当に root が必要なコードブロックの前に、スクリプトは想定した実効 UID で実行されていることを確認する必要があります。思い込まず、必ず検証してください。
$EUID は、現在のプロセスの実効ユーザー ID を保持する Bash の特殊変数です。root の EUID は常に 0 です。スクリプトの先頭、または権限が必要なブロックの前後で確認することで、誤ったユーザーとしての実行を防げます。
次のガードパターンを使用します。
#!/usr/bin/env bash
set -euo pipefail
# Guard: this script must NOT run as root.
if [[ "$EUID" -eq 0 ]]; then
echo "ERROR: Do not run this script as root. Use a normal user account." >&2
exit 1
fi
echo "Running as UID $EUID — proceeding safely."必要な場合にだけ root であることを要求する
正当に root を必要とするスクリプトもあります。その場合はガードを逆にし、root でなければ早い段階で失敗させます。これにより、スクリプトが権限の必要なシステムコールまで進んで、実行途中に分かりにくい権限エラーを出す事態を防げます。
早期終了と分かりやすい使用方法のメッセージを組み合わせると、スクリプト自体が使い方を説明するようになります。
#!/usr/bin/env bash
set -euo pipefail
require_root() {
if [[ "$EUID" -ne 0 ]]; then
echo "ERROR: $(basename "$0") must be run as root." >&2
echo " Try: sudo $(basename "$0") $*" >&2
exit 1
fi
}
require_root "$@"
echo "Root confirmed (EUID=0). Starting privileged work..."sudo を単一のコマンドに限定する
最もよくある間違いは、スクリプトの先頭に sudo を置き、その後のすべてを root として実行することです。代わりに、sudo は必要なコマンドそのものにだけ適用し、それ以外は通常のユーザーとして実行します。
これにより被害範囲を限定できます。攻撃者がスクリプトにコードを注入しても、root 権限で実行できるのは、sudo が前置された部分だけです。
以下の 2 つのパターンを比較してください。2 つ目のほうがはるかに安全です。
#!/usr/bin/env bash
set -euo pipefail
# BAD: escalate early, do everything as root (avoid this)
# sudo bash -c '
# cp config.conf /etc/app/config.conf
# chown app:app /etc/app/config.conf
# systemctl restart app
# '
# GOOD: escalate only for the commands that require it
LOCAL_CONF="./config.conf"
DEST="/etc/app/config.conf"
# Unprivileged: validate the config before touching anything as root
if ! grep -q '^[[:space:]]*\[main\]' "$LOCAL_CONF"; then
echo "ERROR: config.conf is missing [main] section" >&2
exit 1
fi
# Privileged: only these three commands run under sudo
sudo cp "$LOCAL_CONF" "$DEST"
sudo chown app:app "$DEST"
sudo systemctl restart app
echo "Config deployed and service restarted."厳格な sudoers ルールを記述する
スクリプト内で sudo somecommand を呼び出す場合、安全に動作させるには、sudoers ファイルでそのコマンドだけを許可し、それ以外は許可しないように設定する必要があります。サービスアカウントに対して ALL=(ALL) NOPASSWD: ALL のようなルールを設定するのは避けてください。
代わりに、visudo で安全に編集できる構文を使い、特定の引数を伴う特定のコマンドにルールを限定します。sudoers ルールの主なフィールド:
- User — sudo を実行できるユーザー
- Host — どのマシン上で実行できるか(移植性のために
ALLを使用します) - RunAs — どのユーザーとして実行するか(ほとんどの場合は
root) - Command — 完全な絶対パス。必要に応じてリテラルの引数も指定します
デプロイ用サービスアカウント(deployer)に対する、厳格なルールの例:
# /etc/sudoers.d/deployer (edit with: sudo visudo -f /etc/sudoers.d/deployer)
#
# Allow 'deployer' to restart exactly one service — nothing else
deployer ALL=(root) NOPASSWD: /usr/bin/systemctl restart app
# Allow copying a config file to a fixed destination only
deployer ALL=(root) NOPASSWD: /usr/bin/cp /home/deployer/staging/config.conf /etc/app/config.conf
# Allow chown of that specific file only
deployer ALL=(root) NOPASSWD: /usr/bin/chown app\:app /etc/app/config.conf
# NEVER do this — gives full root shell:
# deployer ALL=(ALL) NOPASSWD: ALLsu と runuser で権限を落とす
スクリプトが root として開始されても(root で実行されるシステムの init や cron から起動された場合など)、大部分の処理は権限のないユーザーとして行うべきなら、スクリプト全体を root で実行するのではなく、明示的に権限を落としてください。
そのためのツールは次の 2 つです。
su -s /bin/bash -c 'command' username— username としてシェルを起動し、コマンドを実行しますrunuser -u username -- command args— root 所有のスクリプト内でユーザーを切り替える場合に Linux で推奨されます。suよりもクリーンです
次のパターンは、root 所有のデプロイラッパーが、実際のアプリケーションロジックを app ユーザーに切り替えて実行する例です。
#!/usr/bin/env bash
# This script is called by systemd as root during pre-deployment
set -euo pipefail
APP_USER="app"
DEPLOY_DIR="/opt/myapp"
# Step 1: privileged — fix ownership of deploy directory
chown -R "${APP_USER}:${APP_USER}" "$DEPLOY_DIR"
# Step 2: drop to app user for the actual migration/startup logic
# runuser is available on most modern Linux systems
runuser -u "$APP_USER" -- bash -c "
cd $DEPLOY_DIR
./bin/migrate.sh
./bin/start.sh
"
echo "Deploy complete. Privileged wrapper exiting."sudo -u で別のユーザーとして単一のコマンドを実行する
常に完全なシェルセッションへ切り替える必要があるとは限りません。sudo -u username command は指定したユーザーとして単一のコマンドを実行し、その後、呼び出し元のユーザーに戻ります。サービスアカウントが所有するファイルを操作したい場合に、そのアカウントへ対話的なアクセス権を与えずに済むため便利です。
これには、まさにそのユーザーとコマンドの組み合わせだけを許可する sudoers ルールを組み合わせます。
#!/usr/bin/env bash
set -euo pipefail
# Scenario: deploy script runs as 'deployer'; DB migrations must run as 'postgres'
# sudoers entry needed:
# deployer ALL=(postgres) NOPASSWD: /opt/app/bin/run_migrations.sh
DB_MIGRATION_SCRIPT="/opt/app/bin/run_migrations.sh"
if [[ ! -x "$DB_MIGRATION_SCRIPT" ]]; then
echo "ERROR: migration script not found or not executable: $DB_MIGRATION_SCRIPT" >&2
exit 1
fi
echo "Running DB migrations as postgres user..."
sudo -u postgres "$DB_MIGRATION_SCRIPT"
echo "Migrations done. Returning to deployer context (EUID=$EUID)."環境変数を利用した権限昇格を防ぐ
見落としやすい攻撃対象の 1 つが、sudo セッションに継承される環境変数です。デフォルトでは sudo が環境をリセットしますが、env_keep や env_reset の設定を誤って上書きすると、攻撃者が制御する LD_PRELOAD、PATH、PYTHONPATH などの変数が権限のあるコマンドへ渡される可能性があります。
ベストプラクティス:
sudoで実行するスクリプトでは、必ず絶対パスを使用し、$PATHに依存しないでください- 明示的に必要な変数だけを
sudo env VAR=value /path/to/cmdのように渡します - sudoers では、
env_keep += PATHやenv_keep += LD_*を避けてください - 呼び出し元の環境を完全に制御し、信頼できる場合に限り
sudo -Eを使用します
#!/usr/bin/env bash
set -euo pipefail
# BAD: relies on $PATH — attacker who controls PATH can hijack 'cp'
# sudo cp config.conf /etc/app/
# GOOD: absolute paths for every command called under elevated context
SUDO_BIN="/usr/bin/sudo"
CP_BIN="/usr/bin/cp"
CHOWN_BIN="/usr/bin/chown"
SYSTEMCTL_BIN="/usr/bin/systemctl"
"$SUDO_BIN" "$CP_BIN" ./config.conf /etc/app/config.conf
"$SUDO_BIN" "$CHOWN_BIN" app:app /etc/app/config.conf
"$SUDO_BIN" "$SYSTEMCTL_BIN" restart app
echo "Deployed with hardened absolute-path invocations."コマンド引数の検証で sudo を厳格に制限する
sudoers ルールで特定のスクリプトを許可していても、ルールで引数まで制限しない限り、そのスクリプトには任意の引数を渡せます。よくある落とし穴は次のとおりです。
deployer ALL=(root) NOPASSWD: /opt/scripts/manage.shこれにより sudo /opt/scripts/manage.sh restart は許可されますが、sudo /opt/scripts/manage.sh --arbitrary-flag も許可されます。manage.sh が引数をそのまま権限の必要なサブコマンドへ渡す場合、問題になります。
多層防御を行い、sudoers だけでなく、権限の必要なスクリプトの内部でも引数を検証してください。
#!/usr/bin/env bash
# /opt/scripts/manage.sh — called via sudo; must validate its own args
set -euo pipefail
# Allowlist of valid actions
declare -A ALLOWED_ACTIONS=(
[restart]=1
[status]=1
[reload]=1
)
ACTION="${1:-}"
if [[ -z "$ACTION" ]]; then
echo "Usage: $(basename "$0") <restart|status|reload>" >&2
exit 1
fi
if [[ -z "${ALLOWED_ACTIONS[$ACTION]:-}" ]]; then
echo "ERROR: Unknown action '${ACTION}'. Allowed: ${!ALLOWED_ACTIONS[*]}" >&2
exit 2
fi
/usr/bin/systemctl "$ACTION" app
echo "Action '$ACTION' executed successfully."一時的な特権昇格とクリーンアップ用トラップ
スクリプトで特権付きのファイル、認証情報、またはリソースを短時間だけ保持する必要がある場合は、Bash の trap を使って、エラーやシグナルが発生してもクリーンアップが実行されるようにします。これにより、スクリプトがクラッシュした際に一時的な setuid バイナリや root 所有のソケットが残るなど、特権が漏洩する事態を防げます。
以下のパターンでは、root として一時ファイルを作成して使用し、その後削除します。この処理は EXIT に設定した trap によって必ず実行されます。
#!/usr/bin/env bash
set -euo pipefail
# Must run as root for this demo
if [[ "$EUID" -ne 0 ]]; then
echo "Run as root" >&2; exit 1
fi
TMP_SECRET=""
cleanup() {
local exit_code=$?
if [[ -n "$TMP_SECRET" && -f "$TMP_SECRET" ]]; then
# Overwrite before deletion to reduce forensic recovery risk
shred -u "$TMP_SECRET" 2>/dev/null || rm -f "$TMP_SECRET"
echo "[cleanup] Removed privileged temp file." >&2
fi
exit "$exit_code"
}
trap cleanup EXIT INT TERM
# Create a root-owned temp file for a short-lived secret
TMP_SECRET="$(mktemp /tmp/deploy_secret.XXXXXXXX)"
chmod 600 "$TMP_SECRET"
# Simulate fetching a secret into the temp file
echo "super-secret-token" > "$TMP_SECRET"
# Use the secret (e.g., pass to a sub-command via file descriptor)
/usr/bin/some-privileged-tool --key-file "$TMP_SECRET"
echo "Privileged operation complete."特権操作の監査とログ記録
すべての昇格イベントにコンテキストを付けてログ記録すると、最小権限を適用しやすくなります。つまり、誰が、いつ、何を、なぜ実行したのかを記録します。次の 2 つの層を組み合わせます。
- sudo 自体 —
/var/log/auth.log(Debian/Ubuntu)または/var/log/secure(RHEL)には、sudo の呼び出しがすべて自動的に記録されます - スクリプトレベルの監査ログ — 特権を必要とする各関数の開始時に構造化されたエントリを書き込み、意図をシステムログと併せて記録します
シンプルな構造化ログ関数を使うと、監査証跡の形式を統一でき、grep でも扱いやすくなります。
#!/usr/bin/env bash
set -euo pipefail
AUDIT_LOG="/var/log/app_deploy_audit.log"
log_privileged_action() {
local action="$1"
local reason="${2:-unspecified}"
local ts
ts="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
printf '{"ts":"%s","user":"%s","euid":%d,"action":"%s","reason":"%s"}\n' \
"$ts" "${SUDO_USER:-$USER}" "$EUID" "$action" "$reason" \
| sudo tee -a "$AUDIT_LOG" > /dev/null
}
# Each privileged step is logged before execution
log_privileged_action "cp_config" "deploy release v2.4.1"
sudo /usr/bin/cp ./config.conf /etc/app/config.conf
log_privileged_action "chown_config" "ensure app user owns config"
sudo /usr/bin/chown app:app /etc/app/config.conf
log_privileged_action "restart_service" "activate new config"
sudo /usr/bin/systemctl restart app
echo "Deployment complete. Audit entries written to $AUDIT_LOG"理解度チェック:sudo の適用範囲
Bash スクリプトで最小権限の実行を行う方法について、理解度を確認しましょう。
まとめ:最小権限の実行と sudo の適切な運用
このレッスンでは、各ステップで必要な最小限の権限で Bash スクリプトを実行するための方法を習得しました。主な原則を簡潔にまとめます。
$EUIDでガードする — root を拒否する場合でも、root を必須とする場合でも、誤ったユーザーとしてスクリプトが実行されていたら即座に失敗させますsudoの適用対象を個々のコマンドに限定する — スクリプト全体を昇格させず、本当に必要な行にだけsudoを付けます- 厳密な sudoers ルールを記述する — 完全な絶対パスとリテラル引数を指定し、ワイルドカードや
ALLは避けます runuserまたはsudo -uで権限を落とす — root として開始したスクリプトから非特権ユーザーに処理を引き渡す必要がある場合は、すべてを root として実行するのではなく、適切なツールを使います- 絶対パスを使う — 特権コード内で
$PATHに依存せず、バイナリのパスをハードコードして乗っ取りを防ぎます - 特権スクリプト内で引数を検証する — sudoers ルールは最初の防御線であって、唯一の防御線ではありません。許可リストを使います
- trap を設定してクリーンアップする —
trap cleanup EXITを使い、エラーが発生しても一時的な特権リソースが確実に破棄されるようにします - 昇格をすべてログに記録する — 構造化された監査エントリと組み込みの sudo syslog を組み合わせることで、すべての特権操作を追跡できます
これらの実践を一貫して適用すると、自動化の攻撃対象領域を 常時 root から 証明可能に必要な場合のみ root へと縮小できます。これは、堅牢化された本番品質の Bash に欠かせない特徴です。
よくある質問
「最小権限実行とsudoの適切な運用」レッスンは無料ですか?
はい。「最小権限実行とsudoの適切な運用」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。
「最小権限実行とsudoの適切な運用」で何を学びますか?
権限を降格し、sudoルールの範囲を厳密に限定して、危険な操作の前に実効UIDを検証します。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
DevOps Bootcampを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「最小権限実行とsudoの適切な運用」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このDevOps Bootcampレッスンでコードを書いて実行できますか?
はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- コマンドインジェクションと引数インジェクションの防止
- シークレットの安全な取り扱いと環境の衛生管理
- 最小権限実行とsudoの適切な運用
- ShellCheckによる静的解析と監査