Nandishwar Singh

Home / Learn Linux / Lesson 19

SSH: connecting, keys and a safer server

Log in to another machine, switch from passwords to keys, and lock down the SSH server.

Lesson
19 of 30
Time
About 50 minutes

On this page

  1. What SSH is
  2. Passwords are off, and that's good
  3. Keys: a lock and its key
  4. Shortcuts: ~/.ssh/config
  5. When it doesn't work
  6. Hardening the SSH server
  7. Common mistakes
  8. Practice
  9. Cheat sheet

What SSH is

SSH (Secure Shell) gives you a terminal on another machine, encrypted end to end. It's how you'll manage every server you ever run: the server runs an SSH server (sshd) listening on port 22, and you connect with the ssh client:

ssh user@host

You've been using it all along: multipass shell linux is SSH underneath. In this lesson you'll connect between users on your VM, which works exactly like connecting between two machines, so everything here carries over to real servers.

Ubuntu's SSH server is already running:

$ systemctl is-active ssh.socket
active
$ ssh -V
OpenSSH_10.2p1 Ubuntu-2ubuntu3.8, OpenSSL 3.5.5 27 Jan 2026

Passwords are off, and that's good

Try logging in to your own VM as ravi (the user from lesson 13) with his password:

$ ssh ravi@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ED25519 key fingerprint is: SHA256:ZVOScnETK5a0XrLkGdWrXwgnybL4hn7wEVwfoimHCa8
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.
ravi@localhost: Permission denied (publickey).

Two things happened. First, SSH asked whether you trust this server. Every SSH server has its own host key, and the first time you connect, you're shown its fingerprint. After you say yes, it's saved in ~/.ssh/known_hosts, and SSH checks it on every later visit, so nobody can quietly put a fake server in the way.

Second, the login was refused, and the reason is in the message: (publickey). This server only accepts keys. Ubuntu's cloud images switch passwords off in a small extra config file:

$ cat /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
PasswordAuthentication no

That's the right setting for any server on the internet. Bots try millions of passwords against every public SSH server every day, and a key can't be guessed.

Keys: a lock and its key

An SSH key comes in two halves:

When you connect, the server checks that you hold the private key that matches one of its locks, without the private key ever leaving your machine.

Make a key pair with ssh-keygen. -t ed25519 picks the modern key type, and -C adds a comment so you can tell keys apart later:

$ ssh-keygen -t ed25519 -C "ubuntu@linux"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/ubuntu/.ssh/id_ed25519):
Enter passphrase for "/home/ubuntu/.ssh/id_ed25519" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/ubuntu/.ssh/id_ed25519
Your public key has been saved in /home/ubuntu/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:0kCWjQenHOCsZ5UhBppgptwyu0kD59+XtDKjfSqHfqE ubuntu@linux
The key's randomart image is:
+--[ED25519 256]--+
|.o..+.*=.        |
|*o.+ =o*o        |
|=+..o *.         |
|.o+. . o         |
| +o o . S        |
|. ++ ..o o       |
| o  .o=.+        |
|    Eoo=.        |
|   .o+oo         |
+----[SHA256]-----+
$ ls -l ~/.ssh
total 16
-rw------- 1 ubuntu ubuntu 180 Oct 21 11:32 authorized_keys
-rw------- 1 ubuntu ubuntu 399 Oct 21 12:25 id_ed25519
-rw-r--r-- 1 ubuntu ubuntu  94 Oct 21 12:25 id_ed25519.pub
-rw-r--r-- 1 ubuntu ubuntu 142 Oct 21 12:25 known_hosts

It asked where to save the key (Enter accepts ~/.ssh/id_ed25519) and for a passphrase twice. Here, on a practice VM, we pressed Enter for no passphrase. On your own laptop, always set one: it encrypts the private key, so a stolen laptop doesn't mean stolen servers.

Look at the permissions: the private key is rw-------, the public key rw-r--r--. SSH refuses to use a private key anyone else can read.

The public key is a single line of text:

$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM9hbVOqAknSxBxFTeWaOVXMoQztYX73BUbCqIAi1iCW ubuntu@linux

Putting the lock on the server

On a server, the public key goes into the user's ~/.ssh/authorized_keys, and the permissions must be strict: ~/.ssh is 700 and authorized_keys is 600, both owned by that user. As admin, set it up for Ravi:

$ sudo mkdir -p /home/ravi/.ssh
$ cat ~/.ssh/id_ed25519.pub | sudo tee -a /home/ravi/.ssh/authorized_keys > /dev/null
$ sudo chown -R ravi:ravi /home/ravi/.ssh
$ sudo chmod 700 /home/ravi/.ssh
$ sudo chmod 600 /home/ravi/.ssh/authorized_keys
$ sudo ls -la /home/ravi/.ssh
total 12
drwx------ 2 ravi ravi 4096 Oct 21 12:25 .
drwxr-x--- 3 ravi ravi 4096 Oct 21 12:25 ..
-rw------- 1 ravi ravi   94 Oct 21 12:25 authorized_keys

