Connecting with SSH

Keys, known_hosts, config and debugging.

Beginner14 min · lesson 25 of 29

SSH is how you reach a Linux server: an encrypted connection that gives you a shell on the remote machine, runs single commands there and copies files. This lesson covers the side you use every day, the client: the host key question on a first connection and why it matters, logging in with a key instead of a password, the agent that saves you from retyping a passphrase, a ~/.ssh/config file that shortens every command, copying files with scp and rsync, and reading the errors when a connection fails. The examples connect from web01 to a second server, app01 at 192.0.2.30, which the lab simulates on the same virtual machine, as the account ess-ssh-app.

If you have typed the earlier lessons at your practice machine's console, this is where you move to SSH, which is how servers are normally run: at the end of the lesson you log in to the practice machine from your own computer. If you have been connecting with ssh from the start, this lesson explains what that command was doing.

The first connection and the host key

Every SSH server has its own key pair, the host key, and proves its identity with it on every connection. The first time you connect, your client has nothing to compare the key with, so it shows you the key's fingerprint, a short hash of it, and asks. Get the fingerprint through another channel before you answer: from the server's console, from the boot log your cloud provider shows, or from an administrator, who prints it on the server like this:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
256 SHA256:OJp/AFj4jaglDWB6Uo/A9x/CqlZy16EkrkpT1DPO6jI root@web01 (ED25519)

