Web server security – Part 1: Basic hardening

Web server security – Part 1: Basic hardening

A web server is just another computer and requires basic security configuration. In the first part, we show you how to secure your SSH access and configure your firewall. You can use this configuration for any type of server.


  1. Requirements
  2. Step by step guide for basic server security
  3. Summary
  4. External files
  5. Changelog

Always stay in the loop!
Subscribe to our RSS/Atom feeds.


  • server with operating system installed (e.g. Debian, Ubuntu, CentOS)
  • basic knowledge of shell commands
  • SSH client on your client
  • security token (YubiKey, Nitrokey) or app (FreeOTP) with OATH-TOTP support
  • nmap (optional)

We will use Debian 9 in this series.

Step by step guide for basic server security

First of all, you have to install a server operating system. After that, you will be able to log in using SSH on your computer: ssh root@[IP-address-of-your-server].

When connecting for the first time, SSH will ask for your confirmation that you connect to the right server. Type yes. After confirming, you have to enter your initial password. That’s all.

Step 0: Check for updates

When you are connected to your server for the first time, use apt (Advanced Package Tool or the package manager of your OS) to check for updates: sudo apt update and install updates, if available.

Step 1: Install and configure ufw (your firewall)

Download and install ufw (Uncomplicated Firewall) since ufw is one of the easiest ways to configure your firewall: sudo apt install ufw.

Then, you can check the status of ufw: sudo ufw status which will most likely show Status: inactive.

Change the default rules to:

  • sudo ufw default deny incoming
  • sudo ufw default allow outgoing

Now you have to enable the SSH port 22:

  • sudo ufw allow ssh

However, this enables this port for everyone on the internet. Every possible IP address can connect to port 22 of your server. There are hundreds of automated bots on the internet which try to connect to port 22 in order to get into your server. We observed these bots for two hours and saw about 8,000 login attempts. A good practice is to enable SSH just for your personal IP address:

  • sudo ufw allow from [IP-address-of-your-client] to any port 22

The problem is that your ISP may change your public IP address from time to time. This means that you are unable to connect to your server until you get your configured IP address back. You can observe your IP address for several days and most likely you will see that it doesn’t change much. So, you likely want to allow a subnet to connect with your server:

  • sudo ufw allow from [IP-address-of-your-client]/[subnet] to any port 22

This is the common CIDR notation. Your suffix [subnet] will be “24” to “26” in most cases. This drastically restricts the number of hosts which can connect to your port 22.

Finally, you have to start and enable ufw: sudo ufw enable. Confirm that this command may disrupt existing SSH connections by entering y. You should see Firewall is active and enabled on system startup.

Please note that we only allow connections to port 22 so far. Later, we will also enable ports 80 (HTTP) and 443 (HTTPS), so people can connect to your website. If you don’t plan to run a web server but other server software, only allow the according port number(s).

Step 2: Create a non-root account

A basic security measure of all operating systems is to use non-root accounts for the most time. This ensures that an attacker doesn’t get all privileges if he exploits a program which runs in a non-root context.

On Debian, enter sudo adduser [username]. You will be prompted for a new password twice. Skip the user information by pressing Enter several times if you are the only user of this server and confirm this by pressing Y.

The new user doesn’t have the possibility to get administrative rights by default. To change this, you have to add the new user to the sudo group (other operating systems call this group wheel). Enter sudo usermod -aG sudo [username] to add the new user to the sudo group. You can see all groups of a user by entering sudo groups [username]. In some cases, you also have to install sudo itself: sudo apt install sudo.

Finally, switch to your new account: sudo su [username]. From now on, you have to explicitly enter root mode by preceding each command with sudo, if necessary.

Step 3: Harden your SSH configuration

The default SSH configuration allows password-based and root login. It is considered more secure to allow only non-root users and use keys instead of passwords. Furthermore, there are dozens of legacy algorithms allowed by default which we disable.

Step 3a: Switch from passwords to keys

You have to decide if you want to use RSA keys or newer Ed25519 keys. There is no clear recommendation for either RSA or Ed25519 and there is no big difference when using them.

Generate a key pair on your client, not on your server:

  • ssh-keygen -b 4096 -t rsa -f ~/.ssh/id_[servername] (4096 bit RSA key pair)
  • ssh-keygen -t ed25519 -f ~/.ssh/id_[servername] (256 bit Ed25519 key pair)

