opscape

Networking and remote

ssh

Log in to a remote machine or run a command on it over an encrypted connection; also forwards ports and jumps through hosts.

ssh [OPTION...] [USER@]HOST [COMMAND [ARG...]]

13 of 13 examples run in Debian 13.7 on 2026-10-31 · openssh-client 1:10.0p1-7+deb13u4 · manual: ssh(1)

ssh host opens an interactive shell on a remote machine; ssh host command runs one command there and returns its output and exit status, as if it were a local program. Standard input, output and errors are connected through the encrypted channel, so remote commands fit into local pipelines in both directions.

Authentication is normally by key: ssh-keygen makes a key pair, ssh-copy-id installs the public half on the server, and from then on no password is needed. Per-host settings (user, port, key, jump host) belong in ~/.ssh/config, so that ssh web does what a long command line would otherwise spell out. The same connection machinery carries scp, rsync and git.

The examples connect to server, which in the sandbox is the same machine reached over loopback with a key already installed; the commands work identically against a real remote host.

Examples

Run a single command on a remote host

ssh server hostname
explain
output
demo

ssh logs in, runs hostname remotely, prints its output and exits. No interactive shell is started. (The remote here is the sandbox itself, whose host name is demo.)

Run several commands, quoted

ssh server 'cd demo && wc -l todo.txt notes.txt'
explain
output
  5 todo.txt
  6 notes.txt
 11 total

ssh joins its arguments into one string and hands it to the remote user's shell, so &&, cd and globs work there. Quote the whole thing to stop your local shell from interpreting it first.

Which shell expands the variable

name=laptop
ssh server "echo double: $name"
ssh server 'echo single: $name'
output
double: laptop
single:

In double quotes, $name is expanded locally before ssh sends the command, so the remote side receives echo double: laptop. In single quotes the remote shell expands it, and the variable does not exist there: the output is empty after single:.

Pipe data into and out of a remote command

grep ' 404 ' access.log | ssh server 'wc -l'
ssh server 'cat demo/data/hosts.txt' | sort -k2 | head -n 2
output
4
web01 10.0.0.11
web02 10.0.0.12

Local standard input becomes the remote command's input, and the remote output comes back on standard output. The first line counts on the remote side, the second sorts locally.

The remote exit status comes back

ssh server 'grep -q nothing-like-this demo/todo.txt'
echo "exit status: $?"
output
exit status: 1

grep -q found nothing and exited 1 on the remote host; ssh exits with the same status, so if ssh host 'test -f /etc/app.conf'; then ... works in scripts. ssh itself uses 255 for its own errors.

Run a local script on a remote host

ssh server 'bash -s' <<'EOF'
echo "running as $(whoami) on $(hostname)"
ls demo/logs | wc -l
EOF
output
running as dev on demo
3

bash -s reads the script from standard input, which the here-document supplies, so the script never has to be copied to the server. Quoting 'EOF' keeps $(...) from being expanded locally.

Commands get no terminal unless you ask

ssh server tty
ssh -t server tty
outputexit 1
not a tty
Pseudo-terminal will not be allocated because stdin is not a terminal.
not a tty

A remote command runs without a terminal, so tty prints not a tty. -t requests one, which programs like top, sudo password prompts and vim need; here even -t cannot help because ssh's own standard input is not a terminal, and it says so.

Give a host a short name in an ssh config

ssh_config
Host web
  HostName 127.0.0.1
  User dev
  Port 22
ssh -F ssh_config web 'echo connected to $(hostname) as $USER'
explain
output
connected to demo as dev

A Host block maps the alias web to a real address, user and port. Normally this lives in ~/.ssh/config and needs no -F; the example uses -F to point at a separate file.

See the settings ssh will use for a host

ssh -G server true | grep -E '^(user|hostname|port|connecttimeout) '
explain
output
user dev
hostname server
port 22
connecttimeout none

-G prints the final configuration for a host after applying every config file and default, then exits without connecting (the trailing true is a dummy command, which keeps ssh from warning that it would have no terminal for a login). Useful when a Host pattern is not doing what you expect.

Forward a local port to a remote service

