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 understandssh(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. Thescp …
command that gets to the shell in the container can run additional code; but this is whatscp
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
viassh
anyway.scp
doesn't give me (or anyone else) more possibilities thatssh
does. Still, if I forget that some argument I provide for a localscp
will became (a part of) shell code on the remote side, I may inadvertently run some code on the machine (or container) wherescp
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 keepbalena-helper
relatively simple.