Now log in again:

ubuntu@linux:~$ ssh ravi@localhost
Welcome to Ubuntu 26.04.1 LTS (GNU/Linux 7.0.0-28-generic aarch64)

 * Documentation:  https://docs.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Wed Oct 21 12:25:07 UTC 2026

  System load:  0.01               Processes:             114
  Usage of /:   14.3% of 18.33GB   Users logged in:       0
  Memory usage: 13%                IPv4 address for eth0: 192.168.5.15
  Swap usage:   0%

  => There is 1 zombie process.


Expanded Security Maintenance for Applications is not enabled.

11 updates can be applied immediately.
11 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status



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

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

ravi@linux:~$ whoami
ravi
ravi@linux:~$ hostname
linux
ravi@linux:~$ exit
logout
Connection to localhost closed.

No password, no question. On a server where password logins are still allowed, like a brand-new VPS, ssh-copy-id user@host does the copying for you: it logs in with the password once and adds your public key. Lesson 30 uses it.

From your own computer to the VM

The same steps let you log in from your laptop instead of using multipass shell:

  1. On your laptop, make a key if you don't have one: ssh-keygen -t ed25519.
  2. Print the public key: cat ~/.ssh/id_ed25519.pub (on Windows, in PowerShell, type $env:USERPROFILE\.ssh\id_ed25519.pub).
  3. In the VM, add that line to ~/.ssh/authorized_keys with echo "ssh-ed25519 AAAA... you@laptop" >> ~/.ssh/authorized_keys.
  4. Find the VM's address with multipass list, then from your laptop: ssh ubuntu@192.168.64.5 (with your VM's address).

Running one command

Put a command after the address and SSH runs it on the other side and comes straight back. That's how scripts manage many servers:

$ ssh ravi@localhost 'whoami; uptime'
ravi
 12:25:08 up  1:19,  2 users,  load average: 0.01, 0.05, 0.05

Try it

  1. Run df -h / as Ravi over SSH, without opening a shell.
  2. Which key types does ssh-keygen support? (Check man ssh-keygen for -t.)
Answer
$ ssh ravi@localhost df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        19G  2.7G   16G  15% /

ed25519 is the one to use. rsa (with at least 3072 bits) is the fallback for very old servers, and ecdsa also exists.

Shortcuts: ~/.ssh/config

Typing ssh -p 2222 -i ~/.ssh/work_key admin@203.0.113.10 every time gets old. ~/.ssh/config gives each server a short name:

$ cat > ~/.ssh/config << EOF
> Host lab
>     HostName localhost
>     User ravi
>     IdentityFile ~/.ssh/id_ed25519
> EOF
$ chmod 600 ~/.ssh/config
$ ssh lab hostname
linux

Now ssh lab does it all. Other useful options per host are Port 2222 and ServerAliveInterval 60, which keeps idle connections from dropping. scp and rsync (lesson 20) use the same names.

When it doesn't work

Ask SSH what it's doing: -v

ssh -v prints every step of the connection. The useful lines are near the end: which keys it offered and what the server said:

$ ssh -v ravi@localhost true 2>&1 | grep -E "Offering|Server accepts|Authenticated"
debug1: Offering public key: /home/ubuntu/.ssh/id_ed25519 ED25519 SHA256:0kCWjQenHOCsZ5UhBppgptwyu0kD59+XtDKjfSqHfqE
debug1: Server accepts key: /home/ubuntu/.ssh/id_ed25519 ED25519 SHA256:0kCWjQenHOCsZ5UhBppgptwyu0kD59+XtDKjfSqHfqE
Authenticated to localhost ([127.0.0.1]:22) using "publickey".

Permissions

The most common problem is a key that's set up right but refused, and the cause is nearly always permissions. SSH ignores authorized_keys if the user's home, ~/.ssh or the file itself can be written by anyone else, because then someone else could have added their own key. Break it on purpose:

$ sudo chmod 777 /home/ravi/.ssh
$ ssh -o BatchMode=yes ravi@localhost true
ravi@localhost: Permission denied (publickey).
$ sudo journalctl -u ssh --since "1 min ago" --no-pager | grep -i "bad ownership" | tail -n 1
Oct 21 12:25:09 linux sshd-session[195514]: Authentication refused: bad ownership or modes for directory /home/ravi/.ssh
$ sudo chmod 700 /home/ravi/.ssh
$ ssh ravi@localhost true && echo fixed
fixed

The client only says Permission denied. The server's log (lesson 26 covers journalctl) says why. -o BatchMode=yes stops SSH from asking for anything, so it fails straight away instead of waiting.

