管道里的 sudo 为什么失效:重定向权限问题的两种解决路径

2026-11-09T13:21:08.245841Z

先复现:加了 sudo 仍然 Permission denied

sudo echo "127.0.0.1 api.local" >> /etc/hosts

终端返回:

bash: /etc/hosts: Permission denied

注意报错前缀是 bash,不是 sudo。这一行信息已经把责任方交代清楚了:拒绝访问发生在 shell 尝试打开 /etc/hosts 的时候,此时 sudo 还没被启动。把同样的内容改用 sudo tee -a /etc/hosts 写入就一切正常,说明 sudo 本身没坏,问题出在 >> 上。

根因:重定向由当前 shell 完成

在 Linux 上执行一条带重定向的命令,顺序大致是这样:

  1. shell 解析命令行,识别出 >> 和后面的文件名;
  2. shell 调用 open(),以当前用户身份打开 /etc/hosts,拿到文件描述符;
  3. shell fork() 出子进程,把这个描述符复制到子进程的标准输出;
  4. 子进程 exec() 执行 sudo,sudo 校验策略后再 exec() 真正的 echo。

第 2 步就已经决定了成败。/etc/hosts 属主是 root、权限 644,普通用户没有写权限,open() 直接返回 EACCES,第 3、4 步根本不会发生。sudo 的能力边界在于它启动的那个进程——它能把该进程的 uid 换成 0,但无法追溯修改父 shell 在此前已经完成的系统调用。

一句话概括:sudo 改变的是命令进程的身份,不改变 shell 进程的身份,而重定向是 shell 干的活。

管道里的同类问题:先问“谁在访问受保护资源”

管道本身不落盘,但它把判断拆成了两半,容易看漏。

# 左边 echo 以普通用户运行,右边 tee 以 root 运行
echo "127.0.0.1 api.local" | sudo tee -a /etc/hosts

# 左边 cat 需要读权限,右边 tee 只往普通目录写
sudo cat /etc/shadow | tee /tmp/shadow.copy

# 读端提权了,写端没有
sudo cat /etc/shadow > /root/shadow.copy

判断方法只有一条:列出命令里每个真正打开文件的进程,逐个确认它有没有权限。> 和 >> 归属 shell,tee 归属管道右侧的进程,cat 归属左侧进程。给错的进程加 sudo,当然不起作用。

路径一:用 tee 承接写入

tee 从标准输入读,同时往标准输出和指定文件写。把它放在管道右端并加 sudo,打开文件的动作就由 root 身份的 tee 完成。

echo "127.0.0.1 api.local" | sudo tee -a /etc/hosts > /dev/null

逐段拆开看:

末尾的 > /dev/null 不能省。tee 会把写入内容同时送到标准输出,不加就把整段配置回显在终端里,写进脚本时尤其碍事。

写多行内容用 heredoc,分隔符加上引号可以阻止变量展开:

sudo tee /etc/sysctl.d/99-tuning.conf > /dev/null <<'EOF'
vm.swappiness = 10
net.core.somaxconn = 1024
EOF

两条注意:

路径二:把整段操作交给 root shell

另一种写法是让 sudo 启动一个 shell,重定向就由这个 root shell 来做:

sudo sh -c 'echo "127.0.0.1 api.local" >> /etc/hosts'

这里的 >> 在单引号内,当前 shell 不处理,而是原样传给 sudo 启动的 sh;sh 的 uid 是 0,打开 /etc/hosts 自然成功。

这条路径的坑集中在引号上:

# 错误:$HOME 被当前 shell 提前展开成普通用户的家目录
sudo sh -c "echo $HOME > /etc/marker"

# 正确:单引号阻止提前展开,由 root shell 求值
sudo sh -c 'echo $HOME > /etc/marker'

如果确实需要把当前 shell 的变量带进去,用位置参数传递,而不是把整串命令塞进双引号:

target=/etc/myapp.conf
sudo sh -c 'printf "%s\n" "$1" > "$2"' _ "$value" "$target"

更简单的办法是让内容从标准输入进入,变量在管道左侧展开:

printf 'listen = %s\n' "$port" | sudo tee /etc/myapp.conf > /dev/null

需要多条命令顺序执行时,sh -c 比 tee 省事:

sudo sh -c 'cp /etc/hosts /etc/hosts.bak && sed -i "/api.local/d" /etc/hosts'

代价是提权范围从单个命令扩大到了整段脚本,任何一处写错都以 root 执行。

两条路径的对比

维度 sudo tee 路径 sudo sh -c 路径
提权范围 只有 tee 一个进程 整个子 shell 及其启动的所有命令
变量展开 在当前 shell 展开,行为直观 默认在 root shell 展开,需坚持单引号
sudoers 精细化 可只授权 tee 对特定路径 授权 sh -c 等于放开任意命令
多命令协作 需多次 tee 或额外脚本 一条命令内即可完成
输出控制 内容会回显,需重定向到 /dev/null 由脚本自身控制

补充一点审计上的差异:sudo 日志记录的是实际被执行的那条命令。tee 方案里记录的是 tee /etc/hosts,管道左侧产生内容的那条命令不会进入 sudo 记录;sh -c 方案里整串字符串会进日志,但可读性差。两种都不构成完整的变更审计。这一点取决于本机 sudoers 中 log_output、syslog 等选项的配置,默认配置下不要指望它提供内容级别的记录。

顺带澄清几个相邻的误区

sudo cd /root 不生效。 cd 是 shell 内建命令,不是可执行文件,sudo 找不到可以 exec 的目标。要切换目录请用 sudo -i 或 sudo -s 进入 root shell,或者改用 sudo ls /root 这类真正的外部命令。

sudo echo $PATH 打印的不是 root 的 PATH。 $PATH 在 sudo 启动之前就被当前 shell 展开了,echo 只是把这串字符串打出来。想看 root 的环境用 sudo printenv PATH。

后台任务的重定向同样归属 shell。 sudo cmd > /var/log/x.log & 里的 > 仍由当前 shell 完成,& 只是把整个作业放到后台运行,不改变权限归属。

不要用“把用户加进某个组”来绕过。 为了写一个文件把账号塞进 disk、adm 之类的组,等于扩大了这个账号的全部权限,收益远小于风险。正确做法是收紧目标文件的权限,或用 sudoers 精确授权一条命令。

一段可复用的排查顺序

  1. 看报错前缀。bash:、zsh: 开头说明是 shell 打开文件失败,在前面追加 sudo 无效;程序名开头才是程序自身报错。
  2. 找出命令里所有会打开文件的进程:重定向目标、管道左右两端、命令内部的写操作。
  3. 判断每个进程运行时是什么身份,只给真正需要的那一个加 sudo。
  4. 单条写入优先用 | sudo tee,追加时加 -a;需要多条命令或复杂引号时用 sudo sh -c,并坚持用单引号。
  5. 修改系统配置前先备份,写完用 sudo cat 或 stat 确认属主与权限符合预期。

把“谁在什么身份下打开文件”这一条想清楚,这类问题基本不会出现第二次。

文章转载协议

本文使用 CC BY 4.0 协议,无须申请转载。

原文地址:https://www.geekcorelab.com/zh-cn/post/guan-dao-li-de-sudo-wei-shen-me-shi-xiao-zhong-ding-xiang-quan-xian-wen-ti-de-liang-zhong-jie-jue-lu-jing

协议链接:https://creativecommons.org/licenses/by/4.0/deed.zh-hans