This will generate either a RSA or Ed25519 key pair and store it in your local “~/.ssh/” folder. You can optionally enter a password which you must enter each time you use your SSH key to connect to your server. This password is used to encrypt your private key using 128 bit AES.

You will find two files in “~/.ssh/”:

  • id_[servername] (private key)
  • id_[servername].pub (public key)

You now have to copy the public key to the server by using the following command:

  • ssh-copy-id -i ~/.ssh/id_[servername].pub [username]@[IP-address-of-your-server]

You will have to enter the password of [username] and finally you will see: “Number of key(s) added: 1”. Immediately, open a second terminal windows and try to connect to your server without logging out before: ssh [username]@[IP-address-of-your-server]. If configured correctly, there will be no password prompt since your new key is used for authentication.

Step 3b: Configure sshd

When you successfully checked that your new key is in use, connect to your server using SSH and open the configuration file of sshd: /etc/ssh/sshd_config. Be sure that you actually open sshd_config. ssh_config (no d) is the configuration file for clients.

Change the file accordingly:

  • #PasswordAuthentication yesPasswordAuthentication no (disable password-based login)
  • PermitRootLogin yesPermitRootLogin no (disable root login)
  • #LogLevel INFOLogLevel VERBOSE (log login attempts)

Below # Ciphers and keying add:

# Ciphers and keying
#RekeyLimit default none
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,rsa-sha2-256,rsa-sha2-512,ssh-rsa-cert-v01@openssh.com

Below # Authentication: add:

# Authentication:
AllowUsers [username]
AuthenticationMethods publickey

Now, restart sshd: sudo systemctl restart sshd (or use the command of your operating system). Immediately, open a second terminal windows and try to connect to your server without logging out before: ssh [username]@[IP-address-of-your-server]. If configured correctly, you are logged in without any prompts or errors.

Step 4: Enable 2FA for SSH

For even more security, we will enable two-factor authentication using OATH-TOTP now. Connect with your server.

First of all, we have two install libpam-google-authenticator. This is a module for PAM (pluggable authentication module): sudo apt install libpam-google-authenticator. This also installs libqrencode3.

After installing, configure the module by entering: google-authenticator. When asked “Do you want authentication tokens to be time-based (y/n)”, confirm by pressing y.

Configuring google-authenticator for the first time.
Configuring google-authenticator for the first time. (🔍 Zoom in)

Recommended configuration is:

  • Do you want me to update your “/home/[username]/.google_authenticator” file (y/n) → y
  • Do you want to disallow multiple uses of the same authentication token? → y
  • … we allow an extra token before and after the current time … Do you want to do so? (y/n) → n
  • Do you want to enable rate-limiting (y/n) → y

Now, scan the provided QR code using a TOTP app. Write down the emergency codes and store them in a secure place.

OMG, there is a Google link and Google knows my secret! Yes, there is actually a Google link. You will find your QR code there, on a Google website. Does this mean that Google knows your secret? No, since this link is generated locally on your server. If you don’t click it, no data will be transferred to Google. Google can display any QR code using this URL.

Finally, we must tell SSH that there is a 2FA PAM module. Open your /etc/pam.d/sshd file on the server:

  • change @include common-auth#@include common-auth
  • add auth required pam_google_authenticator.so to the bottom of the file

Save the file and quit.

Now, open /etc/ssh/sshd_config as before. Change:

  • ChallengeResponseAuthentication noChallengeResponseAuthentication yes
  • AuthenticationMethods publickeyAuthenticationMethods publickey,keyboard-interactive

Save all changes, quit and restart sshd: sudo systemctl restart sshd. When you try to connect to your server in future, there will be a “Verification code:” prompt. Enter the TOTP generated by your app or your emergency codes when you lost your app/phone etc.

On GitHub, we provide a Gist showing a minimal configuration file: sshd_config on GitHub.

Step 5: Back up your SSH keys

As always, it is important to back up your cryptographic keys. On your client, go to ~/.ssh/ and back up all files in there. You should also back up the /etc/ssh/sshd_config of your server.

Follow us on Mastodon:

Step 6: Test the configuration

Finally, we want to test our configuration. Most of the configuration like “Does SSH use my public key?” or “Does SSH use 2FA?” can directly be tested by restarting sshd and connecting with your server again. But you can’t see if SSH uses only modern algorithms.

One possibility is to use built-in commands:

  • on your client, use ssh -G [hostname] to view all parameters in use
  • on your server, use sudo sshd -T to view all parameters in use

Furthermore, there are several online tools available, however, we blocked most IP addresses. So we must use a tool on our local computer which is allowed to connect to port 22. nmap is perfect for this test. Enter nmap -Pn --script ssh2-enum-algos [IP-address-of-your-server]. nmap will show:

  • kex_algorithms: (2)
  • server_host_key_algorithms: (1)
  • encryption_algorithms: (3)
  • mac_algorithms: (3)
  • compression_algorithms: (2)
Use nmap to see which algorithms are used by SSH.
Use nmap to see which algorithms are used by SSH. (🔍 Zoom in)

Step 7: Configure your local SSH client

After configuring and testing server-side configuration, we can still improve our SSH setup on our local SSH client. Open ~/.ssh/config on your client. This file contains the user-specific SSH configuration on your local machine.

The idea is to create a list with global parameters, and server-specific configuration. Let’s have a look at the following example:

# ~/.ssh/config
# Global parameters, valid for all hosts
Host *
    HashKnownHosts yes
    VisualHostKey yes
    IdentitiesOnly yes
    KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
    Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
    MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
    HostKeyAlgorithms ssh-ed25519,rsa-sha2-256,rsa-sha2-512,ssh-rsa-cert-v01@openssh.com

# Host-specific parameters, only valid for this specific host
Host ishblog
    VerifyHostKeyDNS yes
    LogLevel VERBOSE
    HostName infosec-handbook.eu
    User remoteuser
    IdentityFile /home/localuser/.ssh/id_ishserver

# Host-specific parameters, only valid for this specific host
Host turrisomnia
    LogLevel QUIET
    User remoteuser
    IdentityFile /home/localuser/.ssh/id_turris

As you can see, the configuration contains global parameters as well as two host-specific entries. These parameters are:

  • HashKnownHosts: Hash all host names and addresses when they are added to ~/.ssh/known_hosts (doesn’t affect existing names and addresses). The idea is to protect identifying information in the file.
  • VisualHostKey: Shows an ASCII art representation of the remote host key fingerprint at login.
  • IdentitiesOnly: Only use the authentication identity files configured in the ssh_config files (~/.ssh/config and /etc/ssh/ssh_config) even if there are more identify files available.
  • KexAlgorithms, Ciphers, MACs, HostKeyAlgorithms: Specific algorithms for different purposes as explained above.
  • VerifyHostKeyDNS: Uses DNS and SSHFP to validate the remote host key. This is only useful if you set a server-side SSHFP resource record.
  • LogLevel: There are different log levels. Valid levels are QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG (DEBUG1), DEBUG2, and DEBUG3. The default is INFO.
  • HostName: The real host name to log into. IP addresses are also valid, however, you must change them if DNS configuration (A, AAAA resource records) changes.
  • User: The remote user name that is used to log in as.
  • IdentityFile: The local identity file used for authentication. We generated these in step 3a.

For more parameters, have a look at the man page of ssh_config.

Additionally, you can configure your local /etc/ssh/ssh_config file, if needed. This configuration is valid for all local users.

This article is part of the "Web server security" series.
Read other articles of this series.


Done! You configured your firewall, so only your client can connect with your server using port 22. Moreover, you hardened your SSH configuration and tested it. Finally, you created a backup. An attacker has find a valid IP address to connect with your server and then he has to provide the correct private RSA/Ed25519 key and the TOTP.

Keep in mind that SSH keys are like passwords: Store them securely, only use them on trusted devices, generate new keys from time to time and securely delete old SSH keys.

Advanced users can set an SSHFP record (Secure Shell fingerprint record) on their DNS server.

In part 4 of this series, we show you how to use Fail2ban to block brute-force attacks.

External files


  • Feb 9, 2019: Added built-in SSH commands to view parameters in use.
  • Jan 16, 2019: Added more complete client-side configuration.
  • Jan 7, 2019: Removed -o flag since the OpenSSH format for private keys is used by default (OpenSSH 7.8+). Added information how the private key is encrypted.

See also