3 · SSH properly¶
Every automation tool — Ansible, Netmiko, NAPALM, your own scripts — is SSH underneath. Setting it up properly is the difference between automation that runs unattended and automation that stops to ask for a password.
Keys, not passwords¶
A key pair is two files: a private key that never leaves your machine, and a public key you copy to every device. The device challenges you to prove you hold the private one.
ssh-keygen -t ed25519 -C "netops@example.com"
Use ed25519 — shorter, faster and stronger than RSA, supported by anything
modern. Fall back to -t rsa -b 4096 only for genuinely old gear.
ssh-copy-id user@10.0.0.1 # installs the public key
ssh user@10.0.0.1 # no password
If ssh-copy-id isn't available, append ~/.ssh/id_ed25519.pub to the device's
~/.ssh/authorized_keys by hand.
Permissions are enforced, and the error doesn't say so
SSH refuses to use keys with loose permissions and often fails without explaining why:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 # private key
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/authorized_keys
Private key 600, directory 700. When key auth "just doesn't work,"
check this first — then run ssh -v and read where it gives up.
The agent: unlock once¶
A passphrase-protected key is the right choice, but you don't want to type the passphrase on every connection. The agent holds the decrypted key in memory:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-add -l # what's loaded
Agent forwarding (-A) lets a remote host use your local keys to connect
onward — convenient for jump hosts, but anyone with root on that host can use your
agent while you're connected. Prefer ProxyJump below.
~/.ssh/config — where the real gain is¶
Stop typing connection details. This file is read by ssh, scp, rsync, Ansible and most tooling:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Host jump
HostName bastion.example.com
User netops
IdentityFile ~/.ssh/id_ed25519
Host r1 r2 r3
User admin
IdentityFile ~/.ssh/id_ed25519
ProxyJump jump
Host legacy-*
# old gear that needs deprecated algorithms
KexAlgorithms +diffie-hellman-group1-sha1
HostKeyAlgorithms +ssh-rsa
Now ssh r1 works — correct user, correct key, automatically via the bastion.
| Setting | Why |
|---|---|
ProxyJump |
reach devices through a bastion, no agent forwarding |
ServerAliveInterval |
stop idle sessions being dropped by firewalls |
IdentityFile |
which key for which device |
ControlMaster |
reuse one connection for many commands — big speedup |
Connection multiplexing is worth enabling when scripting against the same host repeatedly:
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
The first connection authenticates; subsequent ones reuse the open channel and start instantly. Ansible gets noticeably faster.
Jump hosts¶
ssh -J jump user@10.0.0.1 # one hop
ssh -J jump1,jump2 user@target # chained
ProxyJump beats agent forwarding because your private key never reaches the
bastion — the traffic is tunnelled, the authentication stays local.
Running commands remotely¶
ssh r1 "show ip bgp summary" # one command, then exit
ssh r1 "show running-config" > r1.cfg # capture output
ssh r1 <<'EOF' # multiple commands
configure
router bgp 65001
EOF
At scale, in parallel, with a timeout so one dead device can't stall the run:
cat devices.txt | xargs -P8 -I{} timeout 10 ssh -o BatchMode=yes {} "show version"
BatchMode=yes belongs in every script. It makes SSH fail immediately rather
than prompting for a password — otherwise an unattended job hangs forever waiting
for input nobody will type.
Host keys¶
On first connection SSH asks you to verify the host's fingerprint, then stores it in
~/.ssh/known_hosts. If it later changes, you get:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
Usually that's a rebuilt or replaced device. Occasionally it's genuinely someone intercepting the connection.
ssh-keygen -R 10.0.0.1 # remove the stale entry, then reconnect
Don't reach for StrictHostKeyChecking=no
It's the common workaround in lab scripts and it disables the protection entirely — you'll connect to anything claiming to be that address.
For genuinely disposable lab devices, scope it narrowly:
Host clab-*
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Never apply that to Host *, and never to anything in production.
When it doesn't work¶
ssh -v r1 # verbose; -vvv for more
Read for the line where it stops. The useful ones:
| Message | Means |
|---|---|
Permission denied (publickey) |
key not accepted — check permissions and authorized_keys |
Connection refused |
nothing listening — service down or wrong port |
Connection timed out |
filtered or unreachable — firewall or routing |
no matching key exchange method |
old device, modern client — add KexAlgorithms |
Host key verification failed |
key changed — ssh-keygen -R if expected |
The distinction between refused and timed out is worth internalising: refused means something answered and said no (host reachable, service down); timed out means nothing answered at all (routing or firewall). They point at completely different problems.