最小权限执行与 sudo 规范
降低权限、严格限定 sudo 规则,并在高风险操作前验证有效 UID。
最小权限执行与 sudo 规范 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
Shell 脚本中最小权限为何重要
自动化中的大多数安全漏洞并非源于复杂的漏洞利用,而是因为脚本运行时拥有超出所需的权限。一个只需轮换日志文件、却以 root 身份运行的 cron 任务,随时可能酿成事故。
最小权限原则规定:每个进程都只能使用完成工作所需的权限,不能更多。在 Bash 脚本中,这意味着:
- 尽可能以无特权用户运行
- 仅针对确实需要 root 的特定命令提升权限
- 提升权限的工作完成后立即放弃权限
- 绝不在其作用域之外存储或继承凭据
本课将介绍具体技术:限制 sudo 的作用域、使用 su 放弃权限、UID 验证防护以及 sudoers 强化,为生产脚本构建严谨的权限模型。
在危险操作前检查有效 UID
在任何真正需要 root 的代码块之前,脚本都应验证自身是否以预期的有效 UID 运行。绝不要假设;始终进行断言。
$EUID 是 Bash 的特殊变量,保存当前进程的有效用户 ID。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 的部分。
请比较下面两种模式。第二种安全得多:
#!/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 规则中的关键字段:
- 用户 — 谁可以调用 sudo
- 主机 — 在哪台机器上(为便于移植,请使用
ALL) - RunAs — 要采用哪个身份(几乎总是
root) - 命令 — 完整的绝对路径,可选择包含字面参数
针对部署服务账户(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: ALL使用 su 和 runuser 放弃权限
当脚本以 root 身份启动(例如由系统初始化程序启动,或由以 root 身份运行的 cron 启动),但大部分工作应由无特权用户完成时,应明确放弃权限,而不是让整个脚本以 root 身份运行。
可使用以下两个工具:
su -s /bin/bash -c 'command' username— 以 username 身份启动 shell 并运行命令runuser -u username -- command args— 在 Linux 上适合在由 root 拥有的脚本中切换身份;比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 以其他用户身份运行单个命令
您不一定需要切换到完整的 shell 会话。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)."避免通过环境变量提升权限
一个隐蔽的攻击面是:由 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 设置的陷阱保证这一过程一定会执行:
#!/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."审计并记录特权操作
当每次权限提升事件都带有上下文地记录时,就更容易落实最小权限原则:谁在何时、出于什么原因运行了什么操作。请结合以下两层机制:
- sudo 本身——Debian/Ubuntu 中的
/var/log/auth.log或 RHEL 中的/var/log/secure会自动记录每次 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 cleanup EXIT,确保即使发生错误也会销毁临时特权资源 - 记录每次权限提升——结构化审计条目与内置 sudo 系统日志结合后,可以追踪每一次特权操作
持续应用这些实践,可以将自动化系统的攻击面从始终以 root 身份运行缩减为仅在有明确证明确有必要时使用 root 权限——这正是经过强化、达到生产级别的 Bash 应有的特征。
常见问题解答
「最小权限执行与 sudo 规范」课时是免费的吗?
是的 — 「最小权限执行与 sudo 规范」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
「最小权限执行与 sudo 规范」这节课中我会学到什么?
降低权限、严格限定 sudo 规则,并在高风险操作前验证有效 UID。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「最小权限执行与 sudo 规范」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?
能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 防止命令与参数注入
- 安全处理机密信息与保持环境整洁
- 最小权限执行与 sudo 规范
- 使用 ShellCheck 进行静态分析与审计