SSH (Secure Shell) is the protocol used to reach another computer safely over a network, perform remote operations, and transfer files. It swept away the eavesdropping, tampering, and impersonation issues that Telnet, rlogin, and other cleartext protocols had. This article walks through, in practical terms, the three-step connection, public-key authentication, file transfer, port forwarding, and the minimum settings worth getting right.
Where SSH fits #
SSH runs on TCP port 22 and is standardised in RFC 4253. It is essential infrastructure for modern development — server operations, pushing to GitHub, CI/CD deployments. OpenSSH (the OpenBSD-originated open-source implementation) ships with virtually every Linux distribution, macOS, and Windows 10 or later, and is the de facto standard.
Telnet sends usernames, passwords, and command output across the network in cleartext. Anyone running tcpdump on the same segment could see the credentials in plain sight. SSH solved this completely by integrating encryption + host authentication + user authentication into a single protocol.
The three-step connection #
The SSH connection sequence runs in this order. Internalise this skeleton and you've essentially understood how SSH works.
~/.ssh/known_hosts.ChaCha20-Poly1305 or AES-GCM.Try it yourself — step through from the TCP connection to key exchange, host auth, and user auth until the encrypted shell is established.
Host authentication — the TOFU model #
On a first-time connection the client is presented with a previously-unknown host-key fingerprint. The fingerprint is saved into known_hosts, and subsequent connections verify the match. This is the TOFU (Trust On First Use) model.
The authenticity of host 'example.com' can't be established.
ED25519 key fingerprint is SHA256:abcd1234...
Are you sure you want to continue connecting (yes/no/[fingerprint])?TOFU's weak spot is "that very first connection". In strict environments, server administrators publish the fingerprint via a separate channel (internal wiki / an authenticator app) in advance, and the user visually confirms that the value shown on first connection matches.
User authentication — two methods #
| Method | How it works | Recommendation |
|---|---|---|
| Password authentication | Username + password are sent over the encrypted channel | △ a brute-force magnet |
| Public-key authentication | The client signs with its private key, the server verifies with the public key | ◎ the standard for production |
In production the standard practice is to disable password authentication and allow only public-key authentication.
How public-key authentication works #
For public-key authentication, a key pair is generated on the client side.
- Private key — never leaves your hands
- Public key — copied to the server in advance
$ ssh-keygen -t ed25519 -C "your@email"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/user/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Your identification has been saved in /home/user/.ssh/id_ed25519
Your public key has been saved in /home/user/.ssh/id_ed25519.pub
# private key: ~/.ssh/id_ed25519 (never let this leave your machine)
# public key: ~/.ssh/id_ed25519.pub (copy this to the server)Short keys, strong, and fast to generate and verify. Unless compatibility forces your hand, just pick Ed25519 today. Fall back to RSA-4096 only when you really do have to talk to OpenSSH older than 7.0.
Register the public key on the server #
$ ssh-copy-id user@example.com
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/user/.ssh/id_ed25519.pub"
Number of key(s) added: 1
# gets appended to ~/.ssh/authorized_keys on the server (and permissions are set to 600)The challenge-response flow #
authorized_keys. If it matches, authentication succeeds.The signing happens entirely on the client side. The private key itself never touches the wire. That's the fundamental reason it's overwhelmingly stronger than password authentication. The challenge is random every time, so it's also immune to replay attacks.
Basic commands #
# standard connection
$ ssh user@example.com
# specify port
$ ssh -p 2222 user@example.com
# specify key file
$ ssh -i ~/.ssh/special_key user@host
# single-command execution (no interactive shell)
$ ssh user@host 'uptime'
# verbose logging (useful when troubleshooting)
$ ssh -vvv user@host# generate a new key
$ ssh-keygen -t ed25519
# change the passphrase
$ ssh-keygen -p -f ~/.ssh/id_ed25519
# check the public key fingerprint
$ ssh-keygen -l -f ~/.ssh/id_ed25519.pub
# remove an old host key from known_hosts after a key rotation
$ ssh-keygen -R example.comTyping the passphrase every time gets tedious. Start ssh-agent, run ssh-add ~/.ssh/id_ed25519 to decrypt the key once, and the rest of the session is passphrase-free. Never use a key without a passphrase — that's the iron rule.
File transfer #
There are three file-transfer methods that ride on top of the SSH encrypted channel. Pick by use case.
# scp — one-shot copy (simple, good for small transfers)
$ scp local.txt user@host:/path/
$ scp -r local-dir/ user@host:/path/
# sftp — interactive file operations (ls / cd / get / put)
$ sftp user@host
sftp> get remote.txt
sftp> put local.txt
# rsync over SSH — incremental transfer (ideal for lots of files / backups)
$ rsync -avz -e ssh src/ user@host:/dst/Port forwarding #
A feature that tunnels traffic from another application through the SSH encrypted channel. There are three patterns.
| Pattern | Flag | Typical use |
|---|---|---|
| Local forward | -L |
Reach a remote DB through a bastion as if it were local |
| Remote forward | -R |
Temporarily expose a local app via an external server (ngrok-like) |
| Dynamic | -D |
SOCKS proxy — route a browser's egress through SSH |
# connect to bastion, forwarding local 5432 to whatever 5432 the bastion can see
$ ssh -L 5432:localhost:5432 user@bastion.example.com
# in another terminal, just run psql against localhost
$ psql -h localhost
psql (16.0)
postgres=#Multi-hop bastions — ProxyJump #
# one hop — internal via bastion
$ ssh -J user@bastion user@internal-server
# multi-hop — b1 → b2 → target
$ ssh -J user@b1,user@b2 user@target
# the old ProxyCommand syntax (now superseded by -J, no longer recommended)The minimum server settings #
In /etc/ssh/sshd_config, make sure these three lines are in effect.
# /etc/ssh/sshd_config
PermitRootLogin no # no direct root login
PasswordAuthentication no # disable password auth (public key only)
PubkeyAuthentication yes # enable public-key authentication
# always syntax-check before applying
$ sudo sshd -t
$ sudo systemctl reload sshdAfter changing the config, do not close the current session. Open a new terminal, reconnect, confirm the new session works, and only then disconnect the old one. There are countless stories of someone setting PasswordAuthentication no, fat-fingering something else, and being unable to log back in.
Handy ~/.ssh/config tricks #
Give each destination an alias and you can just type ssh internal. Once your inventory grows beyond a few servers — whether for a team or personally — this becomes essential.
# bastion
Host bastion
HostName bastion.example.com
User admin
Port 2222
IdentityFile ~/.ssh/id_ed25519_admin
# internal server (reached via the bastion using ProxyJump)
Host internal
HostName 10.0.0.5
User deploy
IdentityFile ~/.ssh/id_ed25519_deploy
ProxyJump bastion
# now ssh internal is enough to reach the internal serverReferences #
- "Zukai Nyumon TCP/IP, 2nd edition: Mechanisms and Behaviour by Sight" — section 6-5-2 SSH (Management-Access Protocol)
- OpenSSH official manual
- RFC 4253 — The Secure Shell (SSH) Transport Layer Protocol