Skip to content

Phase 1 · FOUNDATIONS

SSH deep-dive — keys, config, scp, tunnels

Day 13 of 90 ~55 min 0/20 in phase Builds on Day 12

By the end of today

  • Generate an ed25519 key pair and install the public key on a server
  • Write a ~/.ssh/config entry that turns a long ssh command into one word
  • Copy files with scp and reach a remote port through a tunnel

SSH keys: proving who you are without a password

Section 1 of 5 · ~3 min

Every server you will ever operate is reached the same way: SSH — Secure Shell — an encrypted channel to a shell on a remote machine. The first question SSH has to answer is who are you? You can answer with a password, but passwords are guessable, phishable, and get typed into the wrong window. The professional answer is a key pair.

A key pair is two mathematically linked files. The private key stays on your laptop and never leaves it. The public key is safe to hand out — you copy it onto every server you want to reach. When you connect, the server sends a challenge that only the matching private key can answer, so SSH proves you hold the private key without ever transmitting it. No shared secret crosses the wire, so there is nothing to steal in transit.

You generate a pair with ssh-keygen. In 2026 the default and correct choice is ed25519 — a modern elliptic-curve algorithm that is faster and far shorter than old RSA keys while being at least as strong. You get two files: ~/.ssh/id_ed25519 (private, must be mode 600) and ~/.ssh/id_ed25519.pub (public). The public key is one line of text.

Installing that public line on a server means appending it to ~/.ssh/authorized_keys in the remote user’s home. SSH reads that file on every login and lets in any key listed there. Permissions matter on both ends, for two different reasons. On your laptop, the SSH client hard-refuses to use a private key that anyone but you can read — a client-side permission check, nothing to do with the server. On the server, an sshd option called StrictModes makes sshd ignore authorized_keys entirely if ~/.ssh or the file itself is group- or world-writable, silently falling back to a password.

SSH public-key authentication: your laptop holds the private key and the server holds your public key in authorized_keys. The server issues a challenge and only the private key can sign the answer, so no password crosses the network. Your laptop private key Server authorized_keys (.pub) 1. offer public key 2. random challenge 3. signed proof — access
The handshake: a challenge the private key signs — no password ever crosses the wire.

Real world: Think of authorized_keys as the guest list taped inside a club door. Your public key is your name on that list; your private key is your face. The bouncer never asks for a password — they glance at the list, glance at you, and wave you through. Copy your name onto a hundred doors and you walk into all hundred, yet nobody can impersonate you, because faces cannot be photocopied off a list.

GitHub is the example every developer meets first: when you git push over SSH, you are authenticating with exactly this mechanism, and GitHub’s own docs recommend ed25519 keys. The git@github.com login is public-key auth against an authorized_keys list GitHub manages for your account.

One config file, scp, and tunnels

Typing ssh -i ~/.ssh/id_ed25519 -p 2222 pushkar@10.0.4.9 a hundred times a day is misery. ~/.ssh/config fixes it: define a Host alias once — user, hostname, port, key — and ssh dev expands to the whole line. Every SSH-aware tool reads this file, so scp and git get the alias for free.

scp copies files over that same encrypted channel: scp notes.txt dev:/tmp/ pushes a file to the server, scp dev:/tmp/notes.txt . pulls it back — same auth, same alias. Finally, local port forwarding (ssh -L) tunnels a port on your laptop to a port reachable from the server. ssh -L 5432:localhost:5432 dev makes the server’s database appear on your own localhost:5432, so you reach a service that only listens internally without exposing it to the internet.

Hands-On Lab

Section 2 of 5 · ~3 min

Budget about 30 minutes. Open your WSL2 Ubuntu 24.04 terminal. There is no separate server to rent today — you will run a real SSH server on this same box and connect to it over localhost, so every command below is genuine SSH, not a simulation. Type each command yourself.

