Tutorial: How to Secure the SSH Server

Here is a summary of my personal most important hardening tips concerning the SSH-Server. I want to share the steps with you, and also take them as a reminder for me ;).

I want to harden 3 Parts: The Authentication, the Key-Exchange Algorithms and the the Bruteforce Surface.

⚠️
Please read the following steps as tips, of how to add more security to the ssh-daemon (sshd). Still I want to encourage you to research by yourself. I bet my guide isn't complete at all and your needs could be different as my needs. Don't blindly add lines to your sshd_config, read carefully and think about it. I will try to explain the following steps as good as I can. As I'm most familiar with Debian based systems, the commands could differ a little bit for other Linux Distributions. I will also try to cover the differences as much as I can and know about.

If you order a new server, you will most likely get your credentials for the ssh login via mail or the server-control-panel. On every machine I ordered so far, it's the login for the "root" user with a password. This is by far not a secure way to run a server nowadays, so let's change that!

💡
The "root" user is the "superuser" on a Unix machine. It has by default the highest privilege level and can do whatever on your server. It has access to all the data and can run all commands.

1. Disable the Login as Root

As the "root" user has all the power it needs to just delete your server in a second, we don't want that someone can login as "root" via ssh.

But before we can disable the root-login we have to create a new user!

Create a New User

First, we create a new user which belongs to the group "users" with a working home directory:

useradd -g users -d /home/[username] -s /bin/bash [username]
💡
I use [username] as a variable here. You need to use the following commands with your preferred username without the [] brackets!

Now the user gets a password:

passwd [username]

And we need to create the home-directory and give the newly added user the ownership of it:

mkdir /home/[username]
chown [username]:users /home/[username]/

Optional: Add user to the "sudo" group:

usermod -aG sudo [username]

This is the easiest way and is used on Ubuntu/Debian... CentOS for example uses the "wheel" group. Just use a search engine to lookup the right command for you.

💡
Members of the "sudo" group can execute commands as "root" via the sudo command. These users need to authenticate after the first sudo with their password.

Now we exit and try to login as the new user with the new password.

⚠️
Only if the login as the new user via ssh works do the next steps. You don't want to disable your only possible login!

PermitRootLogin no

We are going to edit the sshd configuration now with our preferred text editor. I'm using nano:

sudo nano /etc/ssh/sshd_config

We change

PermitRootLogin yes

to

PermitRootLogin no

and save the config file.

💡
You can always test your sshd_config for syntax errors before you apply it with a restart of the daemon. To do so use the sshd -t command.

A sample error message:
/etc/ssh/sshd_config: line 26: Bad configuration option: PermitRootLogins

If there is no output after sshd -t there is also no syntax error.

The ssh-daemon needs to get restarted!

sudo service ssh restart  (Ubuntu/Debian)
sudo service sshd restart (Fedora,CentOS)

Now we can test, if the login as the root user is still possible or not!

2. Only allow PubKeyAuthentication

We can add another layer of security, if we just permit the login with a keypair. So nobody can login with just a password no more.

Create the Keys

On Linux/OSX

To create the keys we can use:

ssh-keygen -t ed25519

Now we can enter the filename in which the private key should be saved, or just hit enter for the default .ssh/id_ed25519 keyfile.

We are asked for a passphrase now. This step is optional, though I prefer to also use a passphrase for the key!

After that the public key is saved, for example as .ssh/id_ed25519.pub

💡
With the ssh-keygen tool you can create keys based on different algorithms. In my example above, I decided to create a key based on the ed25519 algorithm. If you want to read more about the different key algorithms, I can recommend a short article by Nicolas Béguir here

On Windows

On Windows we need the tool "Puttygen". It's part of the Putty package and available here

If we start "Puttygen", we can decide which type of key we want to generate, for example RSA, DSA, ECDSA, ED25519 and the number of bits. We pick our flavor and hit the "Generate" button.

Now we need to move the mouse to add randomness to our keys as long as the progress bar becomes fully green.

We can specify a passphrase if we want to.

Now we click the "Save private key" button to save the private key.

On the top of the Puttygen window we can see the public key for pasting into OpenSSH.

On Android/iOS

We can also create our keypairs on our Smartphone. I've used the apps Termius and JuiceSSH to create the keys succesfully.

⚠️
Please keep in mind, that you need to backup your private keys somewhere. I recommend an encrypted container file or a usb-stick. If your private key get lost - you can't login no more!

Add the public key on the server

Now that we created the private and public keypair, we need to add the public key to our user, which should login with it.

To do so, we login as the user and create the .ssh folder into the home directory

mkdir .ssh

now we copy our public key out of the created .pub file or Puttygen and paste it to the file

.ssh/authorized_keys

for example we copy the public key out of the Puttygen window, then use nano on the server:

nano .ssh/authorized_keys

Paste the key, save the file and we are set.

Now need to edit/check the sshd config file again:

sudo nano /etc/ssh/sshd_config

and we change (if needed)

PubKeyAuthentication no

to

PubKeyAuthentication yes

We save the file and restart the ssh-daemon again:

sudo service ssh restart  (Ubuntu/Debian)
sudo service sshd restart (Fedora,CentOS)

Now we try to login with the private Key!

⚠️
Again, only if the login with the key works, do the next step. You still don't want to disable your only possible login!

Disable Password Logins

To completely disable the possibility to login with just a password, we need to edit the sshd config file again:

sudo nano /etc/ssh/sshd_config

and we edit

PasswordAuthentication yes

to

PasswordAuthentication no

Save the config and restart the ssh-daemon again.

