Skip to main content

Linux command: ssh

17 August 2026

Every server you do not stand in front of, you reach with ssh. It is the front door of Linux administration: the command that puts a shell on a machine in a data centre three countries away, moves your files, carries your deployments, and quietly sits underneath git, rsync, and ansible without ever asking for credit. Most people learn just enough of it to log in and stop there, which is a shame, because the parts they skip are exactly the parts that make daily server work fast and safe.

1. The Basics

The job of ssh is to connect you to another computer over an untrusted network and make that connection safe. You type one line, and a moment later your keyboard is attached to a machine somewhere else.

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it.

Two programs are involved. On your side, ssh is the client. On the far side, a background service called sshd (the SSH daemon) is the server. The client connects to the server on TCP port 22 by default, and the two agree on how to talk before a single character of your session crosses the wire.

In that opening handshake, ssh does three separate things, and it helps to keep them apart in your head:

  • It checks the server. The server proves it owns a private host key, so you know you reached the real machine and not an impostor sitting in between.
  • It checks you. You prove who you are with a key or a password, so the server knows which account to open.
  • It encrypts everything. From the handshake onward, every keystroke and every byte of output is encrypted and integrity-checked.

What makes ssh special is that it did not stay a login tool. Because it can carry any stream of bytes safely, other programs were built on top of it. scp and sftp copy files through it. rsync uses it as its default transport. git push over an ssh:// URL is an SSH session. Ansible configures whole fleets of servers through it. Learn ssh well and you quietly improve every one of those tools at the same time.

The command that turns a remote machine into a local one. Behind a simple login prompt sit host keys, key pairs, an agent, a config file, and a tunnelling engine that most users never touch.

This article starts with your very first connection and builds up to keys, the config file, port forwarding, jump hosts, connection sharing, and server hardening. By the end you should be able to read an ssh command someone else wrote and know exactly what it does.

The right mental model: ssh is not a "remote terminal program". It is an encrypted tunnel between two machines, and a terminal session is only the most common thing you push through it. Once you see the tunnel, port forwarding and jump hosts stop looking like tricks and start looking obvious.

Back to top

2. Where the Name Comes From

The name ssh is short for Secure SHell.

ssh  =  Secure SHell

The "secure" half is the point of the whole program. The "shell" half is inherited. Before SSH existed, people logged into remote Unix machines with telnet, rlogin, and rsh (short for remote shell), and copied files with rcp (remote copy). Those tools worked, but they sent everything across the network in plain text, including your password. Anyone able to watch the traffic could simply read it.

SSH was written as a drop-in replacement for that family, so it copied the naming pattern: rsh became ssh, and rcp became scp. The letter that changed is the letter that matters.

One small correction to a common assumption: ssh does not contain a shell. It never runs one itself. It asks the server to start your normal login shell for your account, and then acts as a secure pipe between that shell and your terminal. The name describes the result you get, not the code inside the program.

The port number has a story too. SSH listens on port 22 because Tatu Ylönen, who wrote it, asked for a number that sat between the two services he was replacing: ftp on port 21 and telnet on port 23. IANA assigned it, and it has been port 22 ever since.

Back to top

3. A Short History

Unlike ls or cat, which reach back to the first days of Unix, SSH is a response to a specific problem at a specific moment. In 1995, Tatu Ylönen, a researcher at Helsinki University of Technology in Finland, discovered a password-sniffing attack on his university network. Someone was capturing the plain-text passwords that telnet and rlogin handed over so freely. He wrote SSH as the fix and released it as freeware in July of that year. By the end of 1995 it had roughly 20,000 users in fifty countries.

Success brought a company, and the company brought licences. Later versions of the original SSH became commercial and increasingly restrictive. In 1999 the OpenBSD team took the last freely licensed code, version 1.2.12, cleaned it up, and released OpenSSH. It shipped with OpenBSD 2.6 on 1 December 1999. A separate portable branch was started almost immediately so the same code could run on Linux and other Unix systems, and that is the version nearly everyone runs today.

You can see both facts in the version string on any Linux box:

$ ssh -V
OpenSSH_9.6p1 Ubuntu-3ubuntu13.18, OpenSSL 3.0.13 30 Jan 2024

The p1 stands for portable, release 1. Everything after the comma is what your distribution added on top.

Meanwhile the protocol itself was rewritten. The original SSH-1 protocol had design weaknesses, and a redesigned SSH-2 replaced it. The IETF published SSH-2 as a standard in January 2006, spread over RFC 4251 to RFC 4254. SSH-1 is long dead and modern OpenSSH cannot speak it at all.

EraMilestone
1995 Tatu Ylönen writes SSH after a password-sniffing attack and releases it as freeware in July
late 1995 About 20,000 users in fifty countries; development continues commercially
1999 The OpenBSD team forks the last free code and ships OpenSSH with OpenBSD 2.6 on 1 December
2006 The IETF publishes SSH-2 as RFC 4251 to RFC 4254 (January)
2014 OpenSSH 6.5 adds Ed25519 keys: small, fast, and modern (January)
2020 OpenSSH 8.2 adds FIDO/U2F hardware security keys, ecdsa-sk and ed25519-sk (February)
2021 OpenSSH 8.8 disables RSA signatures that use SHA-1, because SHA-1 is broken (September)
2022 OpenSSH 9.0 moves scp onto the SFTP protocol and enables hybrid post-quantum key exchange by default (April)
2023 OpenSSH 9.5 makes Ed25519 the default key type for ssh-keygen (October)
2024-2025 OpenSSH 9.8 disables DSA, and 10.0 removes it entirely while making the post-quantum mlkem768x25519-sha256 key agreement the default

That last row is worth a second look. The people who capture encrypted traffic today, hoping to decrypt it once quantum computers arrive, are already a real threat model. SSH has quietly moved to key exchange that resists them, and on OpenSSH 9.6 you can watch it happen:

$ ssh -v server.example
...
debug1: kex: algorithm: This email address is being protected from spambots. You need JavaScript enabled to view it.
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: This email address is being protected from spambots. You need JavaScript enabled to view it. MAC: <implicit> compression: none

You did not ask for any of that, and you did not have to. Keeping ssh updated is most of the security work.

Back to top

4. Simple Use Cases

4.1 The Simplest Possible Connection

Give ssh a hostname and it connects, using your local username on the far side. In the examples below, the line that starts with $ is what you type; the lines under it are what you get back.

$ ssh server.example