(In this lab app01 uses the same host key file as web01, so the command runs here. The text after the fingerprint is only the key's comment.) Now connect. The part before @ is the account on the server:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh ess-ssh-app@192.0.2.30
The authenticity of host '192.0.2.30 (192.0.2.30)' can't be established. ED25519 key fingerprint is: SHA256:OJp/AFj4jaglDWB6Uo/A9x/CqlZy16EkrkpT1DPO6jI This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '192.0.2.30' (ED25519) to the list of known hosts. ess-ssh-app@192.0.2.30's password: Welcome to Ubuntu 26.04.1 LTS (GNU/Linux 7.0.0-34-generic aarch64) … ess-ssh-app@app01:~$ hostname app01 ess-ssh-app@app01:~$ exit logout Connection to 192.0.2.30 closed.

The fingerprint in the question matches the one the administrator printed, so answering yes is safe. You can also paste the fingerprint you were given instead of typing yes, and ssh compares the two for you. ssh then saves the host key and asks for the account's password, which is not shown as you type. After the login banner (left out here; a first login has no "Last login" line yet) you are in a shell on app01, as the prompt shows; exit closes the session.

The key was saved in ~/.ssh/known_hosts, and every later connection is checked against it without asking. That check is what protects you from a machine in the middle: someone on the network who impersonates the server to capture what you type, including your password.

deploy@web01 · Ubuntu 26.04 LTS
$ cat ~/.ssh/known_hosts
|1|T+0lOa2YEpvYybdBZ2JUu5NJeNY=|gZDFUqd9tfFNwZb72BovzbMglJM= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDihpsEnPy21AGMq6GsLZR7xUg3nIC/0DpK4uxZsIgm
$ ssh-keygen -F 192.0.2.30
# Host 192.0.2.30 found: line 1 |1|T+0lOa2YEpvYybdBZ2JUu5NJeNY=|gZDFUqd9tfFNwZb72BovzbMglJM= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDihpsEnPy21AGMq6GsLZR7xUg3nIC/0DpK4uxZsIgm

Ubuntu's client configuration sets HashKnownHosts yes, so the file stores a hash of each host name instead of the name itself, and a stolen copy does not list the servers you use. ssh-keygen -F finds the entry for a host. Comparing fingerprints by hand works for a handful of servers; organisations with many publish their host keys instead (a centrally managed known_hosts file, or SSHFP records in DNS) or sign them as SSH certificates, so that nobody has to answer the question.

app01 accepts passwords, which is OpenSSH's default and the state of many servers built with the Ubuntu installer. Ubuntu's cloud images turn password logins off (the 60-cloudimg-settings.conf file shown in "Reading and editing files safely") and install your public key when the machine is created, which is where the next section ends up.

Logging in with a key

A key pair replaces the password. The private key stays on your machine; the public key goes into ~/.ssh/authorized_keys of the account on the server. At login the server asks your client to sign some data with the private key and checks the signature with the public key, so the private key never crosses the network. ssh-keygen creates the pair; ed25519 is the default key type, and -t ed25519 only makes that explicit. -C sets a comment that tells you later whose key it is:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh-keygen -t ed25519 -C "deploy@web01"
Generating public/private ed25519 key pair. Enter file in which to save the key (/home/deploy/.ssh/id_ed25519): Enter passphrase for "/home/deploy/.ssh/id_ed25519" (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/deploy/.ssh/id_ed25519 Your public key has been saved in /home/deploy/.ssh/id_ed25519.pub The key fingerprint is: SHA256:D6iq7NMNgKopI267Gd+EAOOXLexNEISE1aXAAq2RAG0 deploy@web01 The key's randomart image is: +--[ED25519 256]--+ |XB=+ .. | |=.E.o. | |+= .. | |=o. + . | |.o.= o. S | |. +.=. o | |.o.o+o . | |Bo+oo. | |BX=. . | +----[SHA256]-----+

Pressing Enter at the first question keeps the default file name. The passphrase encrypts the private key file, so a copy of the file is useless without it; it is typed twice and never shown. The fingerprint identifies the key, and the randomart box is the same fingerprint drawn as a picture that is easier to compare by eye.

deploy@web01 · Ubuntu 26.04 LTS
$ ls -l ~/.ssh
total 12 -rw------- 1 deploy deploy 444 Sep 27 12:28 id_ed25519 -rw-r--r-- 1 deploy deploy 94 Sep 27 12:28 id_ed25519.pub -rw-r--r-- 1 deploy deploy 142 Sep 27 12:28 known_hosts

id_ed25519 is the private key, readable by you alone (-rw-------); id_ed25519.pub is the public key, which is safe to show anyone. ssh-copy-id logs in with the password one last time and appends your public key to authorized_keys on the server:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh-copy-id ess-ssh-app@192.0.2.30
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/deploy/.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 ess-ssh-app@192.0.2.30's password: Number of key(s) added: 1 Now try logging into the machine, with: "ssh 'ess-ssh-app@192.0.2.30'" and check to make sure that only the key(s) you wanted were added.
$ ssh ess-ssh-app@192.0.2.30 hostname
Enter passphrase for key '/home/deploy/.ssh/id_ed25519': app01

This time ssh asked for the key's passphrase, which unlocks the private key on your machine and is never sent to the server, and ran hostname on app01. Given a command after the host, ssh runs just that command remotely and returns. With keys working, app01's administrator turns password logins off, as cloud images do. A login that offers no key (-o PubkeyAuthentication=no makes ssh skip its keys) is now refused, and the brackets list the methods the server still accepts:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh -o PubkeyAuthentication=no ess-ssh-app@192.0.2.30 hostname
ess-ssh-app@192.0.2.30: Permission denied (publickey).

ssh also refuses to use a private key that other accounts could read:

deploy@web01 · Ubuntu 26.04 LTS
$ chmod 644 ~/.ssh/id_ed25519 ssh ess-ssh-app@192.0.2.30 hostname
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: UNPROTECTED PRIVATE KEY FILE! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Permissions 0644 for '/home/deploy/.ssh/id_ed25519' are too open. It is required that your private key files are NOT accessible by others. This private key will be ignored. Load key "/home/deploy/.ssh/id_ed25519": bad permissions ess-ssh-app@192.0.2.30: Permission denied (publickey).
$ chmod 600 ~/.ssh/id_ed25519 ls -l ~/.ssh/id_ed25519
-rw------- 1 deploy deploy 444 Sep 27 12:28 /home/deploy/.ssh/id_ed25519
Your private key is the login
Anyone with a copy of id_ed25519 and its passphrase can log in wherever the public key is installed. Give every key a passphrase, never copy a private key onto a server or into a git repository, and use one key per person and device, so that you can remove one without affecting the others. Only the .pub file belongs anywhere else. A key used by a script, such as a nightly backup, cannot have a passphrase because nobody is there to type it; limit it on the server instead, with options such as restrict, from= and command= in front of its line in authorized_keys, which the hardening course covers.

The agent and ~/.ssh/config

Typing the passphrase for every connection gets tiring. ssh-agent is a small program that holds your unlocked keys in memory for the rest of your session, and ssh asks it to sign instead of reading the key file. Desktop sessions usually start one for you; in a plain terminal session, ssh-add -l shows whether one is running, and eval "$(ssh-agent -s)" starts one (the eval sets the variables that tell ssh where to find it). ssh-add then asks for the passphrase once:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh-add -l
Could not open a connection to your authentication agent.
$ eval "$(ssh-agent -s)" ssh-add
Agent pid 394832 Enter passphrase for /home/deploy/.ssh/id_ed25519: Identity added: /home/deploy/.ssh/id_ed25519 (deploy@web01)

The agent keeps running in the background until eval "$(ssh-agent -k)" stops it. The following commands run in the same session, which finds the agent through SSH_AUTH_SOCK, the path of its socket under ~/.ssh/agent/:

deploy@web01 · Ubuntu 26.04 LTS
$ echo "$SSH_AUTH_SOCK"
/home/deploy/.ssh/agent/s.PFB9m6gUIu.agent.ztHHI6ppTv
$ ssh-add -l
256 SHA256:D6iq7NMNgKopI267Gd+EAOOXLexNEISE1aXAAq2RAG0 deploy@web01 (ED25519)
$ ssh ess-ssh-app@192.0.2.30 hostname
app01

No passphrase prompt this time. The agent can also be forwarded to a server with ssh -A, but anyone with root access on that server can then use your keys while you are connected, so forward it only to machines you trust.

Every line of authorized_keys is a working way into that account, and keys tend to outlive the people and laptops they belonged to. ssh-keygen -lf prints the fingerprint and comment of each line, which tells you whose keys they are:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh ess-ssh-app@192.0.2.30 ssh-keygen -lf .ssh/authorized_keys
256 SHA256:D6iq7NMNgKopI267Gd+EAOOXLexNEISE1aXAAq2RAG0 deploy@web01 (ED25519)

Typing the account and address every time is tedious and error-prone. ~/.ssh/config gives servers short names:

~/.ssh/config
Host app01
HostName 192.0.2.30
User ess-ssh-app

Host is the name you type, HostName the real address or DNS name and User the account. Other common settings are Port, IdentityFile for a particular key and ProxyJump to go through a bastion host (the hardening course uses it). scp, rsync and every other program that runs over ssh read the same file.

deploy@web01 · Ubuntu 26.04 LTS
$ ssh app01 hostname
app01

Copying files: scp and rsync

scp copies files over SSH the way cp copies them locally. A remote location is written host:path, and a bare host: means the home directory on that host:

deploy@web01 · Ubuntu 26.04 LTS
$ echo "disk report from web01" > report.txt
$ scp report.txt app01:
$ ssh app01 cat report.txt
disk report from web01

On a terminal scp also draws a progress line for each file. Since OpenSSH 9.0 it transfers files with the SFTP protocol, and scp -O falls back to the old protocol for servers that lack SFTP. rsync is the better tool for directories and repeated copies: it compares both sides and sends only what changed. -a copies recursively and keeps permissions and times, -v lists what it sends, and the slash after site/ means "the contents of site":

deploy@web01 · Ubuntu 26.04 LTS
$ mkdir site echo "<h1>hello</h1>" > site/index.html echo "body { margin: 0 }" > site/style.css
$ rsync -av site/ app01:site/
sending incremental file list created directory site ./ index.html style.css sent 226 bytes received 84 bytes 620.00 bytes/sec total size is 34 speedup is 0.11
$ echo "<p>updated</p>" >> site/index.html rsync -av site/ app01:site/
sending incremental file list index.html sent 176 bytes received 41 bytes 434.00 bytes/sec total size is 49 speedup is 0.23

The second run sent only index.html, the file that changed. rsync must be installed on both machines; "Archives, downloads and checksums" goes further with it. Remove the practice files on both sides when you are done:

deploy@web01 · Ubuntu 26.04 LTS
$ rm -r report.txt site ssh app01 rm -r report.txt site

When a connection fails

The last line of an ssh error names the layer that failed, and ssh -v shows each step on the way. The most common error is a refused key:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh deploy@192.0.2.30 hostname
deploy@192.0.2.30: Permission denied (publickey).

The server does not tell the client why, but it writes the reason to its log. On Ubuntu that is sudo journalctl -u ssh (on RHEL -u sshd); app01's SSH server runs in this lab as a unit called ess-ssh-server:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo journalctl -u ess-ssh-server --no-hostname -n 2
Sep 27 12:29:20 sshd-session[395696]: User deploy from 192.0.2.1 not allowed because not listed in AllowUsers Sep 27 12:29:20 sshd-session[395696]: Connection closed by invalid user deploy 192.0.2.1 port 50576 [preauth]

The account deploy is not allowed to log in to app01 at all. Since OpenSSH 9.8 these lines come from sshd-session, the process that serves one connection. When you cannot read the server's log, -v shows how far the client got:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh -v deploy@192.0.2.30 hostname
… debug1: Connecting to 192.0.2.30 [192.0.2.30] port 22. debug1: Connection established. … debug1: Authenticating to 192.0.2.30:22 as 'deploy' … debug1: Server host key: ssh-ed25519 SHA256:OJp/AFj4jaglDWB6Uo/A9x/CqlZy16EkrkpT1DPO6jI … debug1: Host '192.0.2.30' is known and matches the ED25519 host key. … debug1: Authentications that can continue: publickey … debug1: Offering public key: /home/deploy/.ssh/id_ed25519 ED25519 SHA256:D6iq7NMNgKopI267Gd+EAOOXLexNEISE1aXAAq2RAG0 agent debug1: Authentications that can continue: publickey … debug1: No more authentication methods to try. deploy@192.0.2.30: Permission denied (publickey).

The connection was made, the host key matched, and the key was offered and refused: the problem is on the server side, such as the wrong account, a key missing from that account's authorized_keys, or a rule like the one above.

The loudest error is a changed host key. Here app01 has been rebuilt with a new one:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh app01 hostname
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that a host key has just been changed. The fingerprint for the ED25519 key sent by the remote host is SHA256:2Gg5chCJsJBHb027lF4ECQooRoI85xyqkWXZAT+PbY0. Please contact your system administrator. Add correct host key in /home/deploy/.ssh/known_hosts to get rid of this message. Offending ED25519 key in /home/deploy/.ssh/known_hosts:1 remove with: ssh-keygen -f '/home/deploy/.ssh/known_hosts' -R '192.0.2.30' Host key for 192.0.2.30 has changed and you have requested strict checking. Host key verification failed.

ssh refuses to connect, because this is exactly what a machine in the middle would look like. A rebuilt or reinstalled server is the usual, innocent cause, but confirm it: get the new fingerprint from the administrator (for this lab, from the rebuilt key file) and only then remove the old entry and connect again:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh-keygen -lf /var/tmp/ess-ssh/rebuilt_host_ed25519_key.pub
256 SHA256:2Gg5chCJsJBHb027lF4ECQooRoI85xyqkWXZAT+PbY0 root@app01 (ED25519)
$ ssh-keygen -R 192.0.2.30
# Host 192.0.2.30 found: line 1 /home/deploy/.ssh/known_hosts updated. Original contents retained as /home/deploy/.ssh/known_hosts.old
$ ssh app01 hostname
The authenticity of host '192.0.2.30 (192.0.2.30)' can't be established. ED25519 key fingerprint is: SHA256:2Gg5chCJsJBHb027lF4ECQooRoI85xyqkWXZAT+PbY0 This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '192.0.2.30' (ED25519) to the list of known hosts. app01

The remaining errors come from the network and name resolution, which the previous two lessons explain: a server whose SSH service is stopped, a firewall that drops SSH, and a name that does not exist:

deploy@web01 · Ubuntu 26.04 LTS
$ ssh app01 hostname
ssh: connect to host 192.0.2.30 port 22: Connection refused
$ ssh -o ConnectTimeout=5 app01 hostname
ssh: connect to host 192.0.2.30 port 22: Connection timed out
$ ssh nosuch.invalid
ssh: Could not resolve hostname nosuch.invalid: Name or service not known
Reading an ssh error
ssh fails
the last line names the layer
Could not resolve hostname
The name lookup failed
getent hosts NAME (DNS lesson)
Connection refused
Nothing listens on port 22
is sshd or ssh.socket running? right port?
Connection timed out
Packets are dropped
firewall, security group, wrong address
Host key verification failed
The key differs from known_hosts
confirm the new key, then ssh-keygen -R
Permission denied (publickey)
The server refused your key
ssh -v, then the server's log

On the server side, Ubuntu keeps port 22 open through ssh.socket, which starts ssh.service on the first connection ("Services with systemd"), while RHEL runs sshd.service directly. Configuring the server itself is the subject of the hardening course.

Try this

Match your key on both ends of a login, with your own computer as the client and your practice machine as the server. On your computer, create a key with ssh-keygen as above and install it on the practice machine: ssh-copy-id USER@ADDRESS works if the server still accepts passwords. A cloud image does not, so there, paste the single line of your .pub file into ~/.ssh/authorized_keys through the console (the directory ~/.ssh with mode 700 and the file with mode 600, both owned by you). If your computer cannot reach the machine's address, which happens with some local VM networks, use a second VM as the client. Print your key's fingerprint with ssh-keygen -lf ~/.ssh/id_ed25519.pub, log in with the key, then on the server run sudo journalctl -u ssh -g "Accepted publickey" -n 1 (on RHEL -u sshd). The SHA256: fingerprint at the end of that line is the same as your key's, which is how an administrator tells which key was used for a login. Then connect with ssh -v and find the Server accepts key line that names the same key.

Takeaway

Check a host key's fingerprint the first time you connect and whenever ssh says it has changed. Log in with a passphrase-protected key held by the agent, and when a connection fails, read the last line of the error, then ssh -v and the server's log.

Quick check
01ssh to a server you use every day suddenly prints REMOTE HOST IDENTIFICATION HAS CHANGED. Nobody told you it was rebuilt. What do you do?
Correct — A rebuild is the usual cause, but an impersonated server looks exactly the same. Only an out-of-band check tells them apart.
Incorrect — Removing the entry first throws away the one check that would catch a machine in the middle.
Incorrect — If the server is an impostor, logging in hands it your session, and a password if you type one.
Incorrect — The file is not stale: it holds the key this server used before. Deleting it removes the checks for every other server too.
02ssh app01 fails with Permission denied (publickey). ssh -v shows "Offering public key: ... id_ed25519" and then "Authentications that can continue: publickey". Where is the problem most likely?
Incorrect — ssh would then print UNPROTECTED PRIVATE KEY FILE and never offer the key.
Incorrect — A changed host key stops the connection before authentication, with its own warning.
Correct — The key was offered and refused. Check the account name, its authorized_keys and the server's log.
Incorrect — The connection was established and authentication started, so packets reach port 22.
03You gave your SSH key a passphrase. What does the passphrase protect against?
Incorrect — The passphrase protects your key file only. Password logins are a separate server setting.
Correct — The private key file is encrypted with the passphrase, so a stolen copy is useless without it.
Incorrect — The private key never leaves your machine, passphrase or not. The server only checks a signature made with it.
Incorrect — Every SSH session is encrypted with or without a passphrase on the key.

Related