ssh -f -N -L 9090:localhost:8080 server
curl -s http://localhost:9090/api/status.json
output
{"status": "ok", "version": "2.4.1", "uptime_s": 86400}

-L 9090:localhost:8080 listens on local port 9090 and carries each connection through ssh to localhost:8080 as seen from the server. -N runs no command and -f puts ssh in the background once connected. This is how you reach a database or admin page that only listens on the server's loopback.

Fail fast in scripts

ssh -o BatchMode=yes -o ConnectTimeout=5 server 'echo reachable'
explain
output
reachable

BatchMode=yes forbids password and passphrase prompts, so an unattended script fails instead of hanging, and ConnectTimeout bounds how long a dead host can stall it.

Debug a login with -v

ssh -v server true 2>&1 | grep -E 'Authenticat|accepts key'
explain
output
debug1: Authenticating to server:22 as 'dev'
debug1: Authentications that can continue: publickey
debug1: Server accepts key: /home/dev/.ssh/id_ed25519 ED25519 SHA256:pTQrwWxt+t2g4SlardlErNMcCuna6ARNbiOI3+SF7Po
Authenticated to server ([127.0.0.1]:22) using "publickey".

-v writes a debug trace to standard error; the grep keeps the authentication lines. They show which methods the server allows and which key it accepted, the first thing to check when a key login falls back to a password.

When the connection fails

ssh -p 2222 server true
explain
outputexit 255
ssh: connect to host server port 2222: Connection refused

Nothing listens on port 2222, so ssh reports Connection refused and exits with 255, the status it reserves for its own errors (as opposed to the remote command's).

Common pitfalls

  • ssh host rm $file expands $file locally and the remote shell splits it again. Quote the whole remote command, and for untrusted values use printf %q to build it.

  • A remote command that reads standard input (like ssh host cmd inside while read loops) swallows the loop's input. Use ssh -n or redirect < /dev/null.

  • WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED means the server's host key differs from ~/.ssh/known_hosts. Find out why before removing the old entry with ssh-keygen -R host.

  • ssh refuses keys whose private file is readable by others (UNPROTECTED PRIVATE KEY FILE). chmod 600 ~/.ssh/id_* and chmod 700 ~/.ssh fix it.

  • -p sets the port for ssh, but scp uses -P. A Port line in ~/.ssh/config avoids remembering which.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • Linux distributions and macOS both ship OpenSSH, so options match; versions differ (macOS often lags), which matters for newer options and key types.

Options worth knowing

A selection, described in our own words. For the full list, see ssh(1) or ssh --help.

OptionWhat it does
-p PORTConnect to PORT on the remote host.
-l USERLog in as USER (same as USER@HOST).
-i FILEUse the private key in FILE.
-F FILERead configuration from FILE instead of ~/.ssh/config.
-o OPTIONSet any config option, for example -o ConnectTimeout=5.
-vPrint debug messages; -vv and -vvv for more.
-qQuiet: suppress most warnings and diagnostics.
-tForce a pseudo-terminal, for interactive remote programs.
-TDo not allocate a pseudo-terminal.
-nRead standard input from /dev/null.
-NDo not run a remote command; just forward ports.
-fGo to the background after authentication.
-L [BIND:]PORT:HOST:HOSTPORTForward a local port to HOST:HOSTPORT as reached from the server.
-R [BIND:]PORT:HOST:HOSTPORTForward a port on the server back to HOST:HOSTPORT reached from the client.
-D [BIND:]PORTRun a local SOCKS proxy that tunnels through the server.
-J [USER@]HOST[:PORT]Connect through a jump host (comma-separated for several).
-AForward your ssh agent to the server (only to hosts you trust).
-XEnable X11 forwarding.
-CCompress the connection.
-GPrint the effective configuration for the host and exit.
-4Use IPv4 only.
-6Use IPv6 only.
-MAct as a connection-sharing master (with ControlPath).
-S SOCKETUse SOCKET as the control path for connection sharing.
-O COMMANDSend a control command (check, exit, stop) to a master connection.
-W HOST:PORTForward standard input and output to HOST:PORT through the server.
-VPrint the version and exit.

Related