3. Change the SSH-Port

Changing the port basically doesn't increase the security, but it will reduce the amount of bots trying to login.

Edit the configfile:

sudo nano /etc/ssh/sshd_config

And right at the beginning, there is the Port option we want to change from 22 to a port starting at 1024, for example:

Port 22

to

Port 31337

Restart/Reload the ssh-daemon to apply the changes.

4. Other Options to edit

Here I will explain other edits I've added to my configuration:

MaxAuthTries 3
💡
MaxAuthTries limits the max number of auth attemps by a session. I'm using 3 as value, because my brain should handle that.
LoginGraceTime 30
💡
LoginGraceTime defines the time in seconds in which the user should add the credentials afer connecting to the server. I'm using 30 seconds here.
X11Forwarding no
💡
As we don't need the forward from the display server X11, we disable this potential attack surface
PermitEmptyPasswords no
💡
It's kinda obvious that we don't want, that a user can login without a password.

5. HostKeys, HostKeyAlgorithms, KexAlgorithms, MACs

The default configuration of the ssh-daemon is, like the default configuration of the majority of applications, not mainly focused on security, first of all it's compatibility.

As I found a great tool called ssh-audit. I discovered four settings I want to share with you, to harden the ssh-authentication:

HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ed25519_key

As default, the sshd server is using the rsa, ecdsa and ed25519 hostkeys. As NIST P-curves (ecdsa) are possibly backdoored, we just uncomment the keys for rsa and ed25519.

HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256

The default HostKeyAlgorithms cover also questionable ones, like ssh-rsa, which is exploitable (SHA-1) and ecdsa-sha2-nistp256, which is, possibly, back-doored. We don't want to use these.

KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org

Kex stands for Key EXchange. The default configuration also covers questionable algorithms, like NIST P-curves, which are, possibly, back-doored. I will just use curve25519 ones.

MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

MAC stands for Message Authentication Code. These algorithms are used for data integrity protection. I will only use MACs with the "-etm" tag, as these "encrypt-then-mac" and not "encrypt-and-mac". etm MACs are considered more secure in terms of some attack types.

Restart/Reload the ssh-daemon to apply the changes.

💡
The "ssh-audit" tool also gives you recommendations of other modern algorithms you can add. So the values I've added in my sshd_config are not the only possible/secure way. Please research for yourself and decide what's the best balance of compatibility and security for your usecase!

A list of available options can be found in the official docs >>here<<

6. Fail2Ban

Fail2ban is an application which scans your logfiles for failed logins orother suspicious activity and bans IP addresses with too many fails for a specific ammount of time. It bans them by updating the system's firewall, in our example iptables.

So basically Fail2Ban limits the unauthorized login attempts, most likely caused by botnets.

Installation

Debian/Ubuntu

sudo apt install fail2ban

Fedora/CentOS (EPEL Repo)

sudo yum install fail2ban

Default Configuration

Fail2Ban has a default configuration file - jail.conf. But this file might get overwritten by updates. So the tool also uses the jail.local file which stays persistent.

First we need to create the jail.local. We do this by copying the jail.conf file:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

And now we edit the configuration with our favorite text editor:

sudo nano /etc/fail2ban/jail.local

Scroll down the [DEFAULT] section and you maybe want to edit these values:

bantime = 720m

Bantime: The time an IP gets banned after too many fails. I set this value to 12 hours

findtime = 120m

Findtime: The time window of the failed login attempts. I set this value to 2 hours

maxretry = 3

MaxRetry; The maximum ammount of failed logins between the findtime. I set this value to 3 attempts

Our config:

An IP gets banned for the next 12 hours, if 3 failed logins were logged from this target during the last 2 hours.

We need to restart fail2ban now, to apply the new config:

sudo service fail2ban restart

The SSHD Jail

In my jail.local the sshd jail is enabled by default (scroll down the config the see the relevant settings)

[sshd]
port      = sshd
logpath   = %(sshd_log)s
backend   = %(sshd_backend)s

fail2ban-client

Fail2Ban also has a client which you can use via ssh to check the status of the application:

sudo fail2ban-client status

This command displays the overall status of fail2ban, in our case:

Status
|- Number of jail:      1
`- Jail list:   sshd

If we want more information about the sshd jail:

sudo fail2ban-client status sshd

and the output is something like this:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 1
|  |- Total failed:     3
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 1
   |- Total banned:     1
   `- Banned IP list:   [the banned IPs here]

It seems that fail2ban is working, as it already banned one IP. YWe could also backcheck with sudo iptables -L and look at the "CHAIN f2b-sshd" - it should also include a REJECT rules for the specific IP.

Conclusion

Here is a summary of the things we have done during this little tutorial:

We hardened:

  • the login

We forbid the login as the "root" user. The server only accepts logins as existing users and only via public key.

  • the authentication / key-exchange

We deleted the unsafe algorithms and only use modern key algorithms for the authentication.

  • the ammount of failed logins / bruteforce attempts

Fail2Ban blocks the IPs of malicious acting targets for a specific amount of time. Therefore these hosts can't try (bruteforce) more login attempts.

Keep in mind that my configuration is likely not a perfect one. It just only reflects my preferences.

Thanks for reading, I hope I motivated you to dig into the sshd_config a little more.

https://man.openbsd.org/sshd_config

https://github.com/jtesta/ssh-audit

https://www.fail2ban.org/wiki/index.php/Main_Page

https://nbeguier.medium.com/a-real-world-comparison-of-the-ssh-key-algorithms-b26b0b31bfd9