SSH
SSH is the encrypted channel every server administrator lives inside. One protocol carries an interactive shell, file transfer (scp, sftp, and rsync tunnelled over it), arbitrary TCP port forwarding, and the authentication that decides who gets in. On Debian and Ubuntu the daemon ships in the openssh-server package, runs as the ssh systemd unit, listens on TCP port 22, and reads its configuration from /etc/ssh/sshd_config. The client is openssh-client, configured per-user in ~/.ssh/config.
Almost everything that goes wrong with SSH is a key, a permission, or a configuration problem — not the network. Get the keypair, the file modes, and the agent right and remote access is silent and instant; get them wrong and you are typing passwords into every host, copying private keys onto bastions, or locked out of a box you just hardened. The protocol itself is solid; the operational surface around it is where the time goes.
The Protocol and Client/Server
Two programs make up the system. sshd is the daemon that listens for connections; ssh is the client that opens them. They negotiate a session key, then run everything — keystrokes, file bytes, forwarded ports — inside that encrypted tunnel. The same connection multiplexes all of it, which is why one SSH session can carry your shell and a forwarded database port at once.
On the very first connection to a host, the client has no way to know it is talking to the right machine. The server presents its host key — a public key that identifies the box itself, distinct from any user key — and the client shows you its fingerprint and asks whether to trust it. Say yes and the key is pinned in ~/.ssh/known_hosts; from then on the client verifies it silently and screams if it ever changes. That first prompt is the one moment the protocol cannot protect you, so an attacker who can intercept the initial connection can impersonate the host if you accept blindly.
# connect; first time, you are asked to trust the host key ssh admin@10.0.0.12 # check the daemon and its listening socket on the server systemctl status ssh ss -tlnp 'sport = :22'
On Red Hat and its derivatives the unit is named sshd rather than ssh, and the package is openssh-server there too. The config file path and syntax are identical, so the only thing that changes across distributions is the systemctl unit name you type.
Key-based Authentication
A keypair is two halves of one secret: a private key that never leaves your machine and a public key you copy to every server. The server stores allowed public keys, one per line, in ~/.ssh/authorized_keys under the target account. At login the client proves it holds the matching private key without ever transmitting it, so there is no shared secret crossing the wire and nothing for the server to leak. Generate Ed25519 keys — they are shorter and faster than RSA, and 256-bit Ed25519 is the current default for ssh-keygen.
Keys beat passwords on every axis that matters. A password is phishable, reused across sites, and brute-forceable against an exposed port 22 at thousands of attempts per minute; a 256-bit key is none of those things. Add a passphrase to the private key and a stolen laptop still does not hand over your servers, because the file on disk is encrypted at rest. Once key auth works, you disable password auth entirely on the server and the bots hammering port 22 simply have nothing to guess.
# generate an Ed25519 keypair with a passphrase ssh-keygen -t ed25519 -C "admin@laptop" # copy the public half to the server's authorized_keys ssh-copy-id admin@10.0.0.12
sshd is deliberately paranoid about file permissions. If ~/.ssh is group- or world-writable, or authorized_keys is readable by anyone but the owner, the daemon silently refuses the key and falls back to asking for a password — with no error to the client and a terse line in the server's journal. The directory must be 700 and the file 600, owned by the user, or key auth will not work no matter how correct the key itself is.
The SSH Agent and Config
Typing the key passphrase on every connection defeats the convenience that made you set up keys. The ssh-agent holds your decrypted private key in memory for the session: you unlock it once with ssh-add, and every subsequent ssh, scp, and git push uses it without prompting. The agent never reveals the key — clients ask it to sign challenges, and it signs them in process.
# start an agent and load your key once per session eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519 # list keys currently held by the agent ssh-add -l
The real productivity lever is ~/.ssh/config. Instead of remembering IP addresses, ports, and usernames, you define a Host alias once and connect by name. The most valuable directive is ProxyJump: it tunnels the connection to an internal host through a bastion in a single hop, so your private key authenticates end-to-end and never lands on the jump box. Copying private keys onto a shared bastion — the alternative people reach for — turns one compromised bastion into a compromise of every server those keys reach.
# ~/.ssh/config Host bastion HostName 203.0.113.10 User admin Host db1 HostName 10.0.5.20 User admin ProxyJump bastion
With that in place, ssh db1 reaches an internal host that has no public address at all, routing through the bastion automatically. Agent forwarding (ForwardAgent yes or ssh -A) is the lazier alternative, but it exposes your agent socket to whoever has root on the intermediate host, so prefer ProxyJump for jumps and reserve forwarding for hosts you fully trust.
File Transfer and Tunnels
Because SSH already authenticates and encrypts, file transfer rides the same channel for free. scp copies single files, sftp gives an interactive file session, and rsync over SSH is the one to reach for on anything non-trivial: it transfers only the changed blocks, resumes interrupted runs, and preserves permissions and timestamps. A 50 GB directory that mostly already exists on the far side syncs in seconds with rsync where scp would recopy all 50 GB.
# single file with scp scp report.csv db1:/tmp/ # incremental, resumable directory sync over ssh rsync -avz --progress ./build/ db1:/srv/app/
Port forwarding turns the SSH tunnel into a way to reach services that are not exposed publicly. Local forwarding (-L) brings a remote service to a port on your machine — point a local database client at localhost:5432 and the bytes pop out on the server. Remote forwarding (-R) does the reverse, exposing a local service to the server side. Dynamic forwarding (-D) opens a local SOCKS proxy that routes arbitrary connections through the remote host.
# reach the server's Postgres on your own localhost:5432 ssh -L 5432:localhost:5432 db1 # a SOCKS proxy through the bastion for ad-hoc access ssh -D 1080 bastion
Host Key Verification
The known_hosts file is the client's memory of which key each host presented. On every reconnect the client compares the presented host key against the pinned one; a match is silent, a mismatch is a loud, blocking warning that refuses to connect. That warning has exactly two causes: the server was legitimately rebuilt or reinstalled and got a new host key, or someone is sitting between you and the server presenting their own key — a machine-in-the-middle attack. The protocol cannot tell which, so it stops and makes you decide.
# WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! # someone could be eavesdropping on you right now ... # after a *verified* rebuild, remove the stale entry: ssh-keygen -R 10.0.0.12
The dangerous habit is treating that warning as noise and clearing the entry reflexively. Before you run ssh-keygen -R, confirm out of band that the host really was rebuilt — check the change ticket, the console, the new fingerprint against one recorded at provision time. For a fleet, distribute host keys ahead of time (or use certificate-based host keys signed by a trusted CA) so clients trust new machines without a first-connection prompt at all, and a genuine mismatch is unambiguous.
Password authentication — a shared secret typed at login. Phishable, frequently reused, and brute-forceable against an exposed port 22 at thousands of attempts per minute. Acceptable only on a host that is not internet-reachable, and even then a weak default.
Key authentication — a private key proves identity without transmitting any secret. Not guessable, not phishable, and protected at rest by a passphrase. Use it everywhere, then set PasswordAuthentication no in sshd_config so the bots hammering port 22 have nothing to attempt — the hardening payoff covered in Topic 64.
- Loose permissions on
~/.sshorauthorized_keys— anything more open than700on the directory and600on the file makessshdsilently ignore the key and fall back to a password prompt, with no error to the client and only a terse line in the server journal. - Clearing a changed-host-key warning reflexively with
ssh-keygen -Rbefore confirming the rebuild — if the cause was a machine-in-the-middle and not a legitimate reinstall, you have just waved the attacker through. - Leaving
PasswordAuthentication yeson an internet-facing host — port 22 then takes a continuous stream of credential-stuffing attempts, and one weak account password is all it takes. - Generating private keys with no passphrase on a laptop or shared machine — a stolen or backed-up
id_ed25519is then an unencrypted skeleton key to every server it can reach. - Copying private keys onto a bastion instead of using
ProxyJump— the jump host becomes a single point that, once compromised, hands an attacker every downstream server those keys unlock. - Turning on
ForwardAgentglobally for convenience — anyone with root on an intermediate host can then use your forwarded agent socket to authenticate as you to anything else. - Editing
sshd_configand restarting the daemon while logged in over SSH, without an open second session — a syntax error or a wrongAllowUsersline locks you out of the box with no way back in.
- Generate Ed25519 keys with a passphrase (
ssh-keygen -t ed25519) and distribute the public half withssh-copy-idrather than editingauthorized_keysby hand. - Set
~/.sshto700andauthorized_keysto600on every account, sincesshdrefuses keys with looser modes and gives the client no useful error. - Load keys into
ssh-agentonce per session withssh-addso a passphrase-protected key stays both secure and frictionless. - Define
Hostaliases withProxyJumpin~/.ssh/configto reach internal hosts through a bastion; never copy a private key onto the bastion itself. - Verify a changed host-key warning out of band before removing the
known_hostsentry — treat every mismatch as a possible attack until a rebuild is confirmed. - Use
rsync -avzover SSH for any non-trivial transfer so only changed blocks move and interrupted runs resume, instead of recopying withscp. - Set
PasswordAuthentication noinsshd_configonce key auth works, and keep a second SSH session open while you reload the daemon so a bad edit cannot lock you out.
.ppk key format and Pageant agentKnowledge Check
You add a public key to a server's authorized_keys, but SSH keeps prompting for a password. What is the most likely cause?
- The permissions on
~/.sshorauthorized_keysare too open, sosshdsilently ignores the key and falls back to password auth - Ed25519 keys are not accepted by default and you must regenerate the pair as RSA
- The public key must be added on the client side in
known_hosts, not on the server - Password authentication always takes priority over public-key authentication unless you explicitly disable PasswordAuthentication in sshd_config
Why is ProxyJump bastion preferred over copying your private key onto the bastion to reach an internal host?
- Your private key authenticates end-to-end and never lands on the bastion, so a compromised bastion does not expose the downstream servers
- It encrypts the second hop of traffic between the bastion and the internal host, which a copied key would otherwise leave traveling in plaintext
- It is the only method that works when the internal host has no public IP address
- It removes the need for the internal host to have any entry in
authorized_keys
You connect to a host you use daily and get a loud "REMOTE HOST IDENTIFICATION HAS CHANGED" warning. What does it actually mean?
- The presented host key no longer matches the pinned one — either the host was rebuilt with a new key, or someone is intercepting the connection
- Your private key has reached its built-in expiry date and must be regenerated from scratch before the server will ever let you reconnect to it again
- The server has disabled password authentication since your last login
- The agent has forgotten your key and you need to run
ssh-addagain
For repeatedly syncing a large directory that already mostly exists on the remote host, why is rsync over SSH the right tool rather than scp?
rsynctransfers only the changed blocks and can resume an interrupted run, whilescprecopies every file in full each timersyncopens its own separate encrypted transport channel that is inherently faster than the single SSH tunnel thatscpis forced to ride onscpcannot copy directories at all, only individual filesrsynccompresses the data whereasscpnever encrypts or compresses anything
What is the risk of enabling ForwardAgent when connecting through an intermediate host?
- Anyone with root on that intermediate host can use your forwarded agent socket to authenticate as you to other servers
- Your private key file is copied to and left behind in the intermediate host's
~/.sshdirectory for the duration of the forwarded session - It disables host-key checking for every host reached through the forwarded connection
- The passphrase on your key is transmitted in cleartext to the intermediate host
You got correct