83

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?

VoY
  • 1,265
  • 2
  • 11
  • 9

12 Answers12

155

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.

chicks
  • 3,639
  • 10
  • 26
  • 36
ThoriumBR
  • 5,272
  • 2
  • 23
  • 34
  • 2
    Or even: `ssh user@remotehost "echo \`base64 -w0 script.sh\` | base64 -d | sudo bash"` – Niklas B. Sep 03 '14 at 23:49
  • 1
    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. – CodeGnome Sep 04 '14 at 12:29
  • 1
    Good note, but if it's a script, I don't expect any transfer to be more than a few kb. – ThoriumBR Sep 04 '14 at 12:33
  • 9
    There's a useless use of echo in here too. `base64 script | ssh remotehost 'base64 -d | sudo bash'` would be enough :) – hobbs Sep 05 '14 at 07:48
  • 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. – Soham Chakraborty Aug 22 '16 at 07:52
  • What if I'm using a sudosh filter? How do I then run an arbitarily complex command? – N M Nov 09 '17 at 06:09
36

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.

moebius_eye
  • 1,092
  • 7
  • 21
10

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.

dr. Sybren
  • 241
  • 1
  • 5
8

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'")"
Tom Fenech
  • 190
  • 8
  • 1
    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 :-) – Jon L. Apr 23 '15 at 07:26
8

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

Dejay Clayton
  • 181
  • 1
  • 2
5

Are you aware that you can use sudo to give you a shell where you can run commands as the chosen user?

-i, --login

Run the shell specified by the target user's password database entry as a login shell. This means that login-specific resource files such as .profile or .login will be read by the shell. If a command is specified, it is passed to the shell for execution via the shell's -c option. If no command is specified, an interactive shell is executed. sudo attempts to change to that user's home directory before running the shell. The com‐ mand is run with an environment similar to the one a user would receive at log in. The Command Environment section in the sudoers(5) manual docu‐ ments how the -i option affects the environment in which a command is run when the sudoers policy is in use.

justinpc
  • 151
  • 5
  • this seems like the obvious solution to me. > sudo -i -u brian – code_monk Sep 07 '14 at 02:26
  • 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. :) – dannysauer Jul 13 '15 at 17:57
4

You can define the script on your local machine and then cat and pipe it to the remote machine:

user@host:~/temp> echo "echo 'Test'" > fileForSsh.txt
user@host:~/temp> cat fileForSsh.txt | ssh localhost

Pseudo-terminal will not be allocated because stdin is not a terminal.
stty: standard input: Invalid argument
Test
jas_raj
  • 417
  • 3
  • 6
  • 5
    UUOC you can use – user9517 Sep 02 '14 at 09:54
  • 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? – VoY Sep 02 '14 at 10:36
  • 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 Sep 02 '14 at 10:49
  • Although [this question](http://serverfault.com/questions/116654/running-sudo-over-ssh) may help if you can get the `/etc/sudoers` file changed – jas_raj Sep 02 '14 at 10:51
  • 1
    This works for me: `ssh -tq user@host "sudo bash -s" < test.sh` – AVee Sep 04 '14 at 18:00
3

Simple and easy.

ssh user@servidor "bash -s" < script.sh

rafaelrms
  • 31
  • 1
1

If you use a sufficiently modern Bash shell, you could put your commands in a function and print that function as a string using export -p -f function_name. The result of that string can then contain arbitrary commands that do not necessarily have to run as root.

An example using pipes from my answer on Unix.SE:

#!/bin/bash
remote_main() {
   local dest="$HOME/destination"

   tar xzv -C "$dest"
   chgrp -R www-data "$dest"
   # Ensure that newly written files have the 'www-data' group too
   find "$dest" -type d -exec chmod g+s {} \;
}
tar cz files/ | ssh user@host "$(declare -pf remote_main); remote_main"

An example that retrieves saves the file for the login user and installs a program a root:

remote_main() {
    wget https://example.com/screenrc -O ~/.screenrc
    sudo apt-get update && sudo apt-get install screen
}
ssh user@host "$(declare -pf remote_main); remote_main"

If you would like to run the whole command using sudo, you could use this:

remote_main() {
    wget https://example.com/screenrc -O ~user/.screenrc
    apt-get update && apt-get install screen
}
ssh user@host "$(declare -pf remote_main);
    sudo sh -c \"\$(declare -pf remote_main); remote_cmd\""
# Alternatively, if you don't need stdin and do not want to log the command:
ssh user@host "$(declare -pf remote_main);
    (declare -pf remote_main; echo remote_cmd) | sudo sh"
Lekensteyn
  • 6,111
  • 6
  • 37
  • 55
1

Here is my (not really tested) crappy solution for this:

#!/usr/bin/env ruby
# shell-escape: Escape each argument.
ARGV.each do|a|
  print " '#{a.gsub("'","\'\\\\'\'")}' "
end

Not you can do:

ssh -tq myuser@hostname "$(shell-escape sudo -u scriptuser bash -c "$(shell-escape ls -al)")"

The next step is to create a betterssh script which already does this:

betterssh -tq myuser@hostname sudo -u scriptuser bash -c "$(shell-escape ls -al)"
ysdx
  • 1,623
  • 11
  • 13
1

For these of you who needs to pass parameters to the script it should be as follows:

ssh user@remotehost "echo `base64 -w0 script.sh` | base64 -d | sudo bash -s <param1> <param2> <paramN>"
Romande
  • 111
  • 1
1

First, create a local script and then execute it remotely using the follow command:

cat <Local Script.sh> | ssh user@server "cat - | sudo -u <user> /bin/bash"