Yes, when you run ssh with a command, ssh enters the rsh (as opposed to rlogin) mode whereby instead of running the login shell of the user on the remote system in a pseudo-terminal, sshd runs the specified command with stdin, stdout and stderr connected to three different pipes.
And the contents of those pipes are transmitted over the encrypted tunnel and read/written from/to the stdin, stdout and stderr of the ssh process¹.
So in:
local-cmd | ssh 'remote-cmd' > file
ssh's stdin is the reading of the pipe local-cmd is writing to
ssh's stdout is the file open in write-only mode.
ssh's stderr is likely a terminal device if you ran that from a terminal
And
- what
ssh reads from stdin is sent over the encrypted channel and fed to remote-cmd through a pipe.
- What
remote-cmd writes over there on stdout to a pipe is sent over the encrypted channel by sshd (which is reading from the other end of that pipe) and ssh upon reception writes it to its stdout, so to the file.
- Same for
remote-cmd's stderr on a separate pipe.
You can observe it with lsof:
$ sleep 3 | ((sleep 1; lsof -wc ssh -ad 0-99 +E >&2) <&- & ssh [email protected] 'lsof -wp "$$" -ad 0-2 +E; sleep 2') > file
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
screen 7561 chazelas 9u CHR 5,2 0t0 89 /dev/ptmx ->/dev/pts/5 63562,zsh,0u 63562,zsh,1u 63562,zsh,2u 63562,zsh,10u 63608,sleep,0u 63608,sleep,2u 63609,ssh,2u 63609,ssh,6u 63610,lsof,1u 63610,lsof,2u
sleep 63608 chazelas 1w FIFO 0,14 0t0 698683 pipe 63609,ssh,0r 63609,ssh,4r
ssh 63609 chazelas 0r FIFO 0,14 0t0 698683 pipe 63608,sleep,1w 63609,ssh,4r
ssh 63609 chazelas 1u CHR 1,3 0t0 5 /dev/null
ssh 63609 chazelas 2u CHR 136,5 0t0 8 /dev/pts/5 7561,screen,9u
ssh 63609 chazelas 3u IPv4 697921 0t0 TCP localhost:50006->localhost:ssh (ESTABLISHED)
ssh 63609 chazelas 4r FIFO 0,14 0t0 698683 pipe 63608,sleep,1w 63609,ssh,0r
ssh 63609 chazelas 5w REG 0,39 540 8488375 /home/chazelas/file
ssh 63609 chazelas 6u CHR 136,5 0t0 8 /dev/pts/5 7561,screen,9u
~$ cat file
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sshd 63612 root 5w FIFO 0,14 0t0 698269 pipe 63692,bash,0r 63695,lsof,0r
sshd 63612 root 7r FIFO 0,14 0t0 698270 pipe 63692,bash,1w 63695,lsof,1w
sshd 63612 root 10r FIFO 0,14 0t0 698271 pipe 63692,bash,2w 63695,lsof,2w
bash 63692 root 0r FIFO 0,14 0t0 698269 pipe 63612,sshd,5w
bash 63692 root 1w FIFO 0,14 0t0 698270 pipe 63612,sshd,7r
bash 63692 root 2w FIFO 0,14 0t0 698271 pipe 63612,sshd,10r
Where you can see ssh's fd 4 and 5 (duplicated from fds 0 and 1¹) going to the pipe from sleep and file respectively, and on the remote side (the output of lsof there ends up in file) fds 0, 1, 2 of the remote shell are 3 different pipes all going to sshd.
So, that way, when it comes to standard IO, running ssh cmd is functionally similar to running cmd locally, but still with some important points to note:
- as seen above,
cmd's stdin/stdout/stderr on the remote systems are pipes, regardless of what they are for ssh on the client (like pipe, regular files and tty device respectively in our example). For instance, without the redirection to file, you'd notice that jq's output is not in colour as jq only colours its output when it goes to a tty device.
ssh will consume the output on its stdin and send it to sshd regardless of whether cmd on the remote host reads it, so for instance seq 1000 | (ssh localhost 'IFS= read -r one_line_only'; cat) outputs nothing because even though the remote read command only reads one line, ssh has sent the full output of seq to sshd (most of why was not read by read), so cat has nothing left to read.
Here, you could also do:
curl... | ssh 'jq . > remotefile'
Where the output of the remote jq is stored in a remote file (in the home directory of the remote user). Or:
curl... | ssh 'jq . | tee remotefile' > localfile
(or simply curl | ssh 'jq >&1 > remotefile' > localfile if the login shell of the remote user is zsh) for the output to jq to be stored both locally and remotely.
¹ In the case of openssh at least, stdin and stdout are duplicated on a different fd which are the ones being used and stdout is reopened on /dev/null, but that's an implementation detail and makes no difference in practice.