极
极核实验室

Why sudo in a pipe fails: two paths to solve redirection permission problems

This article is licensed under CC BY 4.0; no application is required.

No application is required; sharing, adaptation, and commercial use are allowed with attribution, the original URL, and a link to the license.

  • Sharing, adaptation, and commercial use are allowed
  • Attribution is required
  • Include the original URL
  • Include a link to this license

Original URL:https://www.geekcorelab.com/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

License URL:https://creativecommons.org/licenses/by/4.0/deed.zh-hans

Optional registration generates license codes and short links; it is not required for reposting.

Reproduce first: Permission denied even after adding sudo

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

The terminal returns:

bash: /etc/hosts: Permission denied

Note that the error prefix is bash, not sudo. This line already makes the responsible party clear: access is denied when the shell tries to open /etc/hosts, and at that moment sudo has not yet been started. If you write the same content with sudo tee -a /etc/hosts, everything works normally, which shows that sudo itself is not broken; the problem lies with >>.

Root cause: redirection is performed by the current shell

When executing a command with redirection on Linux, the sequence is roughly as follows:

  1. the shell parses the command line and identifies >> and the filename after it;
  2. the shell calls open() to open /etc/hosts as the current user identity and obtains a file descriptor;
  3. the shell fork()s a child process and copies this descriptor to the child process's standard output;
  4. the child process exec()s sudo; after sudo validates the policy, it exec()s the real echo.

Step 2 already determines success or failure. /etc/hosts is owned by root and has permissions 644; ordinary users do not have write permission, so open() directly returns EACCES, and steps 3 and 4 never happen. The boundary of sudo's capability lies in the process it starts—it can change that process's uid to 0, but it cannot retroactively modify system calls that the parent shell has already completed.

In one sentence: sudo changes the identity of the command process, not the identity of the shell process, and redirection is work done by the shell.

The same kind of problem in pipes: first ask “who is accessing the protected resource”

The pipe itself does not write to disk, but it splits the judgment into two halves, making it easy to overlook.

# 左边 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

There is only one way to judge: list every process in the command that actually opens a file, and confirm one by one whether it has permission. > and >> belong to the shell, tee belongs to the process on the right side of the pipe, and cat belongs to the process on the left. Adding sudo to the wrong process will naturally not work.

Path one: use tee to handle the write

tee reads from standard input and writes to both standard output and the specified file. Put it at the right end of the pipe and add sudo, and the action of opening the file is performed by tee running as root.

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

Break it down section by section:

  • echo is run by the current user and only writes to the pipe; it does not touch the disk;
  • the pipe is created by the shell and does not involve file permissions;
  • in sudo tee -a /etc/hosts, tee opens the file as root and appends to it;
  • the trailing > /dev/null is executed by the current shell; /dev/null is writable by all users and will not raise an error.

The trailing > /dev/null cannot be omitted. tee sends the written content to standard output at the same time; without it, the entire configuration is echoed back in the terminal, which is especially annoying when writing scripts.

For writing multiple lines, use a heredoc; adding quotes around the delimiter prevents variable expansion:

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

Two cautions:

  • Without -a, sudo tee is equivalent to >, and will truncate the original file; back up before changing configuration;
  • A file newly created by tee is owned by root, and its permissions depend on the umask when tee runs. When fixed permissions are needed, create it with install -m 644, or run chmod once after writing.

Path two: hand the whole operation to a root shell

Another way is to have sudo start a shell, so the redirection is performed by this root shell:

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

Here >> is inside single quotes, so the current shell does not process it; instead it is passed as-is to the sh started by sudo; the uid of sh is 0, so opening /etc/hosts naturally succeeds.

The pitfalls of this path are concentrated around quoting:

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

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

If you really need to bring variables from the current shell in, pass them via positional parameters instead of stuffing the entire command string into double quotes:

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

A simpler approach is to let the content enter through standard input, with the variable expanded on the left side of the pipe:

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

When multiple commands need to be executed in sequence, sh -c is more convenient than tee:

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

The cost is that the privilege escalation scope expands from a single command to the entire script; any mistake anywhere is executed as root.

Comparison of the two paths

Dimension sudo tee path sudo sh -c path
Privilege escalation scope Only the tee process The entire subshell and all commands it starts
Variable expansion Expanded in the current shell, with intuitive behavior Expanded in the root shell by default; must stick to single quotes
sudoers granularity Can authorize tee only for specific paths Authorizing sh -c is equivalent to opening up arbitrary commands
Multi-command collaboration Requires multiple tee invocations or an extra script Can be completed within one command
Output control Content is echoed and needs redirection to /dev/null Controlled by the script itself

One more difference in auditing: the sudo log records the command that was actually executed. In the tee approach, it records tee /etc/hosts; the command on the left side of the pipe that produces the content does not enter the sudo record. In the sh -c approach, the entire string enters the log, but readability is poor. Neither constitutes a complete change audit. This depends on the configuration of options such as log_output and syslog in the local sudoers; under the default configuration, do not expect it to provide content-level records.

A few related misconceptions clarified in passing

sudo cd /root does not take effect. cd is a shell builtin, not an executable file, so sudo cannot find a target to exec. To change directories, use sudo -i or sudo -s to enter a root shell, or switch to a real external command such as sudo ls /root.

sudo echo $PATH does not print root's PATH. $PATH is expanded by the current shell before sudo starts; echo merely prints this string. To see root's environment, use sudo printenv PATH.

Redirection in background jobs also belongs to the shell. In sudo cmd > /var/log/x.log &, the > is still performed by the current shell; & only puts the entire job into the background and does not change permission ownership.

Do not use “add the user to a group” to bypass this. To write one file, putting the account into groups such as disk or adm is equivalent to expanding all permissions of that account, and the benefit is far less than the risk. The correct approach is to tighten the permissions of the target file, or use sudoers to precisely authorize one command.

A reusable troubleshooting sequence

  1. Look at the error prefix. A prefix of bash: or zsh: means the shell failed to open the file, and appending sudo in front is ineffective; only when the program name is the prefix is it an error from the program itself.
  2. Identify all processes in the command that will open files: redirection targets, both ends of the pipe, and write operations inside the command.
  3. Determine what identity each process runs as, and add sudo only to the one that really needs it.
  4. For a single write, prefer | sudo tee, and add -a when appending; when multiple commands or complex quoting are needed, use sudo sh -c and stick to single quotes.
  5. Back up before modifying system configuration; after writing, use sudo cat or stat to confirm that ownership and permissions meet expectations.

Once you think clearly about “who opens the file under what identity”, this kind of problem basically will not occur a second time.

✨ Reactions & Feedback

Enjoyed this article? Share your reaction with the author

Comments

Discussion

Be kind and keep the discussion friendly.

Your email will not be shown publicly.

Loading...

Markdown supported.

Discussion

0 comments

Internal link graph

Loading internal links…

View linked articles