# 1. Generate an ed25519 key pair. -C is a label, -f names the file, -N "" sets an empty passphrase for the lab.
ssh-keygen -t ed25519 -C "pushkar@mission90" -f ~/.ssh/id_ed25519 -N ""
# Output:
# Generating public/private ed25519 key pair.
# Your identification has been saved in /home/pushkar/.ssh/id_ed25519
# Your public key has been saved in /home/pushkar/.ssh/id_ed25519.pub
# The key fingerprint is:
# SHA256:6q3Yl0m2f8k9wZ1c... pushkar@mission90
# The key's randomart image is:
# +--[ED25519 256]--+
# |        ..oo.    |
# |       . .o.o    |
# |        o ..o    |
# |       o o..     |
# |      . S.o.     |
# |       o *.=     |
# |      . B.@ .    |
# |       + %.=     |
# |        =EO      |
# +----[SHA256]-----+
# 2. Look at what was created. The private key is 600 (owner only); the public key is 644.
ls -l ~/.ssh
# Output:
# total 8
# -rw------- 1 pushkar pushkar 411 Jul 10 09:14 id_ed25519
# -rw-r--r-- 1 pushkar pushkar  99 Jul 10 09:14 id_ed25519.pub
# 3. The public key is ONE line — this is what you hand to a server. Its fingerprint identifies it.
cat ~/.ssh/id_ed25519.pub
ssh-keygen -lf ~/.ssh/id_ed25519.pub
# Output:
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...E9k pushkar@mission90
# 256 SHA256:6q3Yl0m2f8k9wZ1c... pushkar@mission90 (ED25519)
# 4. Install a real SSH server on this box so you have something to connect to.
sudo apt-get install -y openssh-server
# Output (trimmed):
# Reading package lists... Done
# The following NEW packages will be installed:
#   openssh-server
# Setting up openssh-server (1:9.6p1-3ubuntu13.5) ...
# Created symlink /etc/systemd/system/sshd.service -> /usr/lib/systemd/system/ssh.service.
# 5. Start it now and confirm it is running. On Ubuntu the unit is ssh.service.
sudo systemctl enable --now ssh
systemctl is-active ssh
# Output:
# active
# 6. Authorize your own key: append the .pub line to authorized_keys, then lock the perms StrictModes requires.
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
# (no output — success is silent)
# 7. Connect over SSH to localhost using the key. accept-new trusts the host key on first contact.
ssh -o StrictHostKeyChecking=accept-new localhost whoami
# Output:
# Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.
# pushkar
# 8. Write a ~/.ssh/config alias so "dev" means the whole connection, then lock the file to 600.
cat > ~/.ssh/config <<'EOF'
Host dev
    HostName localhost
    User pushkar
    IdentityFile ~/.ssh/id_ed25519
EOF
chmod 600 ~/.ssh/config
# (no output)
# 9. Read the config back — this is the one word that replaced the long command line.
cat ~/.ssh/config
# Output:
# Host dev
#     HostName localhost
#     User pushkar
#     IdentityFile ~/.ssh/id_ed25519
# 10. Use the alias. No -i, no user@host — SSH reads it all from the config.
ssh dev whoami
# Output:
# pushkar
# 11. Copy a file over that same encrypted channel with scp, then read it back on the server.
echo "deploy notes" > notes.txt
scp notes.txt dev:/tmp/
ssh dev cat /tmp/notes.txt
# Output:
# notes.txt                              100%   13    18.7KB/s   00:00
# deploy notes
# 12. Local port forward: expose the server's port 22 on your own localhost:2222, then connect THROUGH the tunnel.
ssh -f -N -L 2222:localhost:22 dev
ssh -p 2222 -o StrictHostKeyChecking=accept-new localhost whoami
# Output:
# Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
# pushkar

Before you close the terminal, say the flow back out loud: you made a key pair, put the public half in authorized_keys, connected with no password, shrank the command to ssh dev, copied a file with scp, and reached a port through an -L tunnel — the exact five moves you will make against real servers for the rest of the program.

Common Errors & Fixes

Section 3 of 5 · ~3 min

These three catch nearly everyone in their first week with keys. Read the error text slowly — parsing it is the actual skill.

Common error: Loosening a private key’s permissions — running chmod 644 ~/.ssh/id_ed25519 (or copying a key onto a new box without fixing its mode) — then trying to connect prints:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/home/pushkar/.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.

Why: A private key is a secret. If any user but the owner can read it, SSH assumes it may be compromised and refuses to use it — better to fail loudly than to authenticate with a key the whole machine could copy.

Fix: chmod 600 ~/.ssh/id_ed25519. The private key must be readable only by you; the matching .pub can stay 644.

How you’d spot it in prod: After moving keys to a new CI runner or bastion host, SSH silently falls back to a password prompt or fails outright. Check the key file’s mode first — git and rsync copies frequently reset permissions to 644.

Common error: Installing the public key but leaving ~/.ssh group-writable on the server, so login still asks for a password. Running ssh -v dev whoami shows:

debug1: Authentications that can continue: publickey,password
debug1: Offering public key: /home/pushkar/.ssh/id_ed25519 ED25519 SHA256:6q3Yl0m2f8k9wZ1c...
debug1: Authentications that can continue: publickey,password
pushkar@localhost's password:

