How can I execute a command on a remote server through scp?

Analysis

To scp something to a container you need to be able to run scp in the container. You don't need an SSH server in the container though.

If you cannot run scp in the container then this answer won't help you.

When you scp to or from a remote location, your local scp (the client) runs ssh under the hood to connect to the remote location and run a specific command there. The command is passed as a single string and interpreted by the remote user's shell, like any command passed via ssh.

In case of scp the command is… scp; with special options though. The options are undocumented (you won't find them in man 1 scp), they turn scp into a server.

If you manage to alter the string passed to the remote shell then you will be able to run scp server in the container instead of in the host (assuming you actually can run scp in the container).

You can alter the string thanks to the -S option of scp:

-S program
Name of program to use for the encrypted connection. The program must understand ssh(1) options.

(source)

With -S you can make scp run a custom executable instead of ssh. The executable should then run ssh and pass the altered string to it.


What happens under the hood

(You can skip this section. It's here for educational purposes.)

Let's see what exactly scp invokes and what the undocumented options are. Create a local file named spy and make it executable (chmod +x spy). This is the code to put in spy:

#!/bin/sh
printf '<%s> ' "$@" >/dev/tty
printf '\n' >/dev/tty

Now invoke scp with the spy instead of ssh. Use a dummy name for a server, our spy won't connect to anything.

  • Downloading. Local command:

    scp -S ./spy dummy:/remote/path /local/path
    

    Output from spy:

    <-x> <-oForwardAgent=no> <-oPermitLocalCommand=no> <-oClearAllForwardings=yes> <--> <dummy> <scp -f /remote/path>
    
  • Uploading. Local command:

    scp -S ./spy /local/path dummy:/remote/path
    

    Output from spy:

    <-x> <-oForwardAgent=no> <-oPermitLocalCommand=no> <-oClearAllForwardings=yes> <--> <dummy> <scp -t /remote/path>
    
  • Uploading more than one file. Local command:

    scp -S ./spy /local/path1 ./path2 dummy:/remote/path
    

    Output from spy:

    <-x> <-oForwardAgent=no> <-oPermitLocalCommand=no> <-oClearAllForwardings=yes> <--> <dummy> <scp -d -t /remote/path>
    

This means without spy and in a shell (that is going to remove quotes) the commands would look like the following, respectively:

ssh -x -oForwardAgent=no -oPermitLocalCommand=no -oClearAllForwardings=yes -- dummy 'scp -f /remote/path'
ssh -x -oForwardAgent=no -oPermitLocalCommand=no -oClearAllForwardings=yes -- dummy 'scp -t /remote/path'
ssh -x -oForwardAgent=no -oPermitLocalCommand=no -oClearAllForwardings=yes -- dummy 'scp -d -t /remote/path'

We can see the special options for the remote scp are -f, -t and -d. You can also find them here.


Altering remote command

As said and shown, scp client uses ssh to run scp server on the remote side. Proper command is passed to ssh as a single string. It's always scp … (where includes special options). It's the last argument for ssh.

To run scp in the container instead of in the host, you need to turn this scp … string into balena-engine exec -it <container_name> scp …. But this is somewhat flawed. Note a local command like this:

scp 'server:/remote/path/*' ./

can be used to download many files. The wildcard (*) is supposed to work in a remote shell. Indeed, the remote command will be scp … /remote/path/* and the remote shell will expand /remote/path/*. However if you turn the remote command into:

balena-engine exec -it <container_name> scp … /remote/path/*

then the shell running on the host will expand /remote/path/*. This is wrong, /remote/path/ may not even exist in the host; and if it exists, its content may be different than what scp will find in the container. You most likely want /remote/path/* to be expanded in the container. For this you need a shell in the container. You need to turn scp … into:

balena-engine exec -it <container_name> /bin/sh 'scp …'

This is how the last argument for ssh should look like. Single-quotes hiding behind are troublesome. There are ways to deal with them.


Altering remote command – the script

Let's build a custom executable (script) and tell scp to use it instead of ssh. Our executable will call ssh with the last command line argument modified.

It will be good not to hardcode <container_name>. scp -S takes the name of executable, we cannot pass an option this way. We can pass something in the environment though.

Create a local file named balena-helper and make it executable (chmod +x balena-helper). This is the code to put in the file:

#!/bin/bash
set -- "${@:1:$(($#-1))}" "balena-engine exec -it ${container@Q} /bin/sh ${!#@Q}"
exec ssh "$@"

The code uses few non-portable features of Bash. It wouldn't work in plain sh.


Usage

In general use balena-helper like this:

container=container_name scp -S /local/path/to/balena-helper …

where denotes all options and operands you would normally place after scp (e.g. -P 22222 /local/file root@device-ip:/destination/; note it's -P 22222 while with ssh it was -p 22222).

It's possible to create a wrapper script over scp that would use balena-helper iff container is in the environment (it would behave like regular scp otherwise). I wouldn't do this though. If I were you and was going to work with scp and a specific container, I would create a temporary alias:

alias scp='container=container_name scp -S /local/path/to/balena-helper'

And then I would use scp in this particular local shell in a straightforward way, e.g.:

scp -P 22222 /local/file root@device-ip:/destination/

where /destination/ is valid in the container. Finally I would unalias scp.


Notes

  • scp can copy from local to local. In this case -S is ignored.

  • scp can copy between two remote locations (with or without -3). balena-helper is not designed to be used in this mode.

  • When rebuilding the last argument, balena-helper quotes (@Q) few strings so they cannot inject code when the whole command string is interpreted by the shell on the remote host. The scp … command that gets to the shell in the container can run additional code; but this is what scp can normally do (and fixing it was not my intention). E.g. this command makes my Debian 10 beep:

    scp kamil@debianserver:"; beep" ./
    

    It only works because I can run beep via ssh anyway. scp doesn't give me (or anyone else) more possibilities that ssh does. Still, if I forget that some argument I provide for a local scp will became (a part of) shell code on the remote side, I may inadvertently run some code on the machine (or container) where scp is about to run in server mode. And so may you.

  • This already mentioned answer notices @Q in Bash is not perfect. See this another answer for alternative. I chose @Q to keep balena-helper relatively simple.