Module 5: Remote Machines, Networking, and Scripting

4. Connecting with SSH and key authentication

Description

So far, when something failed in the previous lesson, the problem lived entirely inside your own terminal: you resolved a name, measured whether there was reachability, checked whether a port responded. All of the diagnosis happened on your side. Today you cross that line: you are going to get into another machine, with your own terminal running inside one that is not yours, and you are going to do it without typing a password every time.

By the end of this lesson you will be able to connect to a remote server with ssh, understand exactly what that "fingerprint" message that shows up the first time is asking you and why it exists, generate your own key pair with ssh-keygen -t ed25519, install your public key on a server with ssh-copy-id (and understand what that command does under the hood, instead of treating it as magic), and write a ~/.ssh/config that remembers the user, port, and key for every server you connect to, so you do not have to.

Connection to the module: this is not a syntax exercise. It is the real gateway to any work with remote infrastructure: connecting to the instance where a deployment runs, getting into a client's server to check a production log at eleven at night, or giving a new team member access without sharing a password that nobody later remembers who else has. Every one of those situations goes well or badly depending on whether you understand what you are about to see: what SSH actually guarantees, why a key replaces a password without being "just a longer password," and why the system is so strict about the permissions on those files — which is exactly the least-privilege criterion from module 4, applied now to the keys you use to identify yourself to another machine.


An encrypted channel that also verifies who you are talking to

Imagine you need to deliver a confidential envelope to an office in a building you have never been to. Before you hand over the envelope, two checks have to happen, in this order. First, you need to be sure the building in front of you really is the one you were looking for, and not an identical one set up three doors down by someone who wants to intercept your mail. Second, once you have confirmed it is the right building, the person at the front desk needs to be sure the one delivering the envelope really is you, and not someone who found your business card lying on the street. And everything said from the moment you walk through the door until the envelope reaches its destination travels inside a sealed tube that nobody in the hallway can open to read.

That, with no metaphor left, is exactly what SSH (Secure Shell) guarantees on every connection: server authentication (you confirm the machine on the other end is the one you think it is, not an impostor), your authentication to the server (the server confirms you are who you say you are, usually with a key instead of a password), and an encrypted channel between both ends, so that nobody intercepting the traffic along the way — your internet provider, a public Wi-Fi network, a compromised router — can read a single command or a single line of output from what you do inside that session. All three are necessary: encrypting the channel without authenticating anyone would protect you from someone listening in, but not from connecting to the wrong server or from someone impersonating you.

Your zero-cost lab

Everything that follows you are going to run against a real remote machine, without paying anyone or requesting a cloud account: your own computer, acting as both client and server at once. Enable the SSH server for your system:

macOS (turn on "Remote Login" from the command line, without opening System Settings):

sudo systemsetup -setremotelogin on

Debian/Ubuntu-based Linux (if sshd is not installed or running yet):

sudo apt update && sudo apt install -y openssh-server
sudo systemctl enable --now ssh

(On Red Hat/Fedora-based distributions, the package installs with dnf and the service is called sshd instead of ssh.)

If you would rather not touch your own machine's network configuration — for example, on a managed corporate laptop — you can set up the same lab inside an isolated container, without installing anything on the host: docker run -d --name ssh-lab -p 2222:22 ubuntu:24.04 sleep infinity, and inside the container (docker exec -it ssh-lab bash) you install openssh-server, generate the host keys with ssh-keygen -A, and start the daemon with /usr/sbin/sshd -D &. From there, every command in this lesson is identical, only the port (-p 2222) and the destination (localhost instead of your machine's hostname) change.

Worked example: your first connection and the fingerprint message

With the server already enabled, connect to yourself:

ssh $(whoami)@localhost

By default, ssh assumes the server listens on port 22 — the standard SSH port. If yours listens on a different one (for example, the Docker container from the alternative lab above, mapped to 2222), you specify it with -p:

ssh -p 2222 $(whoami)@localhost

Note the order: -p <port> goes before the user@host destination, like most flags in the terminal that you already know from earlier modules.

What to expect — if this is the first time your SSH client talks to this server (or to this port, on this host), you are going to see something like this before it asks you for any password:

