How can I run arbitrarily complex command using sudo over ssh?

Solution 1:

A trick I use sometimes is to use base64 to encode the commands, and pipe it to bash on the other site:

MYCOMMAND=$(base64 -w0 script.sh)
ssh user@remotehost "echo $MYCOMMAND | base64 -d | sudo bash"

This will encode the script, with any commas, backslashes, quotes and variables inside a safe string, and send it to the other server. (-w0 is required to disable line wrapping, which happens at column 76 by default). On the other side, $(base64 -d) will decode the script and feed it to bash to be executed.

I never got any problem with it, no matter how complex the script was. Solves the problem with escaping, because you don't need to escape anything. It does not creates a file on the remote host, and you can run vastly complicated scripts with ease.

Solution 2:

See the -tt option? Read the ssh(1) manual.

ssh -tt root@host << EOF
sudo some # sudo shouldn't ask for a password, otherwise, this fails. 
lines
of
code 
but be careful with \$variables
and \$(other) \`stuff\`
exit # <- Important. 
EOF

One thing I often do is use vim and use the :!cat % | ssh -tt somemachine trick.


Solution 3:

I think the easiest solution lies in a modification of @thanasisk's comment.

Create a script, scp it to the machine, then run it.

Have the script rm itself at the start. The shell has opened the file, so it's been loaded, and can then be removed without problems.

By doing things in this order (rm first, other stuff second) it'll even be removed when it fails at some point.


Solution 4:

You can use the %q format specifier with printf to take care of the variable escaping for you:

cmd="ls -al"
printf -v cmd_str '%q' "$cmd"
ssh user@host "bash -c $cmd_str"

printf -v writes the output to a variable (in this case, $cmd_str). I think that this is the simplest way to do it. There's no need to transfer any files or encode the command string (as much as I like the trick).

Here's a more complex example showing that it works for things like square brackets and ampersands too:

$ ssh user@host "ls -l test"
-rw-r--r-- 1 tom users 0 Sep  4 21:18 test
$ cmd="[[ -f test ]] && echo 'this really works'"
$ printf -v cmd_str '%q' "$cmd"
$ ssh user@host "bash -c $cmd_str"
this really works

I haven't tested it with sudo but it should be as simple as:

ssh user@host "sudo -u scriptuser bash -c $cmd_str"

If you want, you can skip a step and avoid creating the intermediate variable:

$ ssh user@host "bash -c $(printf '%q' "$cmd")"
this really works

Or even just avoid creating a variable entirely:

ssh user@host "bash -c $(printf '%q' "[[ -f test ]] && echo 'this works as well'")"

Solution 5:

UPDATE: Examples now explicitly use sudo.

Here's a way to use Bash syntax with compound assignments to execute arbitrarily complex commands over SSH with sudo:

CMD=$( cat <<'EOT'
echo "Variables like '${HOSTNAME}' and commands like $( whoami )"
echo "will be interpolated on the server, thanks to the single quotes"
echo "around 'EOT' above."
EOT
) \
SSHCMD=$(
  printf 'ssh -tq myuser@hostname sudo -u scriptuser bash -c %q' "${CMD}" ) \
bash -c '${SSHCMD}'

CMD=$( cat <<EOT
echo "If you want '${HOSTNAME}' and $( whoami ) to be interpolated"
echo "on the client instead, omit the the single quotes around EOT."
EOT
) \
SSHCMD=$(
  printf 'ssh -tq myuser@hostname sudo -u scriptuser bash -c %q' "${CMD}" ) \
bash -c '${SSHCMD}'

Formatting commands properly to be executed dynamically somewhere else (e.g. nested shells, remote SSH shells, etc.) can be very tricky - see https://stackoverflow.com/a/53197638/111948 for a good description of why. Complicated bash structures, like the syntax above, can help these commands work properly.

For more information about how cat and << combine to form inline here documents, please see https://stackoverflow.com/a/21761956/111948