Most of the time you want a specific account, which you put in front with an @:

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it.

If the server listens on a different port, add -p (short for port). Note the lowercase letter; in scp the same job uses a capital -P, which trips up everyone at least once.

$ ssh -p 2222 This email address is being protected from spambots. You need JavaScript enabled to view it.

There is also -l (short for login name), an older way to say the same thing as the @ form:

$ ssh -l peter server.example      # same as This email address is being protected from spambots. You need JavaScript enabled to view it.

4.2 The First Connection: That Question About Authenticity

The first time you connect to any machine, ssh stops and asks you something:

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it.
The authenticity of host 'server.example (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:Cbs69zP2QMpFcfrRMSlg3fsDbEpOD+MH2FANbuWy5J8.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Almost everybody types yes without reading it. It is worth understanding, because this is the moment that decides whether your connection is really private.

Every SSH server has its own host key, generated when it was installed and stored in /etc/ssh/. During the handshake the server proves it holds the matching private key. The fingerprint in the question is a short hash of the server's public host key. Your client is telling you, honestly, that it has never seen this machine before and cannot vouch for it.

When you answer yes, the key is written to ~/.ssh/known_hosts, and from then on ssh checks it silently on every connection. This approach is called trust on first use: the first connection is a leap of faith, and every later one is verified.

To take the faith out of it, ask the server administrator for the fingerprint in advance and compare, or generate it yourself from a machine you already trust:

$ ssh-keygen -lf <(ssh-keyscan -t ed25519 server.example 2>/dev/null)
256 SHA256:Cbs69zP2QMpFcfrRMSlg3fsDbEpOD+MH2FANbuWy5J8 server.example (ED25519)

Notice that the prompt accepts three answers, not two: yes, no, or the fingerprint itself pasted in. That third option is the careful one, because it makes ssh compare the string for you instead of asking your eyes to do it.

4.3 Running One Command Instead of a Session

You do not have to log in interactively. Put a command after the destination and ssh runs it, prints the output on your own terminal, and exits.

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it. 'uptime'
 13:53:17 up  4:27,  1 user,  load average: 1.73, 2.06, 2.03

This is the form that makes ssh useful in scripts. Because the output arrives on standard output, you can pipe it into local tools as if the command had run at home:

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it. 'ls /etc' | wc -l
231

It works the other way as well. Whatever you pipe into ssh arrives on the remote command's standard input, which is a neat way to push a file or a database dump without a second tool:

$ cat backup.sql | ssh This email address is being protected from spambots. You need JavaScript enabled to view it. 'mysql mydb'
$ mysqldump mydb | ssh This email address is being protected from spambots. You need JavaScript enabled to view it. 'cat > /backups/mydb.sql'

Put that in a loop and you have a fleet-wide health check in three lines, which is how most administrators write their first automation without noticing they have done it:

$ for server in web01 web02 web03; do
      echo -n "$server: "
      ssh "$server" 'uptime'
  done
web01:  13:53:17 up  4:27,  1 user,  load average: 1.73, 2.06, 2.03
web02:  13:53:18 up 22 days,  3:11,  0 users,  load average: 0.08, 0.12, 0.09
web03:  13:53:19 up  6 days, 18:44,  1 user,  load average: 0.44, 0.39, 0.35

Those short names come from the config file in section 5.5. Once a loop like this grows past a handful of commands, a configuration-management tool is the better answer, but the underlying mechanism stays exactly this.

4.4 The Quoting Trap

The remote command is handed to a shell on the server, but it also passes through your local shell first. That means quoting decides which shell expands your variables, and the difference is easy to demonstrate:

$ GREETING=hello-from-my-laptop

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it. 'echo [$GREETING]'
[]                          # single quotes: expanded on the SERVER, where it is not set

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it. "echo [$GREETING]"
[hello-from-my-laptop]      # double quotes: your local shell expanded it first

Use single quotes when you mean "run this exactly as written on the server". Use double quotes only when you deliberately want a local value baked into the command before it leaves. Mixing them up produces commands that work on your laptop and quietly do nothing useful on the server.

4.5 Getting Out, and Seeing Why It Failed

To leave an interactive session, type exit or press Ctrl-D. If the session hangs and neither works, section 8.1 has the trick almost nobody knows.

When a connection refuses to work, the single most useful flag is -v (short for verbose). Add more vs for more detail, up to -vvv. It prints the whole conversation and tells you exactly where things went wrong:

$ ssh -v This email address is being protected from spambots. You need JavaScript enabled to view it.
OpenSSH_9.6p1 Ubuntu-3ubuntu13.18, OpenSSL 3.0.13 30 Jan 2024
debug1: Reading configuration data /home/peter/.ssh/config
debug1: Connecting to server.example [203.0.113.10] port 22.
debug1: Connection established.
debug1: Authenticating to server.example:22 as 'peter'
debug1: Host 'server.example' is known and matches the ED25519 host key.
debug1: Will attempt key: /home/peter/.ssh/id_ed25519 ED25519 SHA256:nDjQm9Dso... explicit
debug1: Offering public key: /home/peter/.ssh/id_ed25519 ED25519 SHA256:nDjQm9Dso... explicit
debug1: Server accepts key: /home/peter/.ssh/id_ed25519
debug1: Authentication succeeded (publickey).

Read it as a story: read the config, connect, verify the host, offer keys, get accepted. When something fails, the last few lines before the failure tell you which step broke, and that is usually enough to fix it without guessing.

Back to top

5. Moderate Use Cases

5.1 Keys Instead of Passwords

Typing a password on every connection is slow, and worse, a password can be guessed, phished, or brute-forced by the endless robots that scan port 22 all day. Public key authentication solves both problems at once.

You generate a key pair: a private key that never leaves your machine, and a public key that you can hand out freely. The server keeps a copy of the public key. When you connect, the server sends a challenge, your client signs it with the private key, and the server verifies the signature with the public key. Your secret is never transmitted, not even once.

$ ssh-keygen -t ed25519 -C "peter@laptop"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/peter/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/peter/.ssh/id_ed25519
Your public key has been saved in /home/peter/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:nDjQm9DsoviIQK4am/qOpuskiuvJ1RY4lRpJgs+iyxY peter@laptop
The key's randomart image is:
+--[ED25519 256]--+
|.. .             |
|. o .+.          |
| o ooo+          |
|. o =+ = .       |
|.o +..* S        |
|+E .o...         |
|*+o. o           |
|@@o .            |
|^X+              |
+----[SHA256]-----+