"REMOTE HOST IDENTIFICATION HAS CHANGED"

If a server's host key doesn't match the one in your known_hosts, SSH refuses to connect and prints a big warning. It means either the server was reinstalled (common: you rebuilt a VM or VPS at the same address) or someone is intercepting the connection. Here's what it looks like, faked by putting a wrong key in known_hosts:

$ ssh ravi@localhost true
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    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:ZVOScnETK5a0XrLkGdWrXwgnybL4hn7wEVwfoimHCa8.
Please contact your system administrator.
Add correct host key in /home/ubuntu/.ssh/known_hosts to get rid of this message.
Offending ED25519 key in /home/ubuntu/.ssh/known_hosts:1
  remove with:
  ssh-keygen -f '/home/ubuntu/.ssh/known_hosts' -R 'localhost'
Host key for localhost has changed and you have requested strict checking.
Host key verification failed.

If you know the server was rebuilt, remove the old key and connect again, which shows you the new fingerprint to accept:

$ ssh-keygen -R localhost
# Host localhost found: line 1
/home/ubuntu/.ssh/known_hosts updated.
Original contents retained as /home/ubuntu/.ssh/known_hosts.old
$ ssh ravi@localhost true
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ED25519 key fingerprint is: SHA256:ZVOScnETK5a0XrLkGdWrXwgnybL4hn7wEVwfoimHCa8
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.

If you didn't rebuild it, don't accept the new key until you've found out why it changed.

Hardening the SSH server

The server's settings are in /etc/ssh/sshd_config, and Ubuntu also reads every .conf file in /etc/ssh/sshd_config.d/. One rule decides which setting wins: for each option, the first value sshd reads is the one it uses. The main file includes sshd_config.d on its very first line, so a setting in a drop-in file beats the same setting further down sshd_config. That's why changing PasswordAuthentication in the main file seems to do nothing on a cloud image.

Ask the server what it's really using with sshd -T:

$ sudo sshd -T | grep -E "^(port|permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries) "
port 22
maxauthtries 6
permitrootlogin prohibit-password
pubkeyauthentication yes
passwordauthentication no

Put your own settings in a new drop-in file whose name sorts first, so it wins. These are the standard hardening steps:

$ sudo tee /etc/ssh/sshd_config.d/10-hardening.conf > /dev/null << EOF
> PermitRootLogin no
> PasswordAuthentication no
> MaxAuthTries 3
> AllowUsers ubuntu ravi
> EOF
$ sudo sshd -t && echo "config OK"
config OK
$ sudo systemctl reload ssh
$ sudo sshd -T | grep -E "^(permitrootlogin|maxauthtries|allowusers) "
maxauthtries 3
permitrootlogin no
allowusers ubuntu
allowusers ravi
SettingDoes
PermitRootLogin noNobody logs in as root directly. Admins log in as themselves and use sudo
PasswordAuthentication noKeys only
MaxAuthTries 3Disconnect after three failed attempts
AllowUsers ubuntu raviOnly these users may log in over SSH at all

sudo sshd -t checks the configuration and prints nothing if it's fine. Always run it before reloading, and keep your current SSH session open while you test a new one in a second terminal. If you break the config and lock yourself out of a remote server, the open session is your way back in.

Check that AllowUsers works by taking Ravi off the list:

$ sudo sed -i 's/^AllowUsers .*/AllowUsers ubuntu/' /etc/ssh/sshd_config.d/10-hardening.conf
$ sudo systemctl reload ssh
$ ssh -o BatchMode=yes ravi@localhost true
ravi@localhost: Permission denied (publickey).
$ sudo sed -i 's/^AllowUsers .*/AllowUsers ubuntu ravi/' /etc/ssh/sshd_config.d/10-hardening.conf
$ sudo systemctl reload ssh
$ ssh ravi@localhost true && echo "ravi is back"
ravi is back

Changing the port

Moving SSH off port 22 cuts down the noise from bots in your logs. It isn't real security (scanners find any port), but it's common. On Ubuntu, SSH is started through systemd socket activation: systemd listens on the port and starts sshd when someone connects. So changing Port takes one more step than editing the file:

$ echo "Port 2222" | sudo tee /etc/ssh/sshd_config.d/20-port.conf
$ sudo sshd -t
$ sudo systemctl daemon-reload
$ sudo systemctl restart ssh.socket
$ ssh -p 2222 user@host

daemon-reload makes systemd read the new port from the SSH config, and restarting ssh.socket moves the listener. If a firewall is running (lesson 30), open the new port before you do this, or you'll be locked out.

Try it

  1. Show the effective value of X11Forwarding and PubkeyAuthentication.
  2. Add ClientAliveInterval 300 to your hardening file, check the config, reload, and confirm sshd picked it up.
