A guideline for SSH

SSH is the tool I reach for every day. This post collects the parts I use most: connecting, saving hosts in a config file, changing the port, moving to key-based login, disabling password login for root, and even sending a mail on every login.

Hosts used in this guide

hostIP address
client192.168.0.1
server192.168.0.254
e-mail server192.168.0.253

Connect with SSH hosts

ssh 192.168.0.254

Save connections

To avoid typing the same details every time, I store and label my hosts in the user's.ssh/config file:

~/.ssh/config

A minimal host entry looks like this:

Host HOST
  Hostname 192.168.0.254
  Port 22
  User root

With that saved, I connect by just passing the label:

ssh HOST

And I can override any stored parameter on the command line:

ssh -p 111 user@HOST

Change configuration on the fly

The main configuration file for SSH on a Debian system is:

/etc/ssh/sshd_config

Tip: if you are already logged in and change the SSH config — even the port — you can restart the service safely. Your current session stays up until you log out, so you always have a way back in if something is wrong.

Restart the service

To apply the changes, I restart the service:

service ssh restart

Change your SSH Port

The default SSH port is22, which every bot on the internet already knows. Changing it does not stop a determined attacker, but it cuts the brute-force noise in the logs to almost nothing. I find thePort line in the main config file and change it:

Port 22

Hide the port with knocking

Changing the port cuts the noise, but the port is still open to anyone who scans for it. With port knocking the SSH port stays closed in the firewall until I send a secret sequence of connection attempts to a set of ports. Only then does the firewall briefly open SSH — and only for my address.

I useknockd for this:

apt install knockd

The config defines the knock sequence and the firewall rule it triggers. One sequence opens the port, the reverse sequence closes it again:

[options]
    UseSyslog

[openSSH]
    sequence    = 7000,8000,9000
    seq_timeout = 5
    command     = /sbin/iptables -A INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
    tcpflags    = syn

[closeSSH]
    sequence    = 9000,8000,7000
    seq_timeout = 5
    command     = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
    tcpflags    = syn

The firewall's default policy for the SSH port staysDROP, so without the right sequence the port does not even answer. From my client I knock in order, then connect as usual:

knock <server> 7000 8000 9000
ssh <server>

Port knocking is obscurity, not a real access control — but combined with keys it keeps the port off the radar of automated scanners entirely.

Use SSH without a password

Instead of a password I use a key pair. RSA still works, but I switched toEd25519 — it is the modern default: fast, short keys, and no weak parameter choices to get wrong. First I create the pair:

ssh-keygen -t ed25519

The public key goes onto the remote hosts; the private key stays with me and lets me log in to all of them. That also means anyone who steals the private key can reach every host, so it is worth protecting it with a passphrase. To skip the passphrase, press Enter through the prompts:

Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/test/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/test/.ssh/id_ed25519
Your public key has been saved in /home/test/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:nm9VTWqhaFf8yeOmjq0Xt0X74aa574WpkVL5dJvk2c4 test@testpc
The key's randomart image is:
+--[ED25519 256]--+
|             .   |
|          .   + .|
|         o o =.*.|
|        . B * O++|
|       .S* o B.oo|
|       .o.o . o=.|
|        o. +  +E+|
|         .= .+.+.|
|         ++++=*  |
+----[SHA256]-----+

Then I copy the public key to a host:

ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 root@192.168.0.254

It prompts once for the password, then copies the key over:

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

Number of key(s) added: 1

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

Prevent root login via password

Once the key is in place, there is no reason to allow password login for root anymore. I set this parameter toprohibit-password:

PermitRootLogin prohibit-password

The parameter has these options:

  1. yes — root can log in with a password
  2. no — root cannot log in over SSH at all
  3. without-password orprohibit-password — root can log in, but only with a key pair

After restarting SSH, a host that does not have the private key (.ssh/id_ed25519) can no longer log in — exactly what I want.

Use only modern crypto

Even with keys, the server still offers a range of ciphers and key-exchange algorithms for backward compatibility, and some of the older ones are weak. I restrict the server to the modern set in the main config file:

KexAlgorithms          curve25519-sha256,curve25519-sha256@libssh.org
Ciphers                chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr
MACs                   hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
HostKeyAlgorithms      ssh-ed25519,ssh-ed25519-cert-v01@openssh.com
PubkeyAcceptedKeyTypes ssh-ed25519,ssh-ed25519-cert-v01@openssh.com

I also drop the RSA and ECDSA host keys and keep only the Ed25519 one, so nothing weaker is ever negotiated:

rm /etc/ssh/ssh_host_rsa_key* /etc/ssh/ssh_host_ecdsa_key*

Ban brute-force with fail2ban

For the attempts that still reach SSH,fail2ban watches the auth log and bans an IP after too many failed logins:

apt install fail2ban

I enable thesshd jail in a local override, so a package update never overwrites it:

# /etc/fail2ban/jail.local
[sshd]
enabled  = true
maxretry = 3
findtime = 10m
bantime  = 1h

After a restart, three failed logins from an address get it banned for an hour. I can watch the jail with:

systemctl restart fail2ban
fail2ban-client status sshd

Send mail on SSH login

I also wanted a heads-up whenever someone logs in. This file runs on every successful SSH login:

/etc/ssh/sshrc

The script mails the IP address of the user who just logged in, and writes a line to syslog as well:

ip=`echo $SSH_CONNECTION | cut -d " " -f 1`
    # Get the IP Adress of the connected user

logger -t ssh-wrapper $USER login from $ip
    # Log to syslog

echo "User $USER just logged in from $ip" | sendemail -q -u "SSH Login" -f "Originator" -t "Login <logins>" -s 192.168.0.253 &
    # Send mail

A note on OAuth

Keys, a moved port, knocking and fail2ban are the practical layers I actually run. I have also played around with OAuth/OIDC on top of this: logging in through an identity provider and handing out short-lived SSH certificates instead of managing static keys per machine. For more than a handful of hosts that is a cleaner model — no key sprawl, access expires on its own — but it is a bigger topic than a config tweak, so I will give it its own post once I have run it properly.

Author:René Zingerle,CISSP,SSCP
Last Update: 20.07.2022