That little picture at the end is the randomart: a visual drawing of the same fingerprint. The idea is that humans spot a changed picture faster than a changed string of base64. Almost nobody uses it, and you can safely ignore it.

Three flags matter here. -t (short for type) picks the algorithm, -C (short for comment) adds a label so you can tell your keys apart later, and -f (short for file) sets the filename if you do not want the default.

On OpenSSH 9.5 and newer you can leave -t out entirely, because Ed25519 is now the default. It is the right choice for almost everyone: modern, fast, and short enough to fit on one line.

TypeVerdictNotes
ed25519 Use this Fixed size, fast, tiny public key. Supported since OpenSSH 6.5 (2014)
ed25519-sk / ecdsa-sk Use for high-value access Backed by a FIDO2 hardware token; the key cannot be copied off the device, and you touch it to authenticate
rsa Only for old servers Still fine at 3072 bits (the default) or 4096, but the key is far larger and slower
ecdsa Avoid Works, but Ed25519 is better in every way that matters
dsa Never Obsolete and weak; disabled in OpenSSH 9.8 and removed in 10.0

Look at what you actually get. The public key is one readable line, and the private key is an encrypted block you never open:

$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDA3N37toeUWk+bfwHUdgC64txzH0aqdogl9qkha0mme peter@laptop

$ head -2 ~/.ssh/id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW

The public line has three fields: the key type, the key itself in base64, and your comment. Sharing that line is safe. The file without .pub is the one that must never leave your machine, never be pasted into a chat, and never be committed to git.

Always set a passphrase. A private key file with no passphrase is a plain-text credential: anyone who copies that one file owns every server it opens. With a passphrase, the file on disk is encrypted, and a stolen laptop is an inconvenience instead of a breach. Section 5.4 removes the daily cost of typing it.

If you already have a key without one, you do not have to start over. The -p flag (short for passphrase) changes the passphrase on an existing key, leaving the key itself untouched, so nothing on any server needs updating:

$ ssh-keygen -p -f ~/.ssh/id_ed25519
Key has comment 'peter@laptop'
Enter new passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved with the new passphrase.

5.2 Getting Your Key onto the Server

The server keeps your public keys in ~/.ssh/authorized_keys, one per line. You can append the line by hand, but there is a tool that does it correctly, including creating the directory with the right permissions:

$ ssh-copy-id This email address is being protected from spambots. You need JavaScript enabled to view it.
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/peter/.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

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh This email address is being protected from spambots. You need JavaScript enabled to view it.'"
and check to make sure that only the key(s) you wanted were added.

It logs in with your password one last time and appends the key. Use -i to pick a specific key and -n for a dry run that shows what it would do:

$ ssh-copy-id -n -i ~/.ssh/id_ed25519.pub This email address is being protected from spambots. You need JavaScript enabled to view it.
/usr/bin/ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system.

Permissions matter more than beginners expect, and ssh deliberately refuses to work when they are wrong. Both sides check:

~/.ssh                 700   (drwx------)
~/.ssh/id_ed25519      600   (-rw-------)   private key
~/.ssh/id_ed25519.pub  644   (-rw-r--r--)   public key
~/.ssh/authorized_keys 600   (-rw-------)   on the server

Get the private key wrong and you get one of the loudest error messages in Unix:

$ ssh -i ~/.ssh/id_ed25519 This email address is being protected from spambots. You need JavaScript enabled to view it.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/home/peter/.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.

The fix is one command: chmod 600 ~/.ssh/id_ed25519. This is also the single most common reason a key that "worked yesterday" stops working after files have been copied around with the wrong flags.

5.3 Checking That It Worked

Before you turn passwords off anywhere, confirm from the server's own logs that the key was used. On the server:

$ sudo journalctl -u ssh --since "10 min ago" | tail -3
Accepted publickey for peter from 203.0.113.55 port 50962 ssh2: ED25519 SHA256:nDjQm9DsoviIQK4am/qOpuskiuvJ1RY4lRpJgs+iyxY

The word to look for is publickey. If it says password, your key is not being used yet and disabling passwords will lock you out.

The service name differs between distribution families, which catches people who work on both. Debian and Ubuntu call the unit ssh; Red Hat, Fedora, and SUSE call it sshd. If one name returns nothing, try the other:

$ systemctl status ssh      # Debian, Ubuntu
$ systemctl status sshd     # RHEL, Fedora, SUSE

These logs are worth more than a one-off check. They record failed attempts, unknown usernames, and source addresses, so they are the first place to look when you want to know whether anyone else has been trying the door.

5.4 The Agent: Typing Your Passphrase Once

A passphrase on your key is only bearable if you type it once per day rather than once per connection. That is the job of ssh-agent, a small background program that holds your decrypted private keys in memory and signs challenges on request. It never hands the key itself to anything.

$ ssh-add ~/.ssh/id_ed25519
Enter passphrase for /home/peter/.ssh/id_ed25519:
Identity added: /home/peter/.ssh/id_ed25519 (peter@laptop)

$ ssh-add -l
256 SHA256:nDjQm9DsoviIQK4am/qOpuskiuvJ1RY4lRpJgs+iyxY peter@laptop (ED25519)

The -l flag (short for list) shows what the agent currently holds. On a desktop Linux system an agent is usually started for you at login, and your desktop keyring may unlock the key automatically. On a server or a bare shell you may need to start one yourself:

$ eval "$(ssh-agent -s)"
Agent pid 74960

Two options make the agent nicer to live with. ssh-add -t (short for time) sets a lifetime, so the key is forgotten again after a while, and AddKeysToAgent yes in your config loads a key into the agent the first time you use it, so you never run ssh-add by hand at all:

$ ssh-add -t 8h ~/.ssh/id_ed25519    # forget it after eight hours

5.5 The Config File That Changes Everything

If you learn only one thing from this article beyond keys, make it ~/.ssh/config. It turns long, forgettable commands into short names, and it is where experienced administrators keep their whole server list.

Host web
    HostName web01.example.com
    User peter
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Host *.internal
    User admin
    ServerAliveInterval 60

Host *
    AddKeysToAgent yes
    HashKnownHosts yes

With that in place, this:

$ ssh -p 2222 -i ~/.ssh/id_ed25519 This email address is being protected from spambots. You need JavaScript enabled to view it.

becomes this:

$ ssh web

