TL;DR

I will set up as much protection as possible on my VPS with the help of Cloudflare, strict firewall rules, improved default settings and third party applications such as fail2ban.

Environment

Tested on Debian Based Distribution:

Distributor ID: Debian
Description:    Debian GNU/Linux 11 (bullseye)
Release:        11
Codename:       bullseye

But I assure you that it also works in any Ubuntu, at least in an updated one.

Introduction

When I had the opportunity to manage my own VPS, I was able to confirm all that theory they tell us about the amount of bots and attacks you get when you expose any port to the public.

I had clear ideas, my VPS was going to become my space to run tests, serve my web applications and above all, learn.

I will tell you how I structured my head:

  • Since I had my own domain, I wanted to take advantage of the free solutions offered by Cloudflare.
  • I wanted to set strict rules on my firewall, for example only allowing connections from specific sites/IPs.
  • I wanted to be able to access via SSH, so I would set up a bit stricter settings for this service.
  • And I didn’t want any bots constantly trying to access my server.

Please, take care of the access credentials, all the protections that I am going to explain will cease to have any effect if you do not make use of robust credentials and rotate them with some constancy.

Cloudflare

The configuration is basic and quite simple to apply.

First of all, I will configure so that all requests to my domain, actually go against my server’s IP. These options are found in the left menu: DNS > Records.

For future steps, this DNS configuration is recommended to be done with the proxy redirection parameter enabled.

In the same DNS section but now under Settings I will enable the DNSSEC.

Then, I will force in the left menu: SSL/TLS > Overview the communication in Full (strict) mode to obtain the message:
Your SSL/TLS encryption mode is Full (strict).

Subsequently, under the left menu: SSL/TLS > Overview I will set:

  • Always Use HTTPS = On
  • HTTP Strict Transport Security (HSTS) = All On
  • Minimum TLS Version = TLS 1.2
  • TLS 1.3 = On
  • Automatic HTTPS Rewrites = On

With this we can conclude the Cloudflare part, now whenever you need to add subdomains or new records, you will have to create new type A records (or the corresponding ones as the case may be) pointing to the same server.

Firewall Rules

Ok, it didn’t make sense to block general access to my server, nobody could see this awesome website! D:

So, I understood that if I had configured Cloudflare correctly and allowed the proxy redirection they offer, I would always get an incoming connection from a Cloudflare IP.

If a client has IP A, the Proxy will receive their IP A but will make the next request with their own IP (say IP B), so the end client will receive the client with IP B.

Example:
 ___________        ___________        ___________
|           | IP A |           | IP B |           |
|   IP A    | ---> |   PROXY   | ---> |  SERVER   |
|___________|      |___________|      |___________|

And just, they themselves provide these IPs on their official website!

All the configuration below is applied in iptables with ufw disabled.

Being clear that we only want to expose web services, we only need port 443 open in the firewall (don’t be lazy and stop using port 80).
But as an extra layer, we will only serve port 443 to those clients coming from Cloudflare IPs.

This is great, I’m sure they protect me from any DOS/DDOS. :)

To do this we can do the following:

#!/bin/bash
cloudflare_ips=($(curl https://www.cloudflare.com/ips-v4 2>/dev/null))
for ip in "${cloudflare_ips[@]}"; do
    iptables -I INPUT -p tcp --dport https -s $ip -j ACCEPT
done

Let’s not forget that we also want SSH access and in my case, I want PING to always be allowed:

#!/bin/bash
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p icmp -s 0.0.0.0 -d 0.0.0.0 -j ACCEPT

Before building the entire script, we have to take into account that there are internal connections that we do not want to block, as well as already established connections:

#!/bin/bash
iptables -A INPUT -s localhost -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

Finally, perhaps because of my insecure tendency, I will relaunch every few hours a script that applies all these rules.
To do this, I will set up a script that sequentially does:

  1. Always clean up all the rules that I have already set up.
  2. It will allow all incoming communications.
  3. It will set up the protections I have explained above.
  4. It will block all remaining communications.

I’m sure you will understand better now:

#!/bin/bash

# Allow all the input connections
iptables -P INPUT ACCEPT

# Clean all existing rules
iptables -F

# Open SSH port (you can modify the port if you want)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# Allow internal connections
iptables -A INPUT -s localhost -j ACCEPT

# Allow ping from all the connections
iptables -A INPUT -p icmp -s 0.0.0.0 -d 0.0.0.0 -j ACCEPT

# Allow HTTPS for Cloudflare incomming IPs (IPv4)
cloudflare_ips=($(curl https://www.cloudflare.com/ips-v4 2>/dev/null))
for ip in "${cloudflare_ips[@]}"; do
    iptables -I INPUT -p tcp --dport https -s $ip -j ACCEPT
done

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Drop all the rest of the incomming connections
iptables -P INPUT DROP

Whenever you are testing firewall rules, speak from experience, I recommend that you leave the following script running, it will save you from a few scares:

#!/bin/bash
while [[ true ]]
do 
 sleep 5m
 iptables -F
done

SSH Service

As much as I have set strict access rules for my exposed services, in my case, I don’t want to limit SSH access to a single IP (and I didn’t want to consider a VPN option either).

In this case, good SSH service configurations come into play.

For me, it is crucial:

  • Limit access to a single user with low privileges within the system.
  • Do not allow SSH access to the root user.
  • Do not allow empty passwords.
  • Allow only AuthenticationKey access.

Remember: for the last point, you must first create the private/public keys.

Now I will have to modify the file located in /etc/ssh/sshd/sshd_config indicating the following parameters:

PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
PermitEmptyPasswords no
PermitRootLogin no
HostKeyAlgorithms +ssh-rsa
AllowUsers tiny-user # where this is the user you want to allow to access.

And if you want, this is also where you should change the port to something other than 22.

When making changes to these files always remember running:

sudo systemctl reload ssh.service

Fail2ban

As I said before, the number of access attempts received on port 22 was not normal. Yes, if you move the SSH port to a port other than the default one, this number will be reduced considerably.

Here I do not want to stop too much, because there are many solutions that can fit well to add more security, I just want to name above the configuration that I applied.

And it is that its configuration file is not short at all. What I set up was a pretty strict policy that bans users when they try to access the system for several consecutive times in a very short period of time.

The nice thing about this software, is that it lets you configure it in so many ways that it sure brings out a creative side in you.

Extra mile

In addition to all the recommendations that I have been leaving in the post, there are always more things that can be forced. A topic that I really like and that I encourage you to investigate is the use of techniques such as Port knocking to open ports in a Firewall.

And yes, you should always test and set the configurations that best suit you, because each one surely has specific cases.

Yay! You have it!