You should take a few simple steps to harden your VPS’s security. In this guide, I will show you how to:
These instructions are for Ubuntu/Debian-based systems. This probably applies to your cheap VPS, but you should make sure.
I will also assume that you have a Virtual Private Server (VPS) set up and ready to serve a website. If you don’t have these set up yet, I recommend reading and following my previous posts on serving static and dynamic sites.
Your vps and your users’ data can be accessed directly through ssh or indirectly through the pages that you are serving over its ports. Let’s close some of those openings.
To start, let’s make sure that your VPS can only be accessed through ssh keys. ssh keys are more secure than username and password logins because they are much harder to brute force or guess. Additionally, they are not transmitted across the network to your remote VPS, so your password cannot be intercepted during transmission.
To set up ssh key access to your remote machine:
ssh-keygen -t ed25519
to generate a new key pair with the ed25519 algorithm. Set up a passphrase when prompted for extra security.id_ed25519.pub
, over to the ~/.ssh/authorized_keys
folder on your VPS:
cat ~/.ssh/id_ed25519.pub | ssh username@your-vps-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Replace username
with your VPS username and your-vps-ip
with your server’s IP address.
If you don’t have ssh access, create this file and add the public key manually via your VPS provider’s control panel.
eval "$(ssh-agent -s)" # for linux and mac. for windows use ssh-agent -s
ssh-add ~/.ssh/id_ed25519 # or whatever file name you gave your private key
ssh username@your-vps-ip
If you were able to log in without your password, then this worked!
ONLY ONCE YOU CONFIRM THAT YOU CAN SSH IN WITH THE KEY, you can disable password authentication:
/etc/ssh/sshd_config
to include this line:
PasswordAuthentication no
sudo systemctl restart ssh
If everything is set up correctly, you should be able to log in without being prompted for a password.
A firewall is a service that filters and blocks connections based on predefined rules. Uncomplicated Firewall, or ufw
, is a simple and user-friendly interface for defining a firewall.
To set up ufw
on your VPS:
ufw
if it’s not already installed:
sudo apt-get install ufw
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
sudo ufw status
Automated vulnerability-finders are constantly scanning open ports for accidentally exposed files. If you check the access logs for your site, you will probably see loads of failed requests looking for seemingly random .php, .xml, .env, or other sensitive files. Here’s an example log trace from my website.
20.196.80.178 - - [03/Sep/2025:16:14:41 +0000] "GET /dfre.php HTTP/1.1" 404 162 "-" "-"
20.196.80.178 - - [03/Sep/2025:16:14:42 +0000] "GET /disagimons.php HTTP/1.1" 404 162 "-" "-"
20.196.80.178 - - [03/Sep/2025:16:14:42 +0000] "GET /disagreop.php HTTP/1.1" 404 162 "-" "-"
20.196.80.178 - - [03/Sep/2025:16:14:43 +0000] "GET /manager.php HTTP/1.1" 404 162 "-" "-"
20.196.80.178 - - [03/Sep/2025:16:14:43 +0000] "GET /uploan.php HTTP/1.1" 404 162 "-" "-"
104.248.157.27 - - [03/Sep/2025:16:35:29 +0000] "GET //wp-includes/wlwmanifest.xml HTTP/1.1" 404 564 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
104.248.157.27 - - [03/Sep/2025:16:35:29 +0000] "GET //xmlrpc.php?rsd HTTP/1.1" 404 564 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
104.248.157.27 - - [03/Sep/2025:16:35:29 +0000] "GET //blog/wp-includes/wlwmanifest.xml HTTP/1.1" 404 564 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
This shows automated scanning from two separate source IP addresses. Notice that each of these requests are returned 404 not found errors.
Fail2ban is a service that monitors requests to your page for suspicious activity. It can automatically block IP addresses that show malicious signs, such as too many password failures or seeking for exploits like those shown in the log.
To set up Fail2ban on your VPS:
sudo apt-get install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Within the [DEFAULT]
section, set these values:
bantime = 1h
findtime = 1h
maxretry = 5
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
Another key aspect of server security is to stay on top of updates for your dependencies and server software.
To update your system packages, use the following commands:
sudo apt-get update
sudo apt-get upgrade
To set up automatic updates, you can install and enable the unattended-upgrades
package, which automatically installs security updates:
sudo apt-get install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
For Node.js applications, you can update your npm packages with:
npm update
There are packages for conducting automatic updates for npm, but it’s simpler to set a cron job to execute npm update
at your desired frequency.
nano /etc/cron.daily/npm-update.sh
Copy this into nano /etc/cron.daily/npm-update.sh
:
#!/bin/bash
cd /path/to/your/nodejs/app
npm update
And give the cron job execute permissions:
chmod +x /etc/cron.daily/npm-update.sh
For Python applications, you can update your virtual environment packages with:
source /path/to/your/venv/bin/activate
pip install --upgrade pip
pip list --outdated | awk 'NR>2 {print $1}' | xargs -n1 pip install -U
deactivate
You can also set a cron job to automate this process.
nano /etc/cron.daily/venv-update.sh
Copy this into nano /etc/cron.daily/venv-update.sh
:
#!/bin/bash
source /path/to/your/venv/bin/activate
pip install --upgrade pip
pip list --outdated | awk 'NR>2 {print $1}' | xargs -n1 pip install -U
deactivate
And give the cron job execute permissions:
chmod +x /etc/cron.daily/venv-update.sh
Your systemd services and your Nginx configuration should be set up to use the minimal permissions necessary to function. This means:
You can run systemd-analyze security
on your server to see a list of services and their vulnerability levels.
To address these vulnerabilities, you must restrict each service in this list.
Run systemd-analyze security servicename
to see detailed information for each service.
/etc/systemd/system/yoursite.service
) to contain user and group definitions for some unprivileged group. We will use www-data
for both.
# filepath: /etc/systemd/system/yoursite.service
# ...existing code...
[Service]
User=www-data
Group=www-data
# ...existing code...
/var/www/yoursite/servicefolder
, you can run:
sudo chown -R www-data:www-data /var/www/yoursite/servicefolder # change ownership to our user and group
sudo chmod -R 755 /var/www/yoursite/servicefolder # grants owner rwx, group rx, others rx
sudo systemctl daemon-reload
sudo systemctl restart yoursite
Add some extra protection to your service by restricting its capabilities. Do this for each service you use.
/etc/systemd/system/yoursite.service
) to enable security options under the [SERVICE]
header. For example:
# filename: /etc/systemd/system/yoursite.service
[Service]
PrivateNetwork=yes # make this service only accessible to internal network requests. Only use if Nginx is also configured to use this socket
PrivateTmp=yes # create a private /tmp for this service
ProtectHome=yes # prevent access to /home, /root, and /run/user
NoNewPrivileges=true # prevent privilege escalation
ProtectSystem=full # make /usr read-only
ProtectKernelModules=yes # prevent loading kernel modules
sudo systemctl daemon-reload
sudo systemctl restart yoursite
Nginx starts as root so that it can access all of your server’s files, such as SSL certs. However, you should ensure that it is configured to drop to a user with lower privileges after it starts up. We will do this with user
directive in your Nginx configuration file, usually located at /etc/nginx/nginx.conf
.
Make sure that the user is set to a lower privileged user, such as www-data
from before:
# filename: /etc/nginx/nginx.conf
user www-data;
Nginx by default emits its version on error pages and in the “Server” response header field. To disable this, add the following line to your Nginx configuration file:
# filename: /etc/nginx/nginx.conf
server_tokens off;
Some HTTP methods may be used to exploit vulnerabilities. For instance, PUT
can be used to replace your server files with malicious ones. We can restrict Nginx to only allow certain HTTP methods. For instance, if your app only needs to support GET
and POST
, you can restrict Nginx to those with this:
# filename: /etc/nginx/nginx.conf
server {
location / {
limit_except GET POST {
deny all;
}
}
}
You can also add these restrictions to your site’s specific Nginx configuration, locatated somewhere like /etc/nginx/sites-available/your-site.conf
. The above modifications apply to all Nginx instances running on your server.