The alias works everywhere, not just in ssh: scp file web:/tmp/ and rsync -a site/ web:/var/www/ read the same file.

One rule governs the whole file, and it surprises people: the first value found for each setting wins. Later blocks cannot override an earlier one. That is the opposite of most config formats, and it means specific Host blocks belong at the top and the catch-all Host * belongs at the bottom.

When you are not sure what ssh will actually do, ask it. The -G flag prints the fully resolved configuration for a destination without connecting:

$ ssh -G web | grep -E '^(hostname|user|port|identityfile)'
user peter
hostname web01.example.com
port 2222
identityfile ~/.ssh/id_ed25519

This one command answers most "why is it connecting as the wrong user?" questions in seconds.

5.6 Living with known_hosts

Every host you accept lands in ~/.ssh/known_hosts. On Debian and Ubuntu the entries are hashed by default, because /etc/ssh/ssh_config ships with HashKnownHosts yes, so the file looks like this rather than listing your servers in readable text:

$ head -1 ~/.ssh/known_hosts
|1|2UYDkyiaFpx+aPALlarpS9sQdVI=|KpOmbFYFEeDy0di64KcmzAaRymA= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...

That hashing is a deliberate defence: if someone steals your laptop or your backups, they do not also get a tidy list of every server you administer. The trade-off is that you cannot read the file, so you need ssh-keygen to work with it. Use -F (short for find) to look a host up and -R (short for remove) to delete it:

$ ssh-keygen -F server.example      # is this host known?
$ ssh-keygen -R server.example      # forget it

Sooner or later you will meet the alarming version of this. Reinstall a server, or move a domain to a new machine, and the host key changes:

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    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:pWQKB1gfFxQ8fTJ9KiwFIFmT4g71fILH0jZEHn3mOII.
Offending ED25519 key in /home/peter/.ssh/known_hosts:2
  remove with:
  ssh-keygen -f '/home/peter/.ssh/known_hosts' -R 'server.example'
Host key verification failed.

ssh even hands you the exact command to make the warning go away, which is precisely why the warning gets ignored. Stop and think first. If you reinstalled that server an hour ago, the change is expected and you can clear the entry. If nothing changed on your side, this is the one warning in all of Linux that genuinely means "someone may be intercepting you", and the correct response is to find out why before you type anything else.

Back to top

6. Advanced Use Cases

Everything so far treats ssh as a way to get a shell. This section uses it as what it really is: a tunnel that can carry anything.

6.1 Local Forwarding: Bringing a Remote Port to You

The -L flag (short for local) opens a port on your machine and forwards anything that arrives there through the encrypted connection to an address the server can reach.

$ ssh -L 9099:127.0.0.1:8099 This email address is being protected from spambots. You need JavaScript enabled to view it.

Read the three parts left to right as "listen here, connect to there": listen on local port 9099, and forward to 127.0.0.1:8099 as seen from the server. That last detail is the one people miss. The address in the middle is resolved on the far side, so 127.0.0.1 means the server itself.

While that session is open, a browser on your laptop pointed at http://127.0.0.1:9099/ reaches the service on the server:

$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:9099/
200

This is how you reach things that are deliberately not exposed to the internet: a database on localhost:3306, an admin panel bound to the loopback interface, a monitoring dashboard behind a firewall. You get remote access without opening a single extra port to the world.

$ ssh -L 3307:127.0.0.1:3306 This email address is being protected from spambots. You need JavaScript enabled to view it.
# now connect a local client to 127.0.0.1:3307 as if MySQL ran on your laptop

Two flags almost always join it. -N (short for no command) says "do not give me a shell, only the tunnel", and -f (short for fork) sends ssh into the background:

$ ssh -f -N -L 3307:127.0.0.1:3306 This email address is being protected from spambots. You need JavaScript enabled to view it.

6.2 Remote Forwarding: Handing a Local Port Out

The -R flag (short for remote) is the mirror image. It opens a port on the server and forwards connections back to an address you can reach.

$ ssh -R 9097:127.0.0.1:3000 This email address is being protected from spambots. You need JavaScript enabled to view it.

Now anyone on the server who connects to its port 9097 lands on the application running on port 3000 on your laptop. It is how you show a colleague a site that only exists on your development machine, or let a server reach a service it has no route to.

By default the forwarded port on the server binds to loopback only, so it is reachable from the server itself and not from the whole network. Opening it wider needs GatewayPorts enabled in the server's configuration, which is a deliberate decision and not something to switch on casually.

6.3 Dynamic Forwarding: A SOCKS Proxy in One Flag

The -D flag (short for dynamic) is the most powerful of the three and the least known. Instead of forwarding one fixed port, it turns ssh into a SOCKS proxy on your machine. Any program that speaks SOCKS then reaches the network through the server, choosing its destination per connection.

$ ssh -f -N -D 9098 This email address is being protected from spambots. You need JavaScript enabled to view it.

$ curl -s --socks5-hostname 127.0.0.1:9098 -o /dev/null -w "%{http_code}\n" http://internal.example/
200

Point a browser's SOCKS proxy setting at 127.0.0.1:9098 and every page it loads is fetched by the server. For an administrator this is the quick way into a private network: one flag, no VPN client, no extra software on either side. Note the --socks5-hostname form rather than plain --socks5, which makes DNS lookups happen on the server too, so internal hostnames resolve correctly.

6.4 Jump Hosts: Reaching Machines You Cannot Reach

Production servers often refuse connections from the internet and accept them only from a bastion or gateway machine. The old way was to log into the gateway and then run ssh again from there. The modern way is -J (short for jump):

$ ssh -J This email address is being protected from spambots. You need JavaScript enabled to view it. This email address is being protected from spambots. You need JavaScript enabled to view it.

The connection to the final host is tunnelled through the gateway, but it is still an end-to-end encrypted session between your machine and db01. The gateway forwards bytes it cannot read. You can chain several hops with commas.

One trap is documented but easy to miss: options you pass on the command line apply to the destination, not to the jump host. If the gateway needs its own port, user, or key, put that in the config file, which is where this feature really belongs anyway:

Host gateway
    HostName gateway.example.com
    User peter
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

Host db01
    HostName 10.0.0.5
    User peter
    ProxyJump gateway

After that, ssh db01 just works, and so does rsync -a backup/ db01:/srv/. Nobody has to remember the topology.

6.5 Connection Sharing: Making ssh Nearly Instant