The authenticity of host 'localhost (127.0.0.1)' can't be established.
ED25519 key fingerprint is SHA256:qR7yq3+2Z8pXk1s4Vd0Fh6mCw9tL2aB5jN8xE1yU7oQ.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

(The exact wording may vary slightly by OpenSSH version, but the structure — a question about an unrecognized fingerprint — has stayed the same for years.)

That question is half of the authentication we talked about above: your client has never seen this server's cryptographic identity (its host key), and it is asking you whether you trust that it is who it claims to be, based solely on that fingerprint. This first leap of faith is called TOFU (trust on first use): in a lab against your own machine it is perfectly safe to answer yes, because you know for certain the server is you yourself. On a third party's production server, the correct practice is to compare that fingerprint against one the administrator shared with you through another channel (not the same chat where they sent you the IP) before accepting.

Answer yes and you are going to see:

Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.

That line got saved to ~/.ssh/known_hosts, a plain text file with one entry per known server (host or IP, key type, and the server's public key in base64):

cat ~/.ssh/known_hosts

What to expect:

localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKj3f8s...

From the next time you connect on, ssh is going to compare the key the server presents against this line without asking you anything, because it already knows it. If that key ever changes — because you reinstalled the server, or because someone is trying to impersonate it — you are going to see a much more aggressive warning than the first-time one. That is what we cover in the common mistakes.


Keys instead of a password: the pair that really identifies you

Think of a password as a physical key you make yourself, and for it to work, you have to hand an identical copy to the doorman every time you want them to recognize you — and that copy travels from your hand to theirs on every single entry attempt. An SSH key works the other way around: you make an inseparable pair of pieces, one that never leaves your pocket (the private key) and another designed specifically to be handed out with no risk at all (the public key). You give the doorman the public piece ahead of time, just once. When you show up, the doorman does not ask you to show them anything: they pose a puzzle that only the private piece you never shared can solve, you solve it on your side without the private piece ever traveling anywhere, and the doorman verifies the answer with the public piece they already had. At no point in that exchange did the private key ever cross the network.

That is asymmetric key-pair authentication, and it is the real reason it replaces the password: it is not "a longer password that is harder to guess" (although it is also that), it is a mechanism where the actual secret is never transmitted, not even encrypted, on any connection attempt.

Worked example: generate your key, install it, and log in without a password

Generate your key pair with the algorithm OpenSSH's own documentation recommends today, Ed25519 (shorter and faster to verify than RSA, with equal or better security):

ssh-keygen -t ed25519 -C "alex@laptop"

The -C flag just adds an identifying comment at the end of the public key (typically your user and machine), so you can tell at a glance which key is which once you have several. ssh-keygen is going to ask you three questions:

Generating public/private ed25519 key pair.
Enter file in which to save the key (/Users/alex/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:

The first question is where to save the file — press Enter to accept the default location unless you already have a key there and want to keep it. The other two are the passphrase: a second secret, this one truly yours alone, that encrypts the private key file on disk. Think of it as an extra safe wrapped around the piece that was never supposed to leave your pocket: if someone steals your laptop or copies your id_ed25519 file, without the passphrase that stolen file is useless to them. Leaving it empty (Enter twice) is valid and common in practice labs, but on any key you use against a real server, set one — it is the only defense you have left if the file falls into the wrong hands.

What to expect:

Your identification has been saved in /Users/alex/.ssh/id_ed25519
Your public key has been saved in /Users/alex/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:9fK2mQ7pR1vN8xL3wB6tC4hJ0aS5dY2eU9oI7gM1kFc alex@laptop
The key's randomart image is:
+--[ED25519 256]--+
|      .oo=+.     |
|     . +o+o.     |
|      o.=o.      |
|   . . oS.       |
|    o o. o       |
|   . +.o .       |
|    +.*.E        |
|   ..*.O.        |
|   .+=+*.        |
+----[SHA256]-----+

Two new files in ~/.ssh/: id_ed25519 (private, never leaves your machine) and id_ed25519.pub (public, the one you are going to hand out). Verify their permissions:

ls -l ~/.ssh/id_ed25519*

What to expect:

-rw-------  1 alex  staff  411 Jul 20 10:02 /Users/alex/.ssh/id_ed25519
-rw-r--r--  1 alex  staff  100 Jul 20 10:02 /Users/alex/.ssh/id_ed25519.pub

This is not cosmetic. Applying exactly the table from the previous module: the private key is born with 600 (rw-------, only you can read and write it) and the entire ~/.ssh folder must be 700 (rwx------, only you can traverse it). If those permissions are more open than expected, OpenSSH flatly refuses to use the key — it is not a best-practices suggestion, it is a condition the SSH client and server themselves check before accepting the connection, precisely because a private key readable by any other account has stopped being private.

Now install your public key on the server (in this lab, your own machine) with ssh-copy-id:

ssh-copy-id $(whoami)@localhost

What to expect:

/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/Users/alex/.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
alex@localhost's password:

Number of key(s) added: 1

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

It asked you for your password one last time — it needs it to be able to write to the server. ssh-copy-id does not do any magic: it takes your public key, connects to the server with whatever authentication method you still have available (the password), and on the other end runs exactly these three steps, which you can replicate by hand if you ever work on a system where ssh-copy-id is not available:

cat ~/.ssh/id_ed25519.pub | ssh alex@server \
  "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

The file it creates or edits is called ~/.ssh/authorized_keys, and its logic is simple: one public key per line, whatever shows up there is authorized to log in as that user. Confirm it:

cat ~/.ssh/authorized_keys

What to expect (a single long line, starting with the key type):

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKj3f8s... alex@laptop

Now connect again:

ssh $(whoami)@localhost

What to expect: if you set a passphrase on your key, it is going to ask you for it (to unlock the local file, a secret that never leaves your machine); if you left it empty, you are going to get in directly, no prompt at all. Either way, it is never going to ask you for your account password on the server — the key already handled that authentication.


~/.ssh/config: an alias per server, not an endless command

With a single server, typing ssh alex@localhost is tolerable. As soon as you work with two or three different servers — each with its own user, its own port, and sometimes its own key — repeating all that information every time you connect, or worse, trying to memorize it, is exactly the kind of friction the terminal is designed to eliminate. ~/.ssh/config is a text file where you define, once per server, a short alias and everything that alias implies.

Worked example: an alias with user, port, and key

Create (or edit) the file:

mkdir -p ~/.ssh && chmod 700 ~/.ssh
nano ~/.ssh/config

And add a block like this:

Host lab
    HostName localhost
    User alex
    Port 22
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Each line answers a specific question: Host defines the alias you are going to type (it can be any word, it does not have to look like the real hostname); HostName is the real address or domain that alias translates to; User and Port save you from typing them every time; IdentityFile fixes which private key to use without ssh having to try one by one every key you have in ~/.ssh/; and IdentitiesOnly yes tells ssh to limit itself to that specific key, instead of offering the server every key it finds — useful as soon as you have more than one key and want to avoid getting locked out from too many failed attempts with the wrong one.

Also fix the file's permissions — it is not a private key, but it does contain hostnames and users that do not need to be exposed to other accounts on the system:

chmod 600 ~/.ssh/config

Now connect using just the alias:

ssh lab

What to expect: the same connection as before, without typing user, port, or the key's path — ssh resolved all of it from the Host lab block.

Before actually connecting, you can ask ssh to show you exactly what configuration it is going to use for a given alias, without opening any connection — the same "check before you run it" spirit you already saw with other tools:

ssh -G lab

What to expect (a long list of resolved parameters; the lines relevant to what you configured):

...
hostname localhost
user alex
port 22
identityfile ~/.ssh/id_ed25519
...

If something does not match what you expected — the wrong port, the wrong key — you are going to see it here before ssh even attempts to connect, not after a confusing error message.


Why the industry stopped trusting passwords for SSH

It is not a fad or an aesthetic preference. A server with SSH exposed to the internet on port 22 gets, constantly, automated bot attempts trying common username and password combinations without rest — it is constant background traffic on any public IP, not an attack targeted at you specifically. A password reasonable for a human (something memorable) is, against that kind of sustained brute-force attack over months, an achievable target sooner or later. An Ed25519 key is not: the space of possible combinations is not "bigger," it is on an order of magnitude that makes direct brute force a pointless strategy with the computing power available today.

There is a second reason, less technical and more operational: revoking access. If a password shared by the team leaks, you have to change it and tell everyone who legitimately used it. If the public key of someone who left the team is in authorized_keys, revoking their access is deleting one line from that file — nobody else finds out, nobody else has to change anything. For these two reasons, most cloud server providers configure their images by default with password authentication disabled for SSH, accepting only keys from the very first boot.

A practical warning is worth mentioning here: you are going to find the -o StrictHostKeyChecking=no option in countless continuous integration scripts and rushed tutorials online. What it literally does is turn off the fingerprint question you saw at the start of this lesson — that is, it switches off half of SSH's guarantee (server authentication) so the script does not stop waiting for a yes. It is a defensible decision inside an ephemeral container you created yourself thirty seconds ago; it is a bad idea on any connection to a server that matters to someone.


Common mistakes

"I ran chmod 600 on the ~/.ssh folder instead of on the files inside it, and now nothing works — not even with a password." What happens: you applied a secret file's permission to a directory. Why it happens: as you saw in the chmod lesson in the previous module, a directory needs the x bit to be traversable — even its own owner needs it. 600 (rw-------) does not include x; 700 (rwx------) does. Without that bit on ~/.ssh, neither you nor the ssh process can get in to read config, id_ed25519, or authorized_keys, even if each of those files has perfectly correct permissions of its own. How to spot it: ssh -v (verbose) toward that host shows an explicit permissions error on the directory before it even attempts authentication, and ls -ld ~/.ssh shows you the directory's actual permission (not that of what it contains). How to fix it: chmod 700 ~/.ssh for the directory, and leave 600 only for the files with secret content inside (id_ed25519, authorized_keys, config).

"I accepted the fingerprint message without looking at it, I always answer yes without thinking." What happens: you treat that question as an annoying formality instead of as the only real defense against connecting to the wrong server. Why it is a conceptual problem, not just a habit: if you ever see this message — far more alarming than the first-connection one — on a server you had already connected to before:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

that means the server's key changed since last time, and there are exactly two explanations: someone legitimately reinstalled that server (a new host key generated from scratch), or someone is trying to impersonate that server to intercept your session. How to spot it: the message appears on its own, you cannot ignore it — ssh refuses to continue without explicit confirmation. How to fix it: before accepting anything, confirm through another channel (with whoever administers that server, by chat or call, not through the same connection you are trying to open) whether the change was expected. If you confirm it, delete the old entry with ssh-keygen -R hostname and reconnect to register the new key. If you cannot confirm it, do not continue.

"I copy-pasted the public key by hand into authorized_keys with an editor, and it still asks me for the password." What happens: authorized_keys expects exactly one complete key per line, with no line breaks in the middle. Copying and pasting manually in some text editors (especially if the terminal visually wraps the long line) inserts a real line break where there was only a visual wrap, splitting the key into two invalid lines. How to spot it: cat ~/.ssh/authorized_keys on the server and count the lines — if you expected one key and see two or three short lines instead of one long one, that is the problem; ssh -v from the client also shows it offering the key but the server rejecting it. How to fix it: use ssh-copy-id instead of copy-pasting by hand whenever you can — it automates exactly this step without the risk of manual editing; if you do not have that option, use cat file.pub >> ~/.ssh/authorized_keys from the terminal instead of a visual editor.


Exercises

1. Interpret the message before responding

You connect to a server you have already logged into dozens of times without a problem, and today ssh shows you the REMOTE HOST IDENTIFICATION HAS CHANGED! warning with a fingerprint different from the one you remembered. What should you do before responding anything, and why is it not enough to just trust that "it was probably maintenance"?

See solution

Before responding anything, confirm with whoever administers that server — through a channel different from the one you use to connect — whether there was a legitimate change (a reinstall, a migration to another machine, a host key rotation). It is not enough to assume it was maintenance, because that assumption is exactly what a machine-in-the-middle attack needs you to make: if someone is intercepting the connection and presenting their own key instead of the real server's, the message looks identical. Only after confirming through another channel does it make sense to delete the old entry with ssh-keygen -R host and reconnect.

Why it works: the identity-change message is the only automatic signal you have that the server's key is not the one your client registered before; verifying it through an independent channel is the only way to tell a legitimate change apart from an impersonation, because the compromised channel (if there is one) cannot lie in two different places at once.

2. Generate and classify

You ran ssh-keygen -t ed25519 -C "ana@server" and accepted every default option, including a passphrase. Name the two files that got created, which one gets shared and which one never leaves your machine, and the permissions each one should have.

See solution

id_ed25519 (private): never leaves the machine it was generated on, permission 600 (rw-------). id_ed25519.pub (public): the one that gets shared and installed on every server, permission 644 (rw-r--r--) — being readable by anyone is the point, not a risk.

Why it works: the security of the scheme depends entirely on the private half of the pair never being transmitted or readable by anyone other than its owner; the public half, on the other hand, reveals nothing useful to an attacker even if everyone can see it — it is mathematically infeasible to reconstruct the private key from the public one.

3. Write the config block

You need to connect often to a backup server with the alias backup, at the real host backup.example.com, port 2222, user ops, using specifically the key ~/.ssh/id_backup. Write the full ~/.ssh/config block and the command you would use to verify it resolved correctly without connecting yet.

See solution
Host backup
    HostName backup.example.com
    User ops
    Port 2222
    IdentityFile ~/.ssh/id_backup
    IdentitiesOnly yes

To verify without connecting: ssh -G backup, which prints the entire resolved configuration for that alias (hostname, user, port, key) without opening any real connection.

Why it works: ssh -G evaluates exactly the same ~/.ssh/config resolution rules a real connection would use, but stops before attempting the network step — it lets you confirm a wrong user, port, or key before they turn into a confusing connection error.

4. Diagnose an ssh-copy-id that did not work

A coworker ran ssh-copy-id against a server, saw the success message ("Number of key(s) added: 1"), but when connecting again the server still asks for a password. Name two possible causes and how they would confirm each one from the terminal.

See solution

Cause 1 — broken permissions on the server: if ~/.ssh on the server ended up with more open permissions than expected (for example, writable by the group), OpenSSH can reject key authentication entirely. Confirm it with ssh -v user@host and checking the detailed output, or by still getting in with a password and running ls -ld ~/.ssh && ls -l ~/.ssh/authorized_keys on the server to compare against 700 and 600.

Cause 2 — the client is using a different key than the one that got installed: if the person has several keys and did not specify which one to use (neither with -i nor with IdentityFile in ~/.ssh/config), ssh might be offering a different key than the one that ended up in authorized_keys. Confirm it with ssh -v user@host, looking for the line that says which key it is offering, and comparing it against the contents of authorized_keys on the server.

Why it works: ssh -v exposes exactly the step where key authentication gets decided — which key the client offers and why the server rejects or accepts it — which is information the ssh-copy-id success message cannot guarantee on its own, because that message only confirms that writing the file worked, not that a future connection will accept it.


Summary and next step

Before moving on you should be able to:

  • explain, without looking at notes, SSH's three guarantees (server authentication, your authentication, encrypted channel) and why the first-time fingerprint message is half of that guarantee, not a formality;
  • generate a key pair with ssh-keygen -t ed25519, say which file is which and what permissions each one should have, applying directly what you already knew about chmod;
  • install a public key with ssh-copy-id and explain what that command does under the hood in case it is ever unavailable;
  • write a ~/.ssh/config block with alias, user, port, and key, and verify it with ssh -G before actually connecting.

Today you solved how to get into another machine securely and repeatably. You have not yet solved what to do once inside with something bigger than a single loose command: moving whole files between both machines, or leaving a process running there without it dying the moment you close your laptop. That is exactly what comes in the next lesson — scp, rsync, and tmux — and all of it is going to rest on the key-based connection you just mastered: every one of those commands, underneath, opens exactly the same kind of SSH session you opened today.


Resources