ZetCode

Linux ssh Command

last modified October 23, 2026

The ssh (Secure Shell) command opens an encrypted connection to a remote machine. It is the standard way to log in to Linux servers, run commands on them, copy data, and tunnel network connections. It replaced insecure protocols such as telnet and rlogin; scp, sftp, and rsync use it for file transfers.

The basic syntax is ssh [options] [user@]host [command]. The examples use the OpenSSH 9.2 client on Debian 12, on a local machine called box, with an SSH server called server that listens on ports 22 and 2222 and a host called db that is reachable only from server. Passwords and passphrases typed at the prompts are not echoed, so they do not appear in the output.

Connecting to a server

The first connection to a server asks you to confirm its host key.

ssh jano@server

The host key identifies the server. Before answering yes, compare the fingerprint with the one the administrator gives you through a trusted channel; the server's fingerprint is printed by ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub. The key is then stored in ~/.ssh/known_hosts and checked on every later connection. After the password, you get a shell on the remote machine; exit or Ctrl+D closes the connection. If the user name is omitted, ssh uses your local user name.

$ ssh jano@server
The authenticity of host 'server (172.18.0.2)' can't be established.
ED25519 key fingerprint is SHA256:lr7cZ9OyjmNe4Y9OdsHQDlbeVByPmsrJufBZDBhkcCQ.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'server' (ED25519) to the list of known hosts.
jano@server's password:
Linux server 6.8.0-138-generic #138~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug  7 13:43:15 UTC  x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Fri Oct 23 11:50:00 2026 from 172.18.0.3
jano@server:~$ hostname
server
jano@server:~$ exit
logout
Connection to server closed.

Using a different port

The -p option selects the port when the server does not listen on the default port 22.

ssh -p 2222 jano@server hostname

Because the host key was already accepted for server, ssh does not ask again. Note that scp uses an uppercase -P for the port, which is a common source of confusion.

$ ssh -p 2222 jano@server hostname
jano@server's password:
server

Creating a key pair

Key-based authentication is more secure and more convenient than passwords. The ssh-keygen command creates a key pair; Ed25519 is the recommended type.

ssh-keygen -t ed25519 -C "jano@box"
ls -l ~/.ssh

Pressing Enter at the first prompt keeps the default file name. The passphrase encrypts the private key, so a stolen key file is useless without it. The -C option sets a comment that helps you recognize the key later. The private key id_ed25519 is readable only by its owner and never leaves your machine; the public key id_ed25519.pub is copied to servers. The file known_hosts.old is a backup that ssh made when it added keys to known_hosts.

$ ssh-keygen -t ed25519 -C "jano@box"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/jano/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/jano/.ssh/id_ed25519
Your public key has been saved in /home/jano/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:h7zi9eHe/5wvbUDVGL6FG5d7zLArbREPVGTox/iyWFo jano@box
The key's randomart image is:
+--[ED25519 256]--+
|              o*=|
|             ooo+|
|             .B=o|
|       . .    +#+|
|        S .  .=+=|
|         o   .Eoo|
|      . o . .=+= |
|     . o o oooo.+|
|      .  .+ ...==|
+----[SHA256]-----+
$ ls -l ~/.ssh
total 16
-rw------- 1 jano jano 444 Oct 23 13:51 id_ed25519
-rw-r--r-- 1 jano jano  90 Oct 23 13:51 id_ed25519.pub
-rw------- 1 jano jano 978 Oct 23 13:51 known_hosts
-rw-r--r-- 1 jano jano 142 Oct 23 13:51 known_hosts.old

Copying the public key to a server

The ssh-copy-id command appends a public key to ~/.ssh/authorized_keys on the server and sets the right permissions.

ssh-copy-id -i ~/.ssh/id_ed25519.pub jano@server
ssh jano@server hostname

The account password is needed one last time. From now on, the server accepts the key, and ssh asks only for the passphrase of the local key file. Once all users have keys, password logins can be disabled on the server with PasswordAuthentication no in /etc/ssh/sshd_config.