Every new SSH connection costs a TCP handshake, a key exchange, and an authentication round trip. Over the internet that is a noticeable pause, and it is paid again for every git push, every rsync, and every command in a deployment script.

Connection multiplexing removes that cost. The first connection opens a control socket, and every later connection to the same host reuses the existing encrypted channel instead of building a new one.

Host *
    ControlMaster auto
    ControlPath ~/.ssh/control-%C
    ControlPersist 10m

ControlMaster auto means "reuse a master if there is one, otherwise become the master". ControlPath is where the socket lives. ControlPersist 10m keeps the master alive in the background for ten minutes after you log out, so the next command is instant too.

That %C deserves a note, because the obvious alternative causes trouble. You can spell the path out with %r, %h, and %p (remote user, host, and port), but a control socket is a Unix domain socket and its path cannot exceed roughly 100 characters. Long hostnames or a deep home directory push it over, and you get:

ControlPath too long ('/home/peter/.ssh/This email address is being protected from spambots. You need JavaScript enabled to view it.:22' >= 108 bytes)

%C is a hash of all of those values at once, so it is always the same short length and the problem cannot happen:

$ ls -l ~/.ssh/control-*
srw------- 1 peter peter 0 Aug 11 14:06 /home/peter/.ssh/control-7837652c3f95189960cc3a12f3cfe47d

Notice the permissions on that socket: srw-------, owner only. That is not decoration. Anyone who can open the control socket can ride your authenticated connection to the server without authenticating themselves, so the socket belongs in your own home directory and never in a shared location like /tmp.

The effect is easy to measure. Running the same trivial command twice, once without sharing and once through an existing master:

$ time ssh -o ControlPath=none server true
real    0m0.182s

$ time ssh server true
real    0m0.005s

That test ran against a server on the same machine, so nearly all of the 182 ms is cryptography rather than network. Across the internet the saving is far bigger, because the round trips disappear as well. A deployment script that opens fifty connections feels like a different tool.

You can inspect and control the master with -O (short for control command):

$ ssh -O check server
Master running (pid=74960)
$ ssh -O exit server
Exit request sent.

6.6 Keeping Sessions Alive

An idle SSH session often dies because a router or firewall in the middle drops connections it thinks are finished. The cure is a keepalive, and by default it is switched off on both sides: ServerAliveInterval in the client defaults to 0, and ClientAliveInterval in the server defaults to 0 as well.

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

That sends a small encrypted message every 60 seconds if nothing else is happening, and gives up after three unanswered ones. It fixes the classic "my session freezes when I go for coffee" problem, and it does so from your own config, without needing any change on the server.

6.7 The Server Side: Hardening sshd

Everything above is the client. The server is configured in /etc/ssh/sshd_config, and a handful of settings do most of the security work. The defaults are reasonable but not strict:

SettingDefaultRecommendedWhy
PasswordAuthentication yes no Removes brute-force and guessing attacks completely, once keys work
PermitRootLogin prohibit-password no Forces admins to log in as themselves and use sudo, which is also an audit trail
PubkeyAuthentication yes yes Leave it on; it is what you are switching to
MaxAuthTries 6 3 Cuts off guessing sooner and keeps the logs readable
LoginGraceTime 120 30 Fewer half-open connections waiting around
AllowUsers / AllowGroups unset set it Only named accounts can log in at all, whatever else is misconfigured
AllowTcpForwarding yes no where not needed Stops shell accounts from tunnelling into your internal network (see below)
PermitEmptyPasswords no no Already safe; verify it has not been changed

That forwarding row is the one administrators most often overlook, and section 6.1 to 6.3 explained why it matters. Give somebody an SSH account and you have not only given them a shell. You have given them the ability to reach anything the server can reach: the database on the private interface, the admin panel bound to loopback, the whole internal subnet through -D. Your firewall does not see it, because from the outside it is one ordinary SSH connection.

SSH access implies network access, not just shell access. Before you hand out an account, ask what that account could reach through a tunnel, not only what it could type.

For accounts that only need a shell, AllowTcpForwarding no closes the door. For finer control, restrict individual keys instead (section 8.2), or list exactly which destinations a key may reach with permitopen in authorized_keys.

Requiring more than one factor. For high-value systems, a key alone may not be enough. AuthenticationMethods requires several methods to all succeed, rather than accepting whichever one works first:

AuthenticationMethods publickey,keyboard-interactive

Read the comma as "and". That line demands a valid key and a second challenge, which is typically a one-time code through PAM. You can offer alternatives by separating whole lists with spaces, so publickey,keyboard-interactive publickey,password accepts either combination. Test this from a second terminal with particular care, because a half-configured PAM stack locks out keys that worked a minute earlier.

A hardware-backed key type such as ed25519-sk is often the simpler route to the same goal: the credential cannot be copied off the token, and you physically touch the device on every login.

Modern distributions ship a /etc/ssh/sshd_config.d/ directory that is included from the main file. Put your changes in a file there, such as /etc/ssh/sshd_config.d/99-hardening.conf, so a package update never overwrites them.

Always test the configuration before restarting, and always keep a second session open while you do it:

$ sudo sshd -t                    # syntax check; silence means OK
$ sudo systemctl reload ssh       # apply without dropping existing sessions

The keep a second session open habit is not optional. If you lock yourself out of a remote server with a bad SSH config, the only way back in may be a console through your hosting provider. Test the new setting from a fresh terminal before you close the one that still works.

One popular piece of advice deserves a caveat. Moving SSH to a non-standard port cuts your log noise dramatically, because most scanning robots only try port 22. It does not make the server meaningfully harder to attack, since a real attacker scans all ports anyway. Change the port for quieter logs if you like, but do not count it as security. Keys and PasswordAuthentication no are the change that matters.

Back to top

7. Under the Hood: What the Encryption Actually Does

You can use SSH for years without opening this box, and everything above works whether you read this section or not. But a surprising number of confusions disappear once you know what is really happening, so it is worth twenty minutes.

7.1 Three Kinds of Keys, and Why People Mix Them Up

The word key is used for three completely different things in SSH. Almost every muddled conversation about SSH security comes from mixing them up.

KindBelongs toLives inAnswers the question
Host key The server /etc/ssh/ssh_host_ed25519_key on the server, fingerprint cached in your known_hosts "Am I really talking to the machine I asked for?"
User key You ~/.ssh/id_ed25519 on your machine, public half in authorized_keys on the server "Is this person allowed in?"
Session keys This one connection Memory only, never written to disk, thrown away when you log out "What encrypts the actual bytes?"

