Securing a VPS: First Steps

Three steps to harden your linux servers.

Securing a VPS: First Steps
Photo by Growtika / Unsplash

So you've just provisioned a new server. It's clean, it's fast, and it's ready for whatever you want to build. You spend a few minutes poking around, maybe install a package or two, feeling good about the blank canvas in front of you. Then you check the logs.

Hundreds of failed login attempts. Random IP addresses from around the world, all trying to guess the root password. You've been online for less than an hour, and already you're under attack. Bots found your server before you even finished configuring it. Welcome to the internet. In this post, I'll show you the three simple steps you can take to harden a fresh server; disabling root, setting up two-factor SSH authentication, and configuring a firewall.

Stop Using Root

Most VPS providers let you set up you a root password when you provision the VPS. The root user is the most powerful account on Linux operating systems. It can do anything, delete anything, and consequently ruin everything. Hackers know this, which is why nearly all automated attacks target the username root. By disabling root user log in via SSH, you force an attacker to guess your custom username before they can even try to guess your password.

Creating a New User

  1. First, we create a new user account. Let's call them newuser for the purposes of this guide.
💡
Linux provides two commands for creating a new user from the CLI; adduser and useradd. useradd is low-level and requires specific options to configure new user accounts while adduser is higher-level and more user friendly, creating a home directory and skeleton files by default. I prefer to use adduser for new accounts except for specific instances like writing scripts or creating system accounts.
adduser newuser

create new user

  1. Next, we add this user to the sudo group so they can be granted temporary root privileges when needed. The usermod command with the -aG option does this:
usermod -aG sudo newuser

add user to sudo group

Note: On RHEL/CentOS systems, use wheel instead of sudo.

Its a good idea to verify you can log in as the new user before you proceed. In some cases, you might be required to reset the user's password using the passwd command.

passwd newuser

Changing users password

  1. Now we can log out and log back in as newuser:
ssh newuser@192.168.0.100

log in with SSH (replace username and IP address with your own)

  1. Verify we can execute commands using sudo:
sudo whoami

verify sudo command access

If it returns root, were good. From this point on, we'll use this account instead of root. Next, we'll disable root login entirely later when we configure SSH.

Disabling Root Login

Now that we have a working non-root user, we can disable root login entirely.

  1. Open the SSH daemon config:
sudo nano /etc/ssh/sshd_config
  1. Find PermitRootLogin setting and set it to no:
PermitRootLogin no

disable root login via SSH

  1. Restart SSH to apply the change:
sudo systemctl restart ssh

restart SSH daemon after verifying changes

  1. Let's verify it worked. We try logging in as root from a new terminal:
ssh root@192.168.1.10

try logging in as root

You should see "Permission denied." SSH will now reject any login attempt to the root account, whether the password is correct or not.

Two-Factor Authentication

SSH authentication typically requires a valid SSH key or a password to log in. For additional security, we can configure our server to require a valid SSH key AND a valid password for the specified user. This enforces a degree of Multi-Factor Authentication that works without needing to setup additional PAM modules.

Multi-Factor Authentication is a security method that requires multiple different verification steps before granting access to a resource. In this case its something we have (SSH key file on the physical device) and something we know (password for the newuser account). If someone steals the physical device, they still need a valid password for the newuser account. If someone guesses the password, they still need the key file sitting on our physical device. Both have to be compromised for an attacker to gain access.

SSH Keys

SSH keys use public-key cryptography to authenticate SSH clients to SSH servers. With this method, SSH authentication uses an asymmetric public-private key pair:

  • The private key is stored securely on your local machine. It acts as the physical key.
  • The public key is uploaded to the server you want to access and typically placed in the ~/.ssh/authorized_keys file. It acts like a lock only your private key can open.
⚠️
Private keys should be kept secret and NEVER be shared.

Key Generation and Setup

Generate a dedicated key for each project instead of reusing the same one everywhere. That way, if a key is ever compromised, you only have to deal with one problem instead of ten.

  1. Run the following command in your terminal to generate an ed25519 ssh key pair in the ~/.ssh/ folder on your device:
ssh-keygen -t ed25519 -f ~/.ssh/blog_server -C "newuser@blog"
💡
# -t specifies the algorithm to be used to generate the key.
# -f names the file specifically and its location
# -C adds a comment so I know what this key is for later.

The ssh-keygen command will generate two files. ~/.ssh/blog_server which is the private key and ~/.ssh/blog_server.pub which is the public key. When generating the key, you'll be prompted for a passphrase. This is optional but recommended. The passphrase encrypts your private key, so even if someone steals the key file, they can't use it without the passphrase. If you forget the passphrase, the key is useless. There's no recovery. You'll need to generate a new key and update your servers.

  1. To avoid typing long commands every time, we'll create an alias in my SSH config for each host:
nano ~/.ssh/config

open SSH config file on client

  1. Paste this in the ~/.ssh/config file and save:
Host myblog
    HostName 192.168.1.10 # replace with your remote host's IP address
    User newuser
    IdentityFile ~/.ssh/blog_server

SSH host config file contents

Next, we'll configure the remote host, our server, to allow public key authentication to the server.

