Hello rabbits! Continuing last post’s thematic, in which we used a VPS to deploy a GitLab remote instance, today I will show you some very basic and quick steps that will help us secure our dedicated VPS.
In essence these will be the steps to follow:
- Creation of the VPS.
- Configuration of a non-privileged user.
- Configuration of the SSH service.
- Installation and configuration of UFW.
- Installation and configuration of Fail2Ban.
And as a little extra…
- Automation of the process 🙂
Creation of the VPS
This first step was already performed in the last post (sorry English readers! It’ll be translated soon enough :)), where we used DigitalOcean Droplets. Other providers, such as AWS, allow to create the new VPS with a pre-configured SSH access key in our account (we also did that in last post).
Configuration of a non-privileged user
The next step will be the creation and configuration of a non-privileged user, in case our provider gives us root access to the machine as a default. Changing this will be a nice security practice, disabling this as soon as possible and allowing non-privileged direct access only.
We will continue using DigitalOcean as an example, and a Debian VPS that we will name SECURITY-TESTING. As a default, we will access via SSH as root. Also, Debian is often deployed with a non-privileged user named debian, which we will configure to be our non-privileged user.
First, we will copy the hidden directory /root/.ssh to /home/debian/.ssh. This folder contain the necessary data to connect to the VPS via SSH with our private key; specifically, it is contained in authorized_keys file. Copying this will allow us to log into the machine as debian making use of such key.
We must also stablish debian as the owner of the folder.
# cp -a /root/.ssh /home/debian/.ssh # chown -R debian:debian /home/debian/.ssh
At this point, we can open a new SSH session as user debian making use of the same private key, and we will be granted access (let’s keep the privileged session open in order to continue our configurations).
Having made this, in our root session, we will empty the file /root/.ssh/authorized_keys so we disallow direct access to root account via SSH.
# echo "" > ~/.ssh/authorized_keys
We will notice taht we cannot user our private key to login as root (so still do not close that session!).
We can also see that we are able to execute privileged commands as root, making use of sudo command or simply escalating into root account with sudo su.
What’s the point of accessing our system in a non-privileged secure way, if we can just easily sudo later? DigitalOcen (and others) use cloud-init to perform an initial configuration in the machine after its creation, performing tasks as the pre-configuration of SSH keys and also allowing debian user to escalate easily with the former commands.
To avoid that, we will rename the file /etc/sudoers.d/debian-cloud-init adding a ~ symbol on its end:
# mv /etc/sudoers.d/debian-cloud-init /etc/sudoers.d/debian-cloud-init~
This will cause the system to ignore the file and hence we will nullify that configuration. Now, if we execute sudo su as debian, we will be prompted for its password:
However, since we will only access debian user via SSH, it does not have any password configured, nor we need it. It will not be possible to log in as debian via password, and even if we configured one, it would not be possible to execute privileged commands on the session.
Talking about passwords, what is the password for user root? It does not have one configured so far because, again, we accessed the account via SSH. We must configure one so we can escalate securely to root from a non-privileged user making use of it (not like before, when we could just escalate automagically) and execute administration tasks.
We will configure it in our privileged session and keep in mind to use a nice, long and complex password:
Now, from our debian session, we can execute su root to make use of the former password and obtain a root session:
Configuration of the SSH service
We will make some changes in the /etc/ssh/sshd_config file to configure the SSh service, which allow us access the machine. A nice rule of thumb would be to disable everything we will not use.
Although some configurations I will name are already configured with the value I use as a default, I rather explain and explicitely activate them just in case we face a machine with a different configuration, and also I rather make sure you understand the concepts 🙂
The most basic step will be to change the service’s listening port. Changing the port is not a security measure itself, since just adds “security by obscurity”, and that is not adding security. However, we will avoid the constant bombarding of botnets in our port 22 (normally without success, since we do not allow password-based access).
We can change it for any value we wish. For instance, we will use 3322. Change the Port line like this:
As the SSH service will be accessed from outside, we will leave the service listening on IPv4 0.0.0.0 on the ListenAddress line, which indicates that it will listen on all configured IPv4 addresses on the machine. We can also see another ListenAddress line that corresponds to IPv6, but DigitalOcean does not use this on the Droplet, so we can just ignore it. If we wish to force IPv4-only access, we can configure AddressFamily line to inet.
AddressFamily inet ListenAddress 0.0.0.0
We will configure LoginGraceTime to 10, so after 10 seconds of displaying login prompt, the SSH session will finish if no session was opened.
We will change PermitRootLogin to no, disallowing direct login as root.
The StrictModes option, already disabled as a default, allows the SSH service to check if the user we want to log in with has properly configured the permissions of its key files, such as the authorized_keys we talked about earlier. If they are not, it won’t let us log in. We can check it changing the permissions of such file from 600 to 666 and trying to log in, keeping an eye on SSH logs located at /var/log/auth:
# chmod 666 /home/debian/.ssh/authorized_keys # tail -f /var/log/auth
So, we sill leave the line with a yes:
We will configure MaxAuthTries to 1, so we only allow one authentication attempt per open session, and MaxSessions to 3, so only 3 simultaneous SSH sessions can be open at once.
MaxAuthTries 1 MaxSessions 3
We will leave PubkeyAuthenticaton configured with a yes, allowing access via SSH keys for user debian.
The line HostbasedAuthentication allows a trusted remote machine to automatically log in via SSH into our VPS. Every machine that uses a SSh client has a host key preloaded, independently from user keys. To allow this feature, we would add into the file ~/.ssh/known_hosts on a user’s home folder the public key of the remote machine, so it can easily log into the VPS as such user. This is usually used in environments where a machine acts as a monitor of others, periodically accessing them. Since this is not the case, we can leave it as a no.
We will change PasswordAuthentication to no, since we will only access the machine via SSH keys, never via password. We will leave PermiteEmptyPasswords as a no, disallowing the use of empty passwords. This is redundant, though, since we do not even allow passwords.
PasswordAuthentication no PermitEmptyPasswords no
The ChallangeResponseAuthentication line allows the use of advanced authentication options using an independent service, such as PAM. We will not use it, so we will say no.
We will also disable KerberosAuthentication and GSSAPIAuthentication for the same reason.
KerberosAuthentication no GSSAPIAuthentication no
Also, since only allow access via SSH keys, we can safely disable the use of PAM (Pluggable Authentication Module) service .
The only use we will give our SSH service is the direct connection into the VPS, so we can disable the options related to forwarding and tunnelling, such as X11 Forwarding for graphic applications tunneling.
AllowAgentForwarding no AllowTcpForwarding no GatewayPorts no X11Forwarding no PermitTunnel no
We will set ClientAliveInterval as 60 seconds, after which the server will send a network message to the client to check if it is still connected; if the client does not respond, the session will be terminated. The value ClientAliveCountMax 1 will close the channel the first time this fails. Also we will leave TCPKeepAlive activated, so the state of the communications channel is regularly checked and it closese if a connection failure is detected.
ClientAliveInterval 60 ClientAliveCountMax 1 TCPKeepAlive ye
Lastly, we will configure MaxStartups. It consists of three semicolon-separated values. The firs establishes the maximum number of unauthenticated sessions (login prompts) after which new connections will start to be rejected in a certain percentage (second value), linearly growing until a certain number of unauthenticated sessions is reached (third value), at which point all new sessions will be rejected.
We will leave the default 10:30:100 value:
After this, we will have a pretty secure SSH configuration. I will leave here a gist with a sshd_config file that gathers all the described configurations, so you can upload it into a fresh machine to quickly configure the service. I have left the default port 22, but remember it might be convenient to change it.
To download the gist into the remote machine (previous security backup of the old file) and restart SSH service:
# cp /etc/ssh/sshd_config /etc/ssh/sshd_config.back # wget https://gist.githubusercontent.com/hartek/82decb8f0817d1a6ec8a10454e9134c4/raw/c4aa0d62231e1ecf269ce04a43deb263c9ae0cd5/sshd_config -O /etc/ssh/sshd_config # systemctl restart ssh
With this, our SSH service will be configured and running with a decently secure setup 🙂
Installation and configuration of UFW
UFW or Uncomplicated FireWall is a wrapper for IPTables that allows us to create Firewall rules quickly and easily on our Linux machine. It is a great option if we are interested on creating some basic rules to allow or deny connecions into our VPS.
The tool can be downloaded from the repositories of almost any distribution. For instance, on Debian we can install UFW via apt:
# apt install ufw
By default, UFW will reject any connection into our machine. Depending on our configuration and the service we want to offer to the exterior, we will want to allow the connection into some ports.
In the case of SSH and other known services, UFW allow to simply indicate its name and it will allow the connections into its default port, such as OpenSSH:
# ufw allow openssh
This will open access into TCP/TCPv6 port 22 from anywhere.
However, this does not apply in our case, since we change the listening port and we are only interested on IPv4. We will allow external connections into our SSH service, running on TCP port 3322, and then enable the service (accepting the warning with a y) and check its status and current rules:
# ufw allow proto tcp to 0.0.0.0/0 port 3322 # ufw enable # ufw status
The only allowed connections will be to our SSH port. If we wanted to allow connections from a given IP address only, such as our home or work network, or even a monitoring machine, we could substitute the 0.0.0.0/0 value (which indicates any IPv4 address) with such address.
In order to delete a rule, we can write:
# ufw delete <rule>
Where <rule> is the full rule formerly added. If we use the ufw status numbered command, we will be able to see the rules with in numeric order, and we will be able to use a rule’s number to delete it without having to fully write it.
# ufw status numbered # ufw status <number>
We could repeat the operation adding the correpondent rule to any service port that we want to open to the exterior on the machine. For instance, if you have a GitLab running on port 80:
# ufw allow proto tcp to 0.0.0.0/0 port 80
You will usually see that much simpler rules are used for configuration, such as:
# ufw allow 80/tcp
This is a simplification that allows both TCPv4 and TCPv6 from anywhere. Again, we will prefer to be as concrete as possible 🙂 I will leave you here a nice tutorial by DigitalOcean for further reading about UFW.
Installation and configuration of Fail2ban
The last step to be made is the installation and basci configuration of Fail2ban. This service makes a constant monitoring of other services log files, and performs actions based of its content, normally related to access restricion.
In practice, we will use Fail2Ban to watch connection attempts through SSH and block IP addresses that might be trying to access several times, which could mean a bruteforce attack.
The first step is, again, to install the tool. We can do this from the repositories of almost any distribution. In Debian we can use apt:
# apt install fail2ban
We can check the status of the service, on which we can also see that as default the SSH service is already being protected:
# fail2ban-client status
In Fail2Ban several jails can be configured, which represent the protection layer around a service based on its log files. We can inspect the status of each jail by appending its name into the last command we used. In our case, let’s check the state of the sshd jail that takes care of our SSH service:
# fail2ban-client status sshd
We can see several elements in the output:
- First, the Currently failed and Total failed lines indicate the number of IP addresses with failed login attempts, and the total number of failures.
- File list points at the file or files that fail2ban is monitoring for the service.
- Currently banned and Total banned indicate the number of IP addresses currently blocked at this moment and the total of blocks since the service started.
- Banned IP list is a list of the currently blocked IP addresses.
The first thing we will do is to make a copy of the configuration file /etc/fail2ban/jail.conf into a new file jail.local on the same folder, preventing an overwrite if Debian updates the package:
# cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
If we edit the newly created file, we will be able to see the correspondent lines to the sshd service we want to protect, activated by default in Debian (because of the /etc/fail2ban/jail.d/defaults-debian.conf file). It indicates by default the usual SSH port 22. We can change this with the port number our service is listening on, in this case 3322. We can leave the log paths as they are, given that we are using the default ones.
Continuing with the configuration of the jail.local file, we can find the line banaction in the [DEFAULT] section. This line establishes the action to be taken in order to ban an address. The actions are kept in configuration files located at /etc/fail2ban/actions.d.
By default, the action is iptables-multiport, making use of pure IPTables. We will use UFW to simplify the visualization of the created block rules, setting the action value to ufw.
We will leave banaction_allports line unchanged, which fully blocks a remote IP in every port, but is not used by our sshd jail.
Let’s finish the configuration with the following lines on [DEFAULT] section:
- The bantime line represents the amount of seconds that a host is banned from the service. The default value is 600 seconds. We will set it at 1200 seconds, or two hours.
- The findtime line is the time window on which an IP address tries to access the service unsuccessfully and reaches the maximum attempts. We will also set this at 1200 seconds, overwriting the 600 default.
- The maxretry line sets the named maximum attempts number within the findtime window before getting banned, 5 by default. We will set this at 3.
A more confortable option might be to configure all of this at once at the level of SSH jail only (this is the only service being protected after all) creating a /etc/fail2ban/jail.d/sshd.conf file with the following content:
[sshd] enabled = true port = 3322 banaction = ufw bantime = 1200 findtime = 1200 maxretry = 5
We can reload the fail2ban service with the command:
# systemctl reload fail2ban
If we test and perform several failed SSH login attempts from some IP address, we can easily verify that fail2ban lists such address as blocked, and we cannot access the VPS from that address until 2 hours have passed:
As a little importante detail, we can use the following command to manually unblock an address:
# fail2ban-client set <jail> unbanip <ip_address>
And the basic security configuration of our VPS ends here 🙂
Automation of this process
Lastly, we can execute all the former changes in an automated way once the machine is created. We will gather the former stepts into a script and execute it remotely on our VPS.
I have also created a gist for this:
You can execute it with the following command, given that you have a fresh Debian VPS you want to configure and you have been given root access via SSH keys:
$ ssh -t -i <private_key> root@<vps_address> 'wget --no-cache https://gist.githubusercontent.com/hartek/c9af1c4246bef5228f48ca8c9d51bdb8/raw/d5070c09988c5673ea06bcd6890e8a15a2ddc255/secure_vps.sh -O secure_vps.sh && bash secure_vps.sh'
During the execution you will be able to see the installation of the needed packages and the configuration of the services (making use of the sshd_config gist we named much earlier in the post). We will be prompted for a new root password and for the SSH port to be configured for the service. Once it finishes, you will be able to access the VPS using debian user and the same private key you already had. Remember to use the -t SSH option to spawn an interactive Shell or the data prompting might give you some problems!
And that is all for today! I hope you enjoyed the journey and learnt some things today 🙂 Have a nice week!!