1. Overview

Running a series of commands with sudo over SSH is a common task for system administrators and developers. There are several ways to accomplish this, each with its own advantages and disadvantages.

In this tutorial, we’ll explore various methods to execute a series of arbitrarily complex commands on a remote server over SSH.

2. Environment and Test Script

Suppose we wish to log in as sysadmin on a remote server with IP address 192.168.0.104 in our local area network. Moreover, we want to run a script that computes the sum of numbers. These are within a file named sum.txt located in the remote server’s ~/Documents directory. While the script is located on the client side, sum.txt is on the remote server. The script requires the file sum.txt as a parameter, and we want to execute the script over SSH.

Let’s also assume we’ve already copied our SSH public key to the remote server’s ~/.ssh/authorized_keys, for example, via the ssh-copy-id command:

$ ssh-copy-id [email protected]

The file located on the remote server contains three numbers, one per line:

$ cat sum.txt
10
20
30

The example script called script.sh that we employ for summing the numbers uses the awk command and requires passing the file sum.txt as a parameter:

$ cat script.sh
#!/usr/bin/env bash
filename="$1"
awk '{s=s+$1} END {print s}' "$filename"

The awk command automatically initializes the variable s to the empty string by default, which is converted to zero when used in arithmetic. It then updates s by adding one number at a time as it iterates through the lines of the file. Lastly, it prints the final value of s which represents the sum of all numbers in the file.

If we don’t wish to keep a copy of the script on the remote server, we can modify the script by adding a line near the start for deleting the script file:

$ cat script2.sh
#!/usr/bin/env bash
rm "${BASH_SOURCE[0]}"
filename="$1"
awk '{s=s+$1} END {print s}' "$filename"

This way, the script uses the rm command to remove itself at the beginning of execution. The parameter ${BASH_SOURCE[0]} used with rm represents the path of the script.

In general, the script we wish to run over SSH can be of arbitrary complexity and may even require a remote file as an argument.

3. Running a Script Over SSH

There are a number of ways we can run an SSH script of arbitrarily complex commands without using an interactive shell. These fall under two main approaches:

  1. copy the script to the remote server, for example, via scp, and then execute it through an SSH session
  2. directly execute the script over SSH without copying it to the remote server first

The first approach would normally leave a copy of the script on the remote server, whereas the second approach doesn’t. Therefore, for the two approaches to be equivalent, the script should remove itself at the start of execution. This way, even if errors interrupt the script, the remote server will have no trace of the script file after execution.

If we wish to run the script with sudo privileges, we can use the -t option with ssh forcing a pseudo-terminal allocation so that we can enter a password for sudo when required.

In short, we include sudo in the ssh command. Once the command runs, it prompts us for a password because of sudo. When we enter the password correctly, the script executes with root privileges.

4. Using scp

By first copying script.sh to the remote server with scp which uses the secure shell protocol, we can run it there over the sum.txt file:

$ scp script.sh [email protected]:~/Documents
script.sh                                   100%   95    36.1KB/s   00:00 
$ ssh [email protected] 'cd ~/Documents; bash script.sh sum.txt'
60

Here, the script was first copied to the remote server’s ~/Documents where sum.txt resides. Then we logged in via ssh, changed the directory to ~/Documents, and executed the script while passing sum.txt as an argument. Of course, sudo* can simply be prepended to *bash provided we use the -t option with ssh. The returned result is 60, which is the sum of 10, 20, and 30.

5. Using base64 Encoding

Another approach is to run a script directly over SSH. This requires the use of the -s option with bash so that script commands are read directly from stdin:

$ cmd=$(base64 -w 0 script.sh)
$ ssh [email protected] "echo $cmd | base64 -d | bash -s ~/Documents/sum.txt"
60

Here, we first encode the script content using base64 encoding, which is ASCII-based. The -w 0 option with based64 is for disabling line wrapping. The encoding handles quotations and special symbols. Otherwise, these might have to be carefully escaped when passed explicitly to the ssh command.

After using command substitution to encode and save the content in the variable cmd, we echo the content within the ssh command. Then, we decrypt it using the -d option of base64 and finally pipe it into bash -s which has the path of sum.txt as an argument.

To execute the same command as root, we add the -t option to ssh. Then, we enter the password for sudo when prompted:

$ cmd=$(base64 -w 0 script.sh)
$ ssh -t [email protected] "echo $cmd | base64 -d | sudo bash -s ~/Documents/sum.txt"
[sudo] password for sysadmin: 
60
Connection to 192.168.0.104 closed.

Once the script returns, the connection is automatically closed as the pseudo-terminal allocation is dropped.

6. Using cat and Simplifications

We can also simply cat the script and pass the content over the ssh command:

$ cat script.sh | ssh [email protected] "cat - | bash -s ~/Documents/sum.txt"
60

We begin by piping the content to stdin and use the hyphen () symbol to indicate to the cat command that it should read from stdin. Then, we pipe the content over to bash -s.

An even further simplification is to directly stream the script to the ssh command via redirection:

$ ssh [email protected] 'bash -s ~/Documents/sum.txt' < script.sh
60

We use the script in its local location on the client side as input. The bash -s command accepts the script from stdin, whereas the path to the sum.txt file is used as its first argument.

It’s worth noting that using sudo with this approach isn’t possible, as the redirection of the script from stdin prevents the allocation of a pseudo-terminal.

7. Using a here-document

Another approach for running a script through SSH is to pass the content of the script explicitly using a here-document:

$ ssh [email protected] 'bash -s ~/Documents/sum.txt' << 'EOF'
> filename="$1"
> awk '{s=s+$1} END {print s}' "$filename"
> EOF
60

We quote the EOF marker with single quotes to ensure a literal interpretation of all the content that follows. Also, with this approach, it’s not possible to allocate a pseudo-terminal using the -t option for using sudo.

8. Conclusion

In this article, we’ve learned various ways to execute a series of arbitrarily complex commands over SSH. Each method has its own advantages and disadvantages, depending on the situation and requirements. Shell scripts, encoded commands, and here-documents are all useful for executing commands on remote servers efficiently and securely.