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
- What SSH is
- Passwords are off, and that's good
- Keys: a lock and its key
- Shortcuts: ~/.ssh/config
- When it doesn't work
- Hardening the SSH server
- Common mistakes
- Practice
- 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 noThat'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:
- The private key stays on your computer, readable only by you. It's like your house key.
- The public key goes on every server you want to log in to, in
~/.ssh/authorized_keys. It's like the lock. Sharing it is safe.
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@linuxPutting 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:
- On your laptop, make a key if you don't have one:
ssh-keygen -t ed25519. - Print the public key:
cat ~/.ssh/id_ed25519.pub(on Windows, in PowerShell,type $env:USERPROFILE\.ssh\id_ed25519.pub). - In the VM, add that line to
~/.ssh/authorized_keyswithecho "ssh-ed25519 AAAA... you@laptop" >> ~/.ssh/authorized_keys. - 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.05Try it
- Run
df -h /as Ravi over SSH, without opening a shell. - Which key types does
ssh-keygensupport? (Checkman ssh-keygenfor-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 noPut 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
| Setting | Does |
|---|---|
PermitRootLogin no | Nobody logs in as root directly. Admins log in as themselves and use sudo |
PasswordAuthentication no | Keys only |
MaxAuthTries 3 | Disconnect after three failed attempts |
AllowUsers ubuntu ravi | Only 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
- Show the effective value of
X11ForwardingandPubkeyAuthentication. - Add
ClientAliveInterval 300to 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
- Locking yourself out. Test with
sudo sshd -t, keep a session open, and try logging in from a second terminal before you close the first. - **Wrong permissions on
~/.ssh.** It's700,authorized_keysis600, and both belong to the user. The server log saysbad ownership or modes. - **Editing
sshd_configwhile a drop-in sets the same option.** The first value wins, andsshd_config.dis read first. Check withsshd -T. - Sharing the private key. Only
id_ed25519.pubever leaves your machine. - Accepting a changed host key without thinking. Find out why it changed first.
- Changing the port without opening it in the firewall first.
Practice
- Make a second key pair called
~/.ssh/deploy_key(no passphrase, commentdeploy) 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.
- Count how many keys Ravi's
authorized_keysnow 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.
- Add a
deployentry to~/.ssh/configthat logs in as Ravi withdeploy_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
- 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.
- 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+fqwUU4huwCheat sheet
| Command | What it does |
|---|---|
ssh user@host, ssh -p 2222 user@host | Connect (on another port) |
ssh user@host 'cmd' | Run one command remotely |
ssh-keygen -t ed25519 -C "comment" | Make a key pair |
~/.ssh/id_ed25519, .pub | Private key (never share), public key |
ssh-copy-id user@host | Install your key (needs password login) |
~/.ssh/authorized_keys | Keys allowed to log in (600, in a 700 ~/.ssh) |
~/.ssh/known_hosts, ssh-keygen -R host | Trusted servers, forget one |
~/.ssh/config | Short names and per-host options |
ssh -v, -o BatchMode=yes | Show every step, never ask |
sudo sshd -t | Check the server config |
sudo sshd -T | Show the settings it really uses |
/etc/ssh/sshd_config.d/10-*.conf | Your settings (first value wins) |
sudo systemctl reload ssh | Apply config changes |
sudo systemctl daemon-reload && sudo systemctl restart ssh.socket | After changing the port |
sudo journalctl -u ssh | The server's log |
← Network basics: ip, static IPs and troubleshootingMoving files over the network: scp, rsync and wget →