Answer
$ sudo sshd -T | grep -E "^(x11forwarding|pubkeyauthentication) "
pubkeyauthentication yes
x11forwarding yes
$ echo "ClientAliveInterval 300" | sudo tee -a /etc/ssh/sshd_config.d/10-hardening.conf
ClientAliveInterval 300
$ sudo sshd -t && sudo systemctl reload ssh
$ sudo sshd -T | grep -i "^clientaliveinterval"
clientaliveinterval 300

Common mistakes

Practice

  1. Make a second key pair called ~/.ssh/deploy_key (no passphrase, comment deploy) and install its public key for Ravi as well. Then log in with it explicitly using -i.
Answer
$ ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -C deploy -N ""
Generating public/private ed25519 key pair.
Your identification has been saved in /home/ubuntu/.ssh/deploy_key
Your public key has been saved in /home/ubuntu/.ssh/deploy_key.pub
The key fingerprint is:
SHA256:T2l2fvZz4KRJtvekGtYLWlW6CIuB6x806+fqwUU4huw deploy
The key's randomart image is:
+--[ED25519 256]--+
|                 |
|     . . .       |
|      o + .     .|
|     . o o .   o |
|      E S B . o  |
|       + @ =o+o. |
|      . * ooB*=..|
|     . . o.++=o*.|
|      .o=+o .oo.=|
+----[SHA256]-----+
$ cat ~/.ssh/deploy_key.pub | sudo tee -a /home/ravi/.ssh/authorized_keys > /dev/null
$ ssh -i ~/.ssh/deploy_key ravi@localhost 'echo logged in with deploy_key'
logged in with deploy_key

-N "" sets an empty passphrase without asking. Deployment tools often use a separate key like this, so it can be removed without touching your own.

  1. Count how many keys Ravi's authorized_keys now holds, and show just their comments.
Answer
$ sudo wc -l /home/ravi/.ssh/authorized_keys
2 /home/ravi/.ssh/authorized_keys
$ sudo awk '{print $NF}' /home/ravi/.ssh/authorized_keys
ubuntu@linux
deploy

$NF is the last field of each line, the comment.

  1. Add a deploy entry to ~/.ssh/config that logs in as Ravi with deploy_key, and use it.
Answer
$ printf "\nHost deploy\n    HostName localhost\n    User ravi\n    IdentityFile ~/.ssh/deploy_key\n" >> ~/.ssh/config
$ ssh deploy whoami
ravi
  1. Revoke the deploy key: remove its line from Ravi's authorized_keys, and show that logging in with it now fails.
Answer
$ sudo sed -i '/ deploy$/d' /home/ravi/.ssh/authorized_keys
$ ssh -o BatchMode=yes -o IdentitiesOnly=yes -i ~/.ssh/deploy_key ravi@localhost true
ravi@localhost: Permission denied (publickey).

IdentitiesOnly=yes makes SSH use only that key, instead of also trying your default one.

  1. Show the last three SSH logins the server accepted, from its log.
Answer
$ sudo journalctl -u ssh --no-pager | grep "Accepted publickey" | tail -n 3
Oct 21 12:25:12 linux sshd-session[195963]: Accepted publickey for ravi from 127.0.0.1 port 58492 ssh2: ED25519 SHA256:0kCWjQenHOCsZ5UhBppgptwyu0kD59+XtDKjfSqHfqE
Oct 21 12:25:12 linux sshd-session[196208]: Accepted publickey for ravi from 127.0.0.1 port 58500 ssh2: ED25519 SHA256:T2l2fvZz4KRJtvekGtYLWlW6CIuB6x806+fqwUU4huw
Oct 21 12:25:13 linux sshd-session[196425]: Accepted publickey for ravi from 127.0.0.1 port 58512 ssh2: ED25519 SHA256:T2l2fvZz4KRJtvekGtYLWlW6CIuB6x806+fqwUU4huw

Cheat sheet

CommandWhat it does
ssh user@host, ssh -p 2222 user@hostConnect (on another port)
ssh user@host 'cmd'Run one command remotely
ssh-keygen -t ed25519 -C "comment"Make a key pair
~/.ssh/id_ed25519, .pubPrivate key (never share), public key
ssh-copy-id user@hostInstall your key (needs password login)
~/.ssh/authorized_keysKeys allowed to log in (600, in a 700 ~/.ssh)
~/.ssh/known_hosts, ssh-keygen -R hostTrusted servers, forget one
~/.ssh/configShort names and per-host options
ssh -v, -o BatchMode=yesShow every step, never ask
sudo sshd -tCheck the server config
sudo sshd -TShow the settings it really uses
/etc/ssh/sshd_config.d/10-*.confYour settings (first value wins)
sudo systemctl reload sshApply config changes
sudo systemctl daemon-reload && sudo systemctl restart ssh.socketAfter changing the port
sudo journalctl -u sshThe server's log