$ ssh-copy-id -i ~/.ssh/id_ed25519.pub jano@server
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/jano/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
jano@server's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'jano@server'"
and check to make sure that only the key(s) you wanted were added.

$ ssh jano@server hostname
Enter passphrase for key '/home/jano/.ssh/id_ed25519':
server

The SSH agent

The ssh-agent program keeps decrypted keys in memory, so the passphrase is typed once per session.

eval "$(ssh-agent -s)"
ssh-add
ssh-add -l
ssh jano@server hostname

ssh-agent -s prints shell commands that set the SSH_AUTH_SOCK variable, and eval runs them. ssh-add loads the default keys, and ssh-add -l lists the loaded keys. Desktop environments usually start an agent automatically. With AddKeysToAgent yes in ~/.ssh/config, a key is added to the agent the first time it is used.

$ eval "$(ssh-agent -s)"
Agent pid 100
$ ssh-add
Enter passphrase for /home/jano/.ssh/id_ed25519:
Identity added: /home/jano/.ssh/id_ed25519 (jano@box)
$ ssh-add -l
256 SHA256:h7zi9eHe/5wvbUDVGL6FG5d7zLArbREPVGTox/iyWFo jano@box (ED25519)
$ ssh jano@server hostname
server

Running remote commands

A command after the host name is run on the remote machine instead of an interactive shell. The connection closes when the command finishes.

ssh jano@server 'uname -sr; whoami'
ssh jano@server echo $HOSTNAME
ssh jano@server 'echo $HOSTNAME'
ssh jano@server 'cat /etc/os-release' | grep PRETTY

Quoting decides where things are expanded. Without quotes, the local shell replaces $HOSTNAME before ssh runs, so the remote command is echo box. In single quotes, the variable reaches the remote shell. In the last command, cat runs remotely and grep locally, because the pipe is outside the quotes. For interactive programs such as top or sudo with a password prompt, add -t to allocate a terminal.

$ ssh jano@server 'uname -sr; whoami'
Linux 6.8.0-138-generic
jano
$ ssh jano@server echo $HOSTNAME
box
$ ssh jano@server 'echo $HOSTNAME'
server
$ ssh jano@server 'cat /etc/os-release' | grep PRETTY
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"

The config file

The ~/.ssh/config file stores connection settings per host, so you do not have to repeat them.

config
Host web
    HostName server
    User jano
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60

Host defines an alias, and the indented lines apply to it. ServerAliveInterval 60 sends a keepalive message every minute, which prevents idle connections from being dropped by firewalls and NAT routers. The file must not be writable by others.

ssh web hostname
ssh -G web | grep -E '^(hostname|user|port|identityfile|serveraliveinterval) '

ssh web now connects to port 2222 of server as jano with the given key. The -G option prints the effective configuration for a host without connecting, which helps to check which settings apply; the harmless note about the pseudo-terminal appears because its standard input is not a terminal. The aliases also work with scp, sftp, rsync, and git.

$ ssh web hostname
server
$ ssh -G web | grep -E '^(hostname|user|port|identityfile|serveraliveinterval) '
Pseudo-terminal will not be allocated because stdin is not a terminal.
user jano
hostname server
port 2222
serveraliveinterval 60
identityfile ~/.ssh/id_ed25519

Debugging a connection

The -v option prints debugging messages; -vv and -vvv print more.

ssh -V
ssh -v web true 2>&1 | grep -E 'Reading configuration|Connecting to|Authenticating to|Server accepts key|Authenticated to'

ssh -V prints the client version. The full verbose output is long, so the example shows only the key steps: the configuration files read, the address and port, the user, the accepted key (here, from the agent), and the authentication method. When a login fails, these messages show where it stops. Remove addresses and other private details before sharing such logs.

$ ssh -V
OpenSSH_9.2p1 Debian-2+deb12u10, OpenSSL 3.0.22 25 Aug 2026
$ ssh -v web true 2>&1 | grep -E 'Reading configuration|Connecting to|Authenticating to|Server accepts key|Authenticated to'
debug1: Reading configuration data /home/jano/.ssh/config
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: Connecting to server [172.18.0.2] port 2222.
debug1: Authenticating to server:2222 as 'jano'
debug1: Server accepts key: /home/jano/.ssh/id_ed25519 ED25519 SHA256:h7zi9eHe/5wvbUDVGL6FG5d7zLArbREPVGTox/iyWFo explicit agent
Authenticated to server ([172.18.0.2]:2222) using "publickey".

Piping data through ssh

ssh connects the standard input and output of the remote command to the local ones, so it can be used in pipelines.

tar czf - project | ssh web 'tar xzf - -C /tmp && find /tmp/project'
ssh web 'cat /etc/hostname' > remote-hostname.txt
cat remote-hostname.txt

The first command packs a directory locally and unpacks it on the server in one stream, without a temporary archive. The second saves remote output in a local file. For everyday copying, scp and rsync are more convenient.

$ tar czf - project | ssh web 'tar xzf - -C /tmp && find /tmp/project'
/tmp/project
/tmp/project/readme.txt
/tmp/project/docs
/tmp/project/docs/guide.txt
$ ssh web 'cat /etc/hostname' > remote-hostname.txt
$ cat remote-hostname.txt
server

Local port forwarding

The -L option forwards a local port through the SSH connection to an address reachable from the server.

curl -sS http://server:8080/
ssh -N -L 9090:localhost:8080 web &
sleep 1
curl -sI http://localhost:9090/ | head -n 2
kill $!

A web application on the server listens only on its loopback interface, port 8080, so it cannot be reached from the network. The tunnel makes it available on port 9090 of the local machine; localhost in 9090:localhost:8080 is resolved on the server. The -N option opens no remote shell, only the tunnel. This is a common way to reach admin interfaces and databases that are not exposed.

$ curl -sS http://server:8080/
curl: (7) Failed to connect to server port 8080 after 0 ms: Couldn't connect to server
$ ssh -N -L 9090:localhost:8080 web &
$ sleep 1
$ curl -sI http://localhost:9090/ | head -n 2
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.14.0
$ kill $!

Remote port forwarding

The -R option works in the opposite direction: it opens a port on the server that leads to a service on the local machine.

python3 -m http.server 8000 --bind 127.0.0.1 > /dev/null 2>&1 &
ssh -N -R 9000:localhost:8000 web &
sleep 1
ssh web 'curl -sI http://localhost:9000/ | head -n 1'
kill $!

A development web server runs locally on port 8000. On the server, port 9000 now leads to it. By default, the remote port is bound to the server's loopback interface; making it reachable from other machines requires GatewayPorts in the server configuration.

$ python3 -m http.server 8000 --bind 127.0.0.1 > /dev/null 2>&1 &
$ ssh -N -R 9000:localhost:8000 web &
$ sleep 1
$ ssh web 'curl -sI http://localhost:9000/ | head -n 1'
HTTP/1.0 200 OK
$ kill $!

SOCKS proxy

The -D option starts a SOCKS proxy on a local port. Each connection made through the proxy is opened from the server.

ssh -N -D 1080 web &
sleep 1
curl -sI --socks5-hostname localhost:1080 http://localhost:8080/ | head -n 2
kill $!

With --socks5-hostname, curl lets the proxy resolve the host name, so localhost means the server, and the request reaches the web application from the previous example. Browsers can use the same proxy to route their traffic through the server, for example on an untrusted network. The traffic is encrypted only between your machine and the server.

$ ssh -N -D 1080 web &
$ sleep 1
$ curl -sI --socks5-hostname localhost:1080 http://localhost:8080/ | head -n 2
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.14.0
$ kill $!

Jump hosts

The -J option connects to a host through an intermediate host, often called a bastion or jump host.

ssh jano@db hostname
ssh -J web jano@db hostname

The local machine cannot resolve or reach db. With -J, ssh first connects to web and then opens the connection to db through it. The connection is encrypted end to end, the jump host only forwards it, and the key from the local agent is used for both hops. In the config file, the same is written as ProxyJump web in a Host db block.

$ ssh jano@db hostname
ssh: Could not resolve hostname db: Temporary failure in name resolution
$ ssh -J web jano@db hostname
The authenticity of host 'db (<no hostip for proxy command>)' can't be established.
ED25519 key fingerprint is SHA256:JukR+oypi/uWL7NTbXTDar1uVY2XnP9ZtbXYm2W54+g.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'db' (ED25519) to the list of known hosts.
db

Connection sharing

OpenSSH can reuse one connection for several sessions. The first session becomes the master, and later sessions skip the key exchange and authentication.

time ssh web true
ssh -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%C -o ControlPersist=60 web true
ssh -o ControlPath=~/.ssh/cm-%C -O check web
time ssh -o ControlPath=~/.ssh/cm-%C web true

ControlPersist=60 keeps the master running in the background for 60 seconds after the last session ends. -O check asks whether the master is running. A shared session starts in milliseconds instead of a quarter of a second, which speeds up scripts and tools such as Ansible that open many connections. The options are usually put in the config file.

$ time ssh web true

real	0m0.248s
user	0m0.088s
sys	0m0.007s
$ ssh -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%C -o ControlPersist=60 web true
$ ssh -o ControlPath=~/.ssh/cm-%C -O check web
Master running (pid=199)
$ time ssh -o ControlPath=~/.ssh/cm-%C web true

real	0m0.015s
user	0m0.006s
sys	0m0.004s

Key file permissions

ssh refuses to use a private key that other users can read.

chmod 644 ~/.ssh/id_ed25519
SSH_AUTH_SOCK= ssh -o BatchMode=yes web true
chmod 600 ~/.ssh/id_ed25519

Emptying SSH_AUTH_SOCK hides the agent, so ssh must read the key file, and BatchMode=yes fails instead of asking for a password. The key is ignored and authentication fails. Keep ~/.ssh at mode 700 and private keys at mode 600.

$ chmod 644 ~/.ssh/id_ed25519
$ SSH_AUTH_SOCK= ssh -o BatchMode=yes web true
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/home/jano/.ssh/id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/home/jano/.ssh/id_ed25519": bad permissions
jano@server: Permission denied (publickey,password).
$ chmod 600 ~/.ssh/id_ed25519

When a host key changes

If the server presents a different host key than the one stored in known_hosts, ssh refuses to connect. Here, the administrator regenerated the server's host keys.

ssh web 'sudo rm /etc/ssh/ssh_host_*; sudo ssh-keygen -A'
ssh jano@server hostname
ssh-keygen -R server
ssh jano@server hostname

The warning is loud for a reason: a changed key may mean that someone intercepts the connection. Legitimate causes are a reinstalled server or new keys, as here; confirm with the administrator before you continue. ssh-keygen -R removes the old entries for the host, and the next connection asks about the new key like a first connection.

$ ssh web 'sudo rm /etc/ssh/ssh_host_*; sudo ssh-keygen -A'
ssh-keygen: generating new host keys: RSA ECDSA ED25519
$ ssh jano@server hostname
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:jS8EQUfoe5K6M1e+NmsgEhHBNImrGwgmqbRnun7RwJQ.
Please contact your system administrator.
Add correct host key in /home/jano/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /home/jano/.ssh/known_hosts:3
  remove with:
  ssh-keygen -f "/home/jano/.ssh/known_hosts" -R "server"
Host key for server has changed and you have requested strict checking.
Host key verification failed.
$ ssh-keygen -R server
# Host server found: line 1
# Host server found: line 2
# Host server found: line 3
/home/jano/.ssh/known_hosts updated.
Original contents retained as /home/jano/.ssh/known_hosts.old
$ ssh jano@server hostname
The authenticity of host 'server (172.18.0.2)' can't be established.
ED25519 key fingerprint is SHA256:jS8EQUfoe5K6M1e+NmsgEhHBNImrGwgmqbRnun7RwJQ.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'server' (ED25519) to the list of known hosts.
server

Best practices for ssh

Source

OpenSSH ssh manual page

In this article, we have used the ssh command to log in to a server, set up key authentication and an agent, run remote commands, use a config file, forward ports, connect through a jump host, share connections, and handle changed host keys.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.