The first two are long-lived and you manage them. The third kind is created fresh for every single connection and you never see it. When somebody says "SSH keys", they nearly always mean the second kind, but the warning in section 5.6 is about the first kind, and that is exactly why that warning confuses people.

7.2 The Handshake, Step by Step

Here is the order of events when you type ssh This email address is being protected from spambots. You need JavaScript enabled to view it.:

  1. The client resolves the hostname and opens a plain TCP connection to port 22.
  2. Both sides announce their version strings and their lists of supported algorithms.
  3. They agree on which algorithms to use: one for key exchange, one for the host key, one cipher, one integrity check. The winner is the first entry on the client's preference list that the server also supports, which is why keeping your client up to date improves the connection even to an older server.
  4. They run the key exchange, which produces shared session keys without either side ever sending them across the wire.
  5. The server signs part of that exchange with its host key, proving its identity. Your client checks the signature against known_hosts.
  6. Encryption switches on. Everything from here is protected.
  7. Only now do you authenticate, inside the encrypted channel.
  8. The session starts and the server launches your shell or your command.

Step 7 is the one worth remembering. Your credentials are sent after encryption is running, which is why even a plain password over SSH is not exposed to the network, and why SSH replaced telnet so completely. It also explains the order of the -v output: key exchange chatter first, authentication attempts second.

You can see the negotiated result on any connection:

$ ssh -v server.example 2>&1 | grep -E 'kex:|Server host key'
debug1: kex: algorithm: This email address is being protected from spambots. You need JavaScript enabled to view it.
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: This email address is being protected from spambots. You need JavaScript enabled to view it. MAC: <implicit> compression: none
debug1: Server host key: ssh-ed25519 SHA256:Cbs69zP2QMpFcfrRMSlg3fsDbEpOD+MH2FANbuWy5J8

And you can ask your client what it is willing to negotiate at all. The -Q flag (short for query) lists the algorithms your build supports:

$ ssh -Q kex        # key exchange methods
$ ssh -Q key        # key and certificate types
$ ssh -Q cipher     # symmetric ciphers

This is how you check a compliance requirement or confirm that an old algorithm really is gone, instead of guessing from a blog post.

7.3 Forward Secrecy: Why a Stolen Host Key Does Not Unlock the Past

Notice that the session keys in step 4 are generated fresh and never stored. That gives SSH a property called forward secrecy, and it is stronger than most people assume.

Suppose an attacker records your encrypted SSH traffic today and keeps it. Years later they break into the server and steal its private host key. They still cannot read the recorded sessions. The host key only signed the handshake, it never encrypted the data, and the keys that did encrypt it were discarded the moment you logged out.

The host key proves who you talked to. It does not protect what you said. That job belongs to session keys that no longer exist, which is why capturing SSH traffic for later decryption is a poor investment.

This is also the reason the post-quantum key exchange from section 3 matters so much. Forward secrecy holds as long as the key exchange itself cannot be broken later, so moving that step to a quantum-resistant algorithm protects conversations you are having today.

7.4 Certificates: Keys That Expire By Themselves

Everything so far copies public keys into authorized_keys, one file per user per server. With five servers that is fine. With two hundred servers and thirty engineers it becomes a filing problem, and the hard part is not adding access but removing it when somebody leaves.

OpenSSH solves this with certificates. You create one certificate authority key, and servers are told to trust it. The CA then signs user keys, and a signed key carries an identity, a list of principals, and an expiry date.

$ ssh-keygen -t ed25519 -f user_ca -C "user CA"          # once, kept very safe

$ ssh-keygen -s user_ca -I "peter@laptop" -n peter -V +1h ~/.ssh/id_ed25519.pub
Signed user key id_ed25519-cert.pub: id "peter@laptop" serial 0 for peter valid from 2026-08-11T14:04:00 to 2026-08-11T15:05:38

The flags read as a sentence: -s (short for sign) picks the CA key, -I (short for identity) labels who this is for the logs, -n lists the usernames the certificate may log in as, and -V (short for validity) sets the lifetime. You can inspect the result at any time:

$ ssh-keygen -Lf ~/.ssh/id_ed25519-cert.pub
        Type: This email address is being protected from spambots. You need JavaScript enabled to view it. user certificate
        Signing CA: ED25519 SHA256:f0JisVLECAQaZ8Zvusfs/hcR2RSfhKN3P0sHDRiCmFc
        Key ID: "peter@laptop"
        Valid: from 2026-08-11T14:04:00 to 2026-08-11T15:05:38
        Principals:
                peter
        Extensions:
                permit-pty
                permit-port-forwarding
                ...

On the server, one line in sshd_config replaces the whole filing system:

TrustedUserCAKeys /etc/ssh/user_ca.pub

That is the entire setup. With that in place, a login succeeds even though the user's authorized_keys file is completely empty, and the server log records which certificate was used and which CA vouched for it:

Accepted publickey for peter from 203.0.113.55 port 52318 ssh2: ED25519-CERT SHA256:cSoLJ2nLw74r... ID peter@laptop (serial 0) CA ED25519 SHA256:f0JisVLECAQaZ8Zvusfs/hcR2RSfhKN3P0sHDRiCmFc

And when the hour is up, the key stops working everywhere at once, with no cleanup to forget:

$ ssh This email address is being protected from spambots. You need JavaScript enabled to view it.
This email address is being protected from spambots. You need JavaScript enabled to view it.: Permission denied (publickey).

# in the server log:
Certificate invalid: expired

The same idea works in the other direction. A host certificate signed by a CA that your clients trust removes the trust-on-first-use question from section 4.2 entirely: new servers are recognised immediately, and no one has to compare fingerprints by hand. The trade-off is real, though. The CA private key becomes the most valuable secret you own, because it can vouch for anyone, so it belongs offline or in hardware rather than on a build server.

Back to top

8. Something Most Users Do Not Know

8.1 The Escape Character: A Hidden Command Line Inside Your Session

Here is the detail that surprises almost everyone, including people who have used SSH for years. Inside an interactive session, ssh watches for a secret escape character: a tilde (~) typed at the start of a line. It never reaches the server. It talks to your local ssh client.

The most useful one solves a problem everybody has had. Your connection freezes, the server is gone, and Ctrl-C does nothing because Ctrl-C is being sent into a tunnel that no longer works. Press Enter, then type ~. and the client hangs up immediately, instead of waiting for a server that will never answer.