Why: sshd’s StrictModes check rejects authorized_keys if ~/.ssh or the file itself is writable by group or others — anyone with write access could add their own key. So sshd ignores the whole file and falls through to password auth.

Fix: On the server, chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys. The key is offered but never accepted until the modes are right.

How you’d spot it in prod: Key auth “works on my box” but a teammate’s identical setup keeps prompting for a password. The difference is almost always permissions on the remote ~/.ssh, not the key.

Common error: Reconnecting to a host that was rebuilt (new OS image, so a new host key) with the old fingerprint still cached prints:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    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:9j2Kp0mZ4f8k9wZ1c...
Please contact your system administrator.
Offending ED25519 key in /home/pushkar/.ssh/known_hosts:3
Host key verification failed.

Why: SSH pins each host’s key in ~/.ssh/known_hosts on first connect. If the key later differs, SSH cannot tell a legitimate rebuild from a man-in-the-middle attacker impersonating the server, so it refuses to connect — that refusal is the feature working.

Fix: If you know the server was rebuilt, drop the stale entry: ssh-keygen -R localhost (or the real hostname), then reconnect and accept the new key. If you did not expect a change, stop and investigate before typing anything.

How you’d spot it in prod: After an image upgrade or auto-scaling replaced a node, every engineer and CI job hits this at once. The fix belongs in automation — clear the old host key as part of the rebuild, or manage known_hosts centrally.

SSH Interview Questions

Section 4 of 5 · ~1 min

Cover the answers below and say your own version out loud first — explain how a key pair proves identity before you reveal each answer. Recalling before revealing is what makes these stick when an interviewer asks them cold. The four questions and answers render right after this note.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~25 more minutes today:

  • 5 min — Run man ssh_config and skim the option list. You have used Host, HostName, User and IdentityFile today; see how many more (ProxyJump, ForwardAgent, ServerAliveInterval) exist for later.
  • 5 min — Run ssh -G dev in your terminal. It prints the effective configuration SSH resolved for the dev alias — the fastest way to debug a config that is not behaving.
  • 15 min — Read the SSH — Secure Shell section of the Linux for DevOps guide for hardening sshd_config, disabling root login, and using an SSH agent.
Why is SSH key authentication more secure than a password? Both

A password is a shared secret: the server stores a hash of it and you send it on every login, so it can be guessed, phished, reused across sites, or typed into the wrong window. Key authentication never sends a secret. Your private key stays on your laptop; the server only ever holds your public key. When you connect, the server sends a random challenge and your client signs it with the private key — the server verifies the signature against the public key. Nothing reusable crosses the wire, so an attacker sniffing the connection or breaching the server learns nothing that lets them log in as you later.

What goes in authorized_keys, and why do its permissions matter? Service

authorized_keys lives in the remote user's ~/.ssh directory and holds one public key per line — every key listed there is allowed to log in as that user. You install a key by appending its .pub line to the file. Permissions matter because SSH runs a check called StrictModes: if ~/.ssh is group- or world-writable, or authorized_keys is writable by anyone but the owner, sshd silently ignores the file and refuses the key. The safe values are 700 on ~/.ssh and 600 on authorized_keys. When key auth mysteriously fails and you fall back to a password prompt, wrong permissions on the server side are the first thing to check.

What does ssh -L do, and when would you use it? Both

ssh -L is local port forwarding: it opens a port on your own machine and tunnels everything sent to it, through the encrypted SSH connection, to a destination reachable from the server. ssh -L 5432:localhost:5432 dev makes the server's PostgreSQL, which only listens on its own localhost, appear on your laptop's localhost:5432. You use it to reach internal services — databases, admin dashboards, a metrics endpoint — that are deliberately not exposed to the internet, without opening a firewall port. The traffic is encrypted end to end because it rides inside SSH, so it is far safer than exposing the service publicly just to reach it.

Why choose ed25519 over RSA for a new key today? Product

ed25519 is a modern elliptic-curve signature scheme. It gives strong security in a tiny, fixed-size key — the public key is one short line — where an equivalent RSA key must be 3072 or 4096 bits to be comparable, making it slower to generate and verify. ed25519 keys are fast, have no weak-parameter footguns, and are supported everywhere modern: OpenSSH, GitHub, GitLab, every cloud. RSA is still fine and you will meet it on older systems, so keep the ability to read an RSA key, but for anything you generate in 2026 the default answer is ssh-keygen -t ed25519. GitHub's own docs recommend it.

Mark Day 13 complete

Tomorrow you play: DNS Detective

Stuck on today’s lab? Ask in Mission 90 Q&A