Allowing Public Key Authentication on the Remote Server

Before your key will work, the server needs to be configured to accept public key authentication. Most distributions enable this by default, but it's worth confirming before you go any further.

  1. Open the SSH daemon config:
sudo nano /etc/ssh/sshd_config
  1. Look for this line:
PubkeyAuthentication yes

If it's commented out (starting with a #) or set to no, change it. This tells the server to check incoming connections against the keys stored in ~/.ssh/authorized_keys.

  1. If we changed PubkeyAuthentication from no to yes, we restart SSH to apply the change:
sudo systemctl restart ssh

restart SSH daemon

Sending the SSH Public Key to the remote server

  1. Send the SSH public key key to the server. The ssh-copy-id command handles this for you:
ssh-copy-id ~/.ssh/blog_server.pub myblog

Copy ssh key to server

It connects to the server (using your password, since the key isn't there yet), then appends your public key to ~/.ssh/authorized_keys.

If SSH refuses your key, ensure your .ssh folder is set to chmod 700 and authorized_keys is chmod 600 .
  1. Once that's done, test the connection:
ssh myblog

Connecting to the remote server

If you get in without typing your password, the key is in place. If it still asks for a password, something went wrong. Check that PubkeyAuthentication is enabled and that your key file path is correct in ~/.ssh/config. Also check the contents of the ~/.ssh/authorized_keys file to verify your public key was copied there.

Locking It Down: Key + Password

To tie it all together, you need to configure the server to require BOTH the key and the password.

  1. Open the SSH daemon config file:
sudo nano /etc/ssh/sshd_config
  1. Find the settings below and make sure they look like this:
PermitRootLogin no

verify again root login is disabled entirely

PubkeyAuthentication yes

Make sure the server accepts public keys

PasswordAuthentication yes

Make sure password authentication is also enabled

AuthenticationMethods publickey,password

Require BOTH key and password

That comma is where the magic happens. The comma means both required. A space would mean either works, which is not what we want.

  1. Restart SSH to apply the changes.
sudo systemctl restart ssh

(On some distributions like Fedora or CentOS, the service is called sshd instead. If ssh doesn't work, try sshd.)

‼️
Critical warning: Don't close this terminal session yet!!! If something is wrong, your original session is your lifeline, use it to fix the config file. Only close it once you've confirmed the new connection flow works as it should.
  1. Open a new terminal and connect with ssh myblog.
ssh myblog

run this in a NEW terminal while keeping the original session open in the previous tab

If everything is configured correctly, you'll authenticate with your key first, then be asked for your password. If it only asks for one or the other, something's off. Double-check your sshd_config settings.

The Firewall (UFW)

At this point, we've locked down who can log in. But the server is still listening on every port, waiting for connections we don't need. My preferred default firewall for Linux servers is UFW (Uncomplicated Firewall). UFW comes preinstalled on most Debian-based distributions. If yours doesn't have it, install with the following command:

sudo apt install ufw

command to install ufw

The default firewall configuration tool for Ubuntu is ufw. Developed to ease iptables firewall configuration, ufw provides a user-friendly way to create an IPv4 or IPv6 host-based firewall.
ufw is not intended to provide complete firewall functionality via its command interface, but instead provides an easy way to add or remove simple rules. It is currently mainly used for host-based firewalls.

Ensure the firewall is disabled before we start making any changes to the configuration. If UFW is already enabled from a previous setup, disable it while we configure the rules:

sudo ufw disable

disable ufw firewall

Setting the Defaults

For an initial setup, the idea is straightforward. We want to block everything coming in, allow everything going out by default and make exceptions only for what we actually use. To configure this as the default behaviour, run the following commands.

  1. Disable all incoming traffic unless explicitly allowed:
sudo ufw default deny incoming

block all incoming traffic

  1. Your server still needs to reach the outside world for updates, API calls, and general internet access so allow outgoing traffic:
sudo ufw default allow outgoing

allow outgoing traffic

Allow SSH Traffic

Before enabling the firewall, you need to allow SSH traffic. Skip this and you'll lock yourself out the moment you enable UFW.

sudo ufw allow ssh

allow SSH traffic through the UFW firewall

⚠️
if you changed the ssh port, enable traffic through that port by running the command `sudo ufw allow <port>.

Verify the rules created by running this command:

sudo ufw status

view UFW firewall status and rules

You should see SSH listed as allowed (or your custom port), with default deny for everything else. After verifying the rules, activate the firewall with your configured rules:

sudo ufw enable

enable UFW firewall

The Result

We started with a server that was wide open to the internet. Let's take a step back and look at what we've achieved so far.

  • Root login is disabled, so the common and high-value attack vector is gone.
  • SSH requires both a key and a password, which means an attacker would need to compromise two separate things at the same time to get in.
  • The firewall blocks every port except the one we explicitly allowed (SSH).

None of this makes the server impenetrable, but it raises the bar high enough that most automated attacks will fail and attackers will move on to easier targets. The security foundation is solid. There's more we could add down the line like fail2ban, intrusion detection, regular audits... but those are for another post. Now let's go build something on it.

See you in the next log.