There are more of them, and the client will list them for you. Press Enter, then type ~?:

Supported escape sequences:
 ~.   - terminate connection (and any multiplexed sessions)
 ~B   - send a BREAK to the remote system
 ~R   - request rekey
 ~V/v - decrease/increase verbosity (LogLevel)
 ~^Z  - suspend ssh
 ~#   - list forwarded connections
 ~&   - background ssh (when waiting for connections to terminate)
 ~?   - this message
 ~~   - send the escape character by typing it twice
(Note that escapes are only recognized immediately after newline.)
commandline disabled

That last line points at the most interesting escape of all, and it explains why you may never have seen it work. ~C opens a small command line inside your session where you can add a port forward without logging out and reconnecting with -L. It used to be on by default, but OpenSSH 9.2 switched it off so the client could run under a stricter sandbox, so today you have to ask for it:

Host *
    EnableEscapeCommandline yes

With that in your config, you are halfway through a session, you realise you need a tunnel to the database, and you simply add one. Press Enter, type ~C, and you get a prompt:

ssh> -L 3307:127.0.0.1:3306
Forwarding port.

You can check the result without leaving either, with ~#:

The following connections are open:
  #0 client-session (t4 [session] r0 i0/1 o0/0 e[write]/4 fd 4/5/6 sock -1 cc -1 io 0x01/0x01)

The rule that catches people is in the note at the bottom of that help text: the tilde only counts immediately after a newline. If nothing happens, press Enter first and try again. And if you are nested two levels deep (an ssh from inside an ssh), double the tilde: ~~. reaches the second client instead of the first.

8.2 Keys That Can Only Do One Thing

An authorized_keys line does not have to grant a full shell. You can prefix it with options that restrict exactly what that key is allowed to do, and this is how professionals set up automated access.

command="/usr/local/bin/backup.sh",no-port-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3Nza... backup@ci

The command= option is a forced command: whatever the client asks for is thrown away and this script runs instead. Watch what happens when a key restricted like that tries to do something else:

$ ssh -i backup_key This email address is being protected from spambots. You need JavaScript enabled to view it. 'rm -rf /'
RESTRICTED: only the backup script runs

The dangerous command never runs. It was never even considered. And the forwarding restrictions are enforced by the server, not requested politely from the client:

$ ssh -i backup_key -N -L 9096:127.0.0.1:8099 This email address is being protected from spambots. You need JavaScript enabled to view it.
channel 2: open failed: administratively prohibited: open failed

Two more options are worth knowing. from="203.0.113.0/24" restricts a key to connections from specific addresses, and restrict switches off every optional feature at once, so you can add back only what you need:

restrict,command="/usr/local/bin/backup.sh",from="203.0.113.10" ssh-ed25519 AAAAC3Nza... backup@ci

This is how a backup server, a deployment pipeline, or a monitoring job gets exactly the access it needs and nothing more. If that key ever leaks, the attacker inherits the ability to run one script from one address.

8.3 Why Agent Forwarding Is a Trap, and What to Use Instead

Sooner or later someone suggests -A, agent forwarding, so your keys "follow you" from one server to the next. It works, and the manual page itself warns against it.

When you forward your agent, a socket appears on the remote machine that can ask your local agent to sign things. Anyone on that server who can reach the socket, including root and anyone who has compromised the box, can use your keys for as long as you stay connected. They cannot steal the key material, but they can authenticate as you to every server your keys open, and you will not see it happen.

Agent forwarding hands a remote machine the ability to act as you. A jump host does not: with -J, the middle machine only forwards encrypted bytes it cannot read, and your key never signs anything for it.

Use ProxyJump for reaching machines through a gateway, and ssh-add -c (short for confirm) if you truly need forwarding, so every single use of the key requires you to approve it on your own screen.

8.4 Knowing Where ssh Stops

Part of expertise is knowing which tool takes over. ssh is the foundation, not the answer to everything built on it.

NeedUseWhy
Copy a file once scp or sftp Both run over SSH; since OpenSSH 9.0 scp uses the SFTP protocol underneath
Copy the same tree repeatedly rsync Transfers only what changed, over the same SSH connection
Survive a dropped connection tmux or screen Runs on the server, so your work continues when the session dies
Work over a bad mobile link mosh Uses SSH to log in, then a roaming UDP protocol that survives IP changes
Configure many servers at once ansible Drives SSH for you, in parallel, with a record of what it changed
Full network access, permanently WireGuard or another VPN -D is a fine ad-hoc proxy, but a VPN is the real answer for daily use
Manage keys for a large team SSH certificates Sign short-lived keys with a CA instead of copying authorized_keys to every host

That last row is the one growing teams reach eventually. Once you have twenty servers and ten engineers, distributing public keys by hand stops scaling, and OpenSSH's built-in certificate support lets one trusted CA key vouch for everyone, with an expiry date attached.

Back to top

9. Best Practices

  • Use Ed25519 keys with a passphrase, and an agent. ssh-keygen -t ed25519 once, ssh-add once a day, and you get better security and less typing at the same time.
  • Turn off password authentication once keys work. Check the server log for Accepted publickey first, keep a second session open, and only then set PasswordAuthentication no.
  • Put everything in ~/.ssh/config. Hosts, users, ports, keys, jump hosts, keepalives. Remember that the first matching value wins, so specific blocks go at the top.
  • Never share or copy a private key. One key pair per machine you type on. If a laptop is lost, you remove one public key from authorized_keys instead of rotating everything.
  • Read the host key warning before you clear it. Expected after a reinstall, alarming at any other time. This is the one warning that is worth ten minutes of your attention.
  • Restrict automated keys. Any key used by a script or a CI system deserves restrict, a forced command=, and a from= address list.
  • Prefer ProxyJump over agent forwarding. Same convenience, none of the risk.
  • Ask what an account can reach, not just what it can type. SSH access is network access. Set AllowTcpForwarding no for accounts that do not need tunnels.
  • Once key files become a filing problem, move to certificates. One trusted CA, short validity periods, and access that expires on its own instead of waiting for someone to remember it.
  • Turn on connection sharing. ControlMaster auto with ControlPersist makes every SSH-based tool you own feel faster for the cost of four lines of config.
  • Keep OpenSSH updated. Cipher and key-exchange defaults improve with every release, and you inherit those improvements for free.
  • Reach for the documentation. The manual pages are excellent and cover far more than any article can.
$ man ssh              # the client
$ man ssh_config       # every client option, including the config file
$ man sshd_config      # every server option
$ man ssh-keygen       # keys, fingerprints, known_hosts, certificates
$ ssh -G server        # what ssh will actually do for this host
Back to top

10. Common Mistakes

10.1 Common Myths

MythReality
"Moving SSH to port 2222 secures the server." It reduces log noise from automated scanners. A real attacker scans every port. Keys and disabled passwords are the actual defence.
"A key without a passphrase is fine, the file is on my laptop." That file is the credential. Anyone who copies it owns every server it opens, with no second factor at all.
"I have to paste my private key onto the server." Only the .pub file ever leaves your machine. If a form or a colleague asks for the other file, something is wrong.
"Agent forwarding (-A) is just a convenience." It lets anyone with root on that server authenticate as you elsewhere while you are connected. Use -J instead.
"The host key warning is a bug in ssh." It is ssh doing its single most important job. Clear it only when you know why the key changed.
"ssh host 'echo $HOME' shows my local home directory." Single quotes send the command through unexpanded, so the server's shell expands it. Double quotes expand it locally first.

10.2 Traps to Avoid

  • Too many keys in the agent. ssh offers agent keys before the one you named with -i, and the server refuses the connection once MaxAuthTries is reached:
    Received disconnect from 203.0.113.10 port 22:2: Too many authentication failures
    The fix is IdentitiesOnly yes in the host's config block, which makes ssh use only the key you specified.
  • Wrong permissions. chmod 600 the private key and authorized_keys, chmod 700 the ~/.ssh directory. A world-readable private key is silently ignored, and a group-writable home directory on the server can make key authentication fail with no useful message.
  • Editing sshd_config with no way back. Run sshd -t to check the syntax, reload instead of restart, and never close your working session until a new one succeeds.
  • Confusing -p and -P. Lowercase for ssh, uppercase for scp. It has caught everyone.
  • Forgetting which side -L resolves. In -L 9099:127.0.0.1:8099, the middle address is resolved on the server. localhost there means the server, not you.
  • Copying a key pair to every machine. One key per device. Rotating a leaked key that lives in six places is a bad afternoon.
  • Leaving passwords enabled "just in case". That case is exactly what the scanners are counting on. Set up a console fallback with your hosting provider instead.
  • Assuming an idle session will survive. Without ServerAliveInterval, a firewall in the middle will eventually drop it. Long jobs belong in tmux anyway.
Back to top

11. Summary

The ssh command looks like a way to get a terminal on another machine, and it is. It is also the encrypted tunnel that most of modern server administration runs through.

  • ssh means Secure SHell. It replaced telnet, rlogin, and rsh, which sent passwords in plain text, and it uses port 22 because that sat between ftp and telnet.
  • Tatu Ylönen wrote it in 1995 after a password-sniffing attack in Finland. The OpenBSD team forked the last free version into OpenSSH in 1999, and the SSH-2 protocol became an IETF standard in 2006.
  • Every connection does three things: verifies the server's host key, authenticates you, and encrypts everything. The first-connection question and known_hosts are how the first of those works.
  • Public key authentication beats passwords: ssh-keygen -t ed25519, ssh-copy-id, then PasswordAuthentication no on the server. Always set a passphrase and let ssh-agent hold the key.
  • ~/.ssh/config turns long commands into short aliases and is read by scp, rsync, and git too. The first matching value wins, so specific hosts go at the top.
  • -L brings a remote port to you, -R hands a local port out, and -D turns ssh into a SOCKS proxy for a whole private network.
  • -J (ProxyJump) reaches machines behind a bastion, end to end encrypted, and is the safe replacement for agent forwarding.
  • ControlMaster with ControlPersist reuses one connection for many sessions and makes every SSH-based tool noticeably faster.
  • A tilde at the start of a line talks to your local client, not the server: ~. kills a frozen session, ~? lists the rest, and ~C adds a port forward without reconnecting once you set EnableEscapeCommandline yes.
  • In authorized_keys, restrict, command=, and from= reduce a key to exactly one job from exactly one place.
  • Three different things are called a key: the server's host key, your user key, and the throwaway session keys that do the actual encrypting. Because the session keys are discarded, a host key stolen later cannot decrypt traffic recorded today.
  • SSH certificates replace copying public keys everywhere: one CA signs a key with an identity and an expiry date, servers trust the CA through TrustedUserCAKeys, and access ends by itself.
  • Giving somebody SSH access gives them network access to everything the server can reach. AllowTcpForwarding no and permitopen put limits on that.
  • On the server, PasswordAuthentication no, PermitRootLogin no, and AllowUsers do most of the work. Test with sshd -t and keep a second session open.
  • When in doubt, run ssh -v to see what happens, or ssh -G host to see what would happen.

This is the quick reference worth keeping:

ssh user@host                      log in
ssh -p 2222 user@host              non-standard port
ssh user@host 'uptime'             run one command and exit
ssh -v user@host                   show what is going wrong
ssh -G host                        show the resolved config, do not connect

ssh-keygen -t ed25519 -C "me@laptop"   create a key pair
ssh-copy-id user@host                  install the public key
ssh-add -t 8h ~/.ssh/id_ed25519        load it into the agent for 8 hours
ssh-keygen -lf ~/.ssh/id_ed25519.pub   show a key fingerprint
ssh-keygen -p -f ~/.ssh/id_ed25519     add or change the passphrase
ssh-keygen -R host                     forget a changed host key
ssh-keygen -s ca -I who -n user -V +8h key.pub   sign a short-lived certificate
ssh-keygen -Lf key-cert.pub            inspect a certificate

ssh -f -N -L 3307:127.0.0.1:3306 host  remote database on your local port 3307
ssh -f -N -R 9097:127.0.0.1:3000 host  your local app on the server's port 9097
ssh -f -N -D 9098 host                 SOCKS proxy through the server
ssh -J user@gateway user@internal      reach a host behind a bastion
ssh -O check host                      is a shared master connection running?

~.     disconnect a frozen session      (press Enter first)
~#     list the forwards on this connection
~?     list the escape sequences
~C     add a port forward mid-session   (needs EnableEscapeCommandline yes)

SSH is the one command where a small amount of extra knowledge pays back every single day: fewer passwords, faster deployments, and a server that stops being interesting to the robots scanning it.

Back to top
Linux command: ssh
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.