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

67,113

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

Share:
67,113
VoY
Author by

VoY

Web application programmer - Python and Javascript

Updated on September 18, 2022

Comments

  • VoY
    VoY over 1 year

    I have a system that I can only log in to under my username (myuser), but I need to run commands as other user (scriptuser). So far, I have come up with the following to run the commands I need:

    ssh -tq myuser@hostname "sudo -u scriptuser bash -c \"ls -al\""
    

    If however, when I try to run a more complex command, such as [[ -d "/tmp/Some directory" ]] && rm -rf "/tmp/Some directory" I quickly get into trouble with quoting. I'm not sure how I could pass this example complex command to bash -c, when \" already delimites the boundaries of the command I'm passing (and so I don't know how to quote /tmp/Some directory, which includes a spaces.

    Is there a general solution allowing me to pass any command no matter how complex/crazy the quoting is, or is this some sort of limitation I have reached? Are there other possible and perhaps more readable solutions?

    • user9517
      user9517 over 9 years
      Copy a script to $remote then execute it.
    • VoY
      VoY over 9 years
      That would work, but I find a solution which leaves no script files behind (in case something fails etc.) would be a bit cleaner.
    • thanasisk
      thanasisk over 9 years
      have the script rm itself at end of each run
    • Nils Toedtmann
      Nils Toedtmann over 9 years
      Consider to use fabric fabfile.org. Can make you life much easier if you have to sudo remotely a lot.
    • mpontillo
      mpontillo over 9 years
      Can we assume you've set up sudo so that it does not require a pasword prompt?
    • Navern
      Navern over 9 years
      fabric or ansible is the way to go. Consider to use something like this.
    • bbaassssiiee
      bbaassssiiee over 9 years
      If you have Ansible installed this command shows the documentation for your use case: ansible-doc script
    • Anil Sharma
      Anil Sharma over 9 years
      You could try something like my answer to a question for dealing with embedded quotes
  • user9517
    user9517 over 9 years
    UUOC you can use <
  • VoY
    VoY over 9 years
    Can you modify the example to include sudo -u scriptuser? Would heredoc be usable considering I need to have variables in the script I'd be piping to the machine?
  • jas_raj
    jas_raj over 9 years
    I was trying: echo "sudo `cat fileForSsh.txt`" | ssh ... but I keep getting sudo: sorry, you must have a tty to run sudo.
  • jas_raj
    jas_raj over 9 years
    Although this question may help if you can get the /etc/sudoers file changed
  • CodeGnome
    CodeGnome over 9 years
    It's worth noting that in most cases you can substitute lzop or gzip for base64 for faster transfer times. However, there may be edge cases. YMMV.
  • ThoriumBR
    ThoriumBR over 9 years
    Good note, but if it's a script, I don't expect any transfer to be more than a few kb.
  • Scott
    Scott over 9 years
    Of course :!cat % | (command) can be done as :w !(command) or :!(command) < %.
  • AVee
    AVee over 9 years
    This works for me: ssh -tq user@host "sudo bash -s" < test.sh
  • answer42
    answer42 over 9 years
    There's a useless use of echo in here too. base64 script | ssh remotehost 'base64 -d | sudo bash' would be enough :)
  • moebius_eye
    moebius_eye over 9 years
    Yes. My line is a cat abuse
  • Jon L.
    Jon L. about 9 years
    this is an awesome solution. I've actually had to use printf before for a similar situation, but I always forget about it. Thanks for posting this :-)
  • dannysauer
    dannysauer almost 9 years
    That works best when combined with "ssh -t" in the case of remote access. Which is included in the question, but bears repeating for the "I can't read this /whole/ page" folks. :)
  • Soham Chakraborty
    Soham Chakraborty over 7 years
    While I haven't tested this solution, will it output result of the script, say for example 'yum check-update' in the stdout? I wanted to ask because if someone thinks I am doing something obviously naive, I can pick up few things.
  • N M
    N M over 6 years
    What if I'm using a sudosh filter? How do I then run an arbitarily complex command?
  • Admin
    Admin over 5 years
    running ssh -tt causes my ssh to not terminate after certain commands are run (adding exit at the end doesn't help)
  • Pedro Vagner
    Pedro Vagner about 4 years
    The question asks for a single sudo command.
  • Dejay Clayton
    Dejay Clayton about 4 years
    @PedroVagner - thanks for pointing that out, I've updated my answer accordingly.