In my previous article, I explained why self-hosting on a VPS makes sense for indie developers and small businesses. Your new Virtual Private Server (VPS) is like a house on the internet. Just as you wouldn’t leave your front door unlocked, you shouldn’t leave your server unsecured. Now it’s time to get practical. In this comprehensive guide, I’ll walk you through securing your Ubuntu 24.04 LTS VPS with industry-standard practices that I’ve developed over years of managing production servers.
While I do provide an automated script for this process (available on my GitHub), I strongly recommend following this manual guide first. Understanding each security measure helps you:
- Know exactly what’s happening on your system
- Troubleshoot issues effectively
- Make informed decisions about security
- Avoid blindly running scripts from the internet
The Reality of Server Security
Within minutes of your VPS going live, it’s already under attack. Don’t believe me? After securing your server, go and check the /var/log/auth.log
file, and you’ll see dozens of failed login attempts. Here’s a real example from one of my servers:
2025-03-23T07:35:17.870730+00:00 us3 sshd[2361597]: Invalid user donghan from 45.192.170.78 port 58770
2025-03-23T07:35:17.872994+00:00 us3 sshd[2361597]: pam_unix(sshd:auth): check pass; user unknown
2025-03-23T07:35:17.873510+00:00 us3 sshd[2361597]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=45.192.170.78
2025-03-23T07:35:20.263695+00:00 us3 sshd[2361597]: Failed password for invalid user donghan from 45.192.170.78 port 58770 ssh2
2025-03-23T07:35:20.812535+00:00 us3 sshd[2361597]: Received disconnect from 45.192.170.78 port 58770:11: Bye Bye [preauth]
2025-03-23T07:35:20.812827+00:00 us3 sshd[2361597]: Disconnected from invalid user donghan 45.192.170.78 port 58770 [preauth]
2025-03-23T07:36:46.822325+00:00 us3 sshd[2362211]: Invalid user maintenance from 45.192.170.78 port 46186
These are automated bots trying common username/password combinations. If you’re using password authentication, it’s not a matter of if you’ll be compromised, but when.
Prerequisites
Before we begin, ensure you have:
- A fresh Ubuntu 24.04 LTS VPS from your chosen provider. I have VPS’s hosted with DigitalOcean, Vultr and Hostinger.
- SSH access to your server
- About 30 minutes of time
- Basic command line knowledge
- A cup of coffee (optional but recommended)
The Case for Strong Security
Let’s start with a fundamental truth about server security: most attacks are automated and target the lowest hanging fruit. This means they focus on standard ports and common configurations. Here’s what typically happens when you deploy a new VPS with default settings:
Common Attack Vectors on Default Configurations
- SSH on Port 22:
- Continuous automated scanning
- Brute force attempts against common usernames
- Dictionary attacks using known password combinations
- Waste of server resources handling these attempts
- Default Service Ports:
- Database ports (3306, 5432)
- Web server ports (80, 443)
- Mail server ports (25, 587)
- Application specific ports
However, there’s a simpler approach that immediately eliminates most automated attacks: using non-standard ports. This isn’t security through obscurity – it’s about reducing noise and potential attack surface.
Why Ubuntu 24.04 LTS?
We’re using Ubuntu 24.04 LTS as our base system for several practical reasons:
- Long-term Support Benefits:
- Security updates until April 2029
- Stable package ecosystem
- Predictable maintenance schedule
- Security Features:
- Modern security tools available
- Regular security updates
- Strong default configurations
- Production Environment Stability:
- Minimal breaking changes
- Well-documented upgrade paths
- Enterprise-grade reliability
The key to effective VPS security isn’t just about blocking attacks – it’s about smart configuration choices that prevent your server from being targeted in the first place. Using non-standard ports, combined with other security measures we’ll implement, creates multiple layers of protection that work together effectively.
In the next section, we’ll start with these smart configuration choices, beginning with setting up SSH on a non-standard port and implementing additional security measures that complement this approach.
Initial Server Hardening
When you first get your VPS, you’ll typically receive root login credentials via email. While it’s tempting to just start installing applications, let’s take the essential first steps to secure your server. We’ll do this methodically to ensure we don’t lock ourselves out.
The first step is creating a secure non-root user and setting up SSH key authentication. This is crucial because:
- Running everything as root is dangerous
- Password authentication is vulnerable to brute force attacks
- SSH keys are significantly more secure
Step 1: Create Your Non-Root User
First, SSH into your server as root (this should be the last time):
# Create new user
adduser yourusername
# Add user to sudo group
usermod -aG sudo yourusername
# Test sudo access
su - yourusername
sudo whoami # Should return 'root'
Remember: When setting your user password, use a password manager like 1Password, do not ever user an ‘easy for you to remember’ password on a VPS.
Step 2: SSH Key Authentication Setup
On your local machine, create an SSH key pair if you haven’t already:
# Create SSH key (use a strong passphrase)
ssh-keygen -t ed25519 -C "[email protected]"
# Copy key to server (replace username and IP)
ssh-copy-id -i ~/.ssh/id_ed25519.pub yourusername@your-server-ip
Step 3: Moving SSH to Non-Standard Port
This is where we diverge from common tutorials. Instead of hardening port 22, we’ll move SSH to a non-standard port entirely:
# Edit SSH config
sudo vi /etc/ssh/sshd_config
# Change these settings:
Port 1337 # Choose your preferred port
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Before restarting SSH, verify the configuration:
# Test configuration
sudo sshd -t
# If no errors, restart SSH
sudo systemctl restart sshd
Important: Open a new terminal and test your connection before closing your current session:
ssh -p 1337 yourusername@your-server-ip
Why This Approach Works
Using a non-standard port isn’t security through obscurity – it’s about practical risk reduction:
- Eliminates Automated Scanning:
- No more constant auth.log entries
- Reduced server load
- Clean, readable logs
- Key-Only Authentication:
- Even if port is discovered, no password attacks possible
- Each device needs explicit key access
- Keys can be individually revoked if needed
- No Root Login:
- Additional layer of security
- Better audit trail of admin actions
- Reduces impact of any potential breach
The next section will go over configuring your hosts’ firewall correctly. A lot of VPS providers do provide external firewalls as well as Cloudflare, but it is always good practice to configure the local one first.
Firewall Configuration
A properly configured firewall is your first line of defence after SSH hardening. Ubuntu comes with UFW (Uncomplicated Firewall), which, as the name suggests, makes firewall management straightforward.
Understanding UFW
UFW is a frontend for iptables that simplifies firewall management. By default, it’s installed but not enabled on Ubuntu. Our first step is to set up basic rules before enabling it (this prevents accidentally locking yourself out).
# Check UFW status
sudo ufw status
# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
Basic Firewall Rules
With our non-standard SSH port, we’ll start with these essential rules:
# Allow SSH on custom port (replace 1337 with your port)
sudo ufw allow 1337/tcp
# Allow HTTP/HTTPS for web servers
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable UFW
sudo ufw enable
Opening Additional Ports
Depending on your applications, you might need additional ports. Here are common examples:
# Django (if needed)
sudo ufw allow 5432/tcp
# Node.js application (example)
sudo ufw allow 3000/tcp
# Check all rules before enabling
sudo ufw show added
Though, it is a good idea to put any web applications behind a proxy like Nginx.
Automated Configuration
Our security script automates this process with some additional hardening:
# Rate limiting for SSH port
sudo ufw limit 1337/tcp
# Block common attack ports
sudo ufw deny 23/tcp # Telnet
sudo ufw deny 25/tcp # SMTP unless needed
To verify your configuration:
# Show numbered rules
sudo ufw status numbered
# Show detailed status
sudo ufw status verbose
The beauty of UFW is its simplicity – you can easily add or remove rules as your needs change.
Remember: always verify your SSH access works before ending your current session.
Intrusion Prevention
While moving SSH to a non-standard port eliminates most automated attacks, we still want protection against any targeted attempts. Fail2ban provides this layer of security by monitoring logs and automatically blocking suspicious activity.
Setting Up Fail2ban
First, install and enable Fail2ban:
# Install Fail2ban
sudo apt install fail2ban
# Create local config
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Basic Configuration
Edit the local configuration file:
sudo vi /etc/fail2ban/jail.local
Key settings to modify:
[DEFAULT]
# Ban time in seconds (24 hours)
bantime = 86400
# Time in seconds before counter resets
findtime = 600
# Number of failures before ban
maxretry = 3
# Email notifications (optional)
destemail = [email protected]
sender = [email protected]
[sshd]
enabled = true
port = 1337
filter = sshd
logpath = /var/log/auth.log
Understanding Ban Times
My usual configuration uses:
- 24-hour bans (bantime)
- 10-minute window (findtime)
- 3 failed attempts (maxretry)
This means: if someone fails to login 3 times within 10 minutes, they’re banned for 24 hours.
Monitoring Failed Attempts
Check Fail2ban status and banned IPs:
# Check status
sudo fail2ban-client status
# Check SSH jail specifically
sudo fail2ban-client status sshd
# View current bans
sudo fail2ban-client get sshd banned
Review the auth log for failed attempts:
# View recent authentication attempts
sudo tail -f /var/log/auth.log
Additional Jail Configuration
While SSH is the primary concern, Fail2ban can protect other services:
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /var/log/nginx/access.log
After making changes:
# Restart Fail2ban
sudo systemctl restart fail2ban
# Verify it's running
sudo systemctl status fail2ban
The combination of non-standard SSH ports and Fail2ban creates a robust defense against brute force attempts. Even if someone discovers your SSH port, they’ll be quickly banned after a few failed attempts.
Malware Protection
While Linux servers are generally more resistant to malware than desktop systems, it’s still crucial to have protection in place. This is particularly true if you allow users to upload files, or run standard web applications like WordPress. We’ll use ClamAV for antivirus scanning and Rootkit Hunter for detecting system compromises.
Installing and Configuring ClamAV
First, let’s set up ClamAV:
# Install ClamAV and its daemon
sudo apt install clamav clamav-daemon
# Stop the daemon to update virus definitions
sudo systemctl stop clamav-freshclam
# Update virus definitions
sudo freshclam
# Start the daemon again
sudo systemctl start clamav-freshclam
Setting Up Rootkit Hunter
Rootkit Hunter (rkhunter) checks for rootkits, backdoors, and local exploits:
# Install rkhunter
sudo apt install rkhunter
# Update rkhunter database
sudo rkhunter --update
# Perform initial system check (establish baseline)
sudo rkhunter --propupd
Automated Scanning
Let’s create a simple script to automate regular scans. Create /usr/local/sbin/security-scan.sh
:
#!/bin/bash
# Set log file
LOG_FILE="/var/log/security-scan.log"
# Start log entry
echo "Security scan started at $(date)" >> $LOG_FILE
# Run ClamAV scan
echo "Starting ClamAV scan..." >> $LOG_FILE
clamscan -r --infected /home >> $LOG_FILE 2>&1
# Run rkhunter check
echo "Starting rkhunter check..." >> $LOG_FILE
rkhunter --check --skip-keypress >> $LOG_FILE 2>&1
# End log entry
echo "Security scan completed at $(date)" >> $LOG_FILE
echo "----------------------------------------" >> $LOG_FILE
Make it executable:
sudo chmod +x /usr/local/sbin/security-scan.sh
Set up automated weekly scans with cron:
sudo crontab -e
# Add this line for weekly Sunday 3AM scans
0 3 * * 0 /usr/local/sbin/security-scan.sh
Important Directories to Scan
ClamAV should focus on these key areas:
- /home – User directories
- /var/www – Web applications
- /tmp – Temporary files
- /var/tmp – More temporary files
- Any other directories you configure for your applications.
Handling Scan Results
Check scan results:
# View the latest scan results
sudo tail -f /var/log/security-scan.log
# Check rkhunter logs specifically
sudo cat /var/log/rkhunter.log
If malware is detected:
- Isolate the infected files
- Check file creation dates and permissions
- Review system logs for suspicious activity
- Consider restoring from a clean backup
Email Notifications
For important findings, set up email notifications by adding to the scan script:
# Add to security-scan.sh
if grep -q "Infected files: [1-9]" "$LOG_FILE"; then
mail -s "Malware Detected on $(hostname)" [email protected] < $LOG_FILE
fi
This combination of regular scanning and automated notifications helps ensure you’re aware of any potential security issues quickly.
Please Note: One thing to realise is you might need to configure a proper email set up as generally your email account will mark things coming direct from your servers as spam.
Automatic Updates
Keeping your system updated is crucial for security. While manual updates give you more control, automated security updates ensure you’re protected against known vulnerabilities even when you’re not actively managing the server.
Configuring Unattended Security Updates
First, install the required package:
sudo apt install unattended-upgrades
Configure automatic updates by editing /etc/apt/apt.conf.d/50unattended-upgrades
:
sudo vi /etc/apt/apt.conf.d/50unattended-upgrades
Key settings to configure:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
// "${distro_id}:${distro_codename}-updates";
};
// Auto-remove unused dependencies
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Auto reboot if needed (recommended: "false")
Unattended-Upgrade::Automatic-Reboot "false";
// Email notifications
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";
Enable automatic updates:
sudo dpkg-reconfigure -plow unattended-upgrades
Safe Update Practices
While automatic updates are helpful, follow these best practices:
- Keep a Test Environment:
# Create update script for test environment
sudo vi /root/test-updates.sh
#!/bin/bash
apt update
apt list --upgradable
- Monitor Update Logs:
# Check update logs
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
- Set Update Schedule:
sudo vi /etc/apt/apt.conf.d/10periodic
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
Package Cleanup
Prevent disk space issues with regular cleanup:
# Create cleanup script
sudo vi /usr/local/sbin/cleanup.sh
#!/bin/bash
LOG="/var/log/cleanup.log"
echo "Cleanup started at $(date)" >> $LOG
apt-get autoremove -y >> $LOG 2>&1
apt-get autoclean >> $LOG 2>&1
apt-get clean >> $LOG 2>&1
echo "Cleanup finished at $(date)" >> $LOG
echo "----------------------------------------" >> $LOG
Schedule monthly cleanup by adding a cront job:
sudo chmod +x /usr/local/sbin/cleanup.sh
# Add to crontab (runs first of every month)
sudo crontab -e
0 0 1 * * /usr/local/sbin/cleanup.sh
Update Status Monitoring
Create a simple script to check update status:
sudo vi /usr/local/sbin/check-updates.sh
#!/bin/bash
UPDATES=$(apt list --upgradable 2>/dev/null | grep -c upgradable)
SECURITY=$(apt list --upgradable 2>/dev/null | grep -c security)
if [ $SECURITY -gt 0 ]; then
echo "WARNING: $SECURITY security updates available"
echo "$UPDATES total updates pending"
mail -s "Security Updates Required on $(hostname)" [email protected] << EOF
$SECURITY security updates pending
$UPDATES total updates available
EOF
fi
Run this check daily:
sudo chmod +x /usr/local/sbin/check-updates.sh
sudo crontab -e
0 9 * * * /usr/local/sbin/check-updates.sh
This setup ensures your system stays updated while giving you visibility into what’s changing.
Monitoring and Maintenance
Effective monitoring ensures you catch issues before they become problems. We’ll set up basic monitoring practices that don’t require expensive tools but still provide comprehensive coverage.
Essential Log Locations
Key logs to monitor:
# System logs
/var/log/syslog # General system messages
/var/log/auth.log # Authentication attempts
/var/log/kern.log # Kernel messages
/var/log/nginx/ # Web server logs
/var/log/fail2ban.log # Intrusion attempts
# Create log monitoring script
sudo vi /usr/local/sbin/check-logs.sh
#!/bin/bash
LOG_DIR="/var/log"
ALERT_EMAIL="[email protected]"
# Check for authentication failures
AUTH_FAILURES=$(grep "authentication failure" $LOG_DIR/auth.log | wc -l)
if [ $AUTH_FAILURES -gt 10 ]; then
echo "High number of authentication failures: $AUTH_FAILURES" | \
mail -s "Security Alert: Auth Failures on $(hostname)" $ALERT_EMAIL
fi
Basic Monitoring Practices
1. System Resource Monitoring
# Install monitoring tools
sudo apt install htop iotop nethogs
# Create resource check script
sudo vi /usr/local/sbin/check-resources.sh
#!/bin/bash
THRESHOLD=90
EMAIL="[email protected]"
# Check CPU load
LOAD=$(uptime | awk '{print $10}' | cut -d. -f1)
# Check memory usage
MEMORY=$(free | grep Mem | awk '{print $3/$2 * 100.0}' | cut -d. -f1)
# Check disk usage
DISK=$(df / | tail -1 | awk '{print $5}' | cut -d% -f1)
if [ $LOAD -gt $THRESHOLD ] || [ $MEMORY -gt $THRESHOLD ] || [ $DISK -gt $THRESHOLD ]; then
echo "Resource Alert on $(hostname)
Load: $LOAD%
Memory: $MEMORY%
Disk: $DISK%" | mail -s "Resource Alert: $(hostname)" $EMAIL
fi
2. Uptime Robot Setup
Sign up for a free Uptime Robot account (https://uptimerobot.com/):
- Add HTTP(s) monitoring for your web services
- Add port monitoring for custom services
- Set up 5-minute check intervals
- Configure email/SMS alerts
Example webhook for server notifications:
# Create notification receiver
sudo vi /var/www/monitoring/webhook.php
<?php
$data = json_decode(file_get_contents('php://input'), true);
if ($data['alertType'] == 'down') {
// Log downtime
file_put_contents('/var/log/uptime.log', date('Y-m-d H:i:s') . " - Service Down\n", FILE_APPEND);
}
3. VPS Provider Monitoring
Most VPS providers offer basic monitoring. Enable:
- CPU usage alerts
- Memory usage alerts
- Disk space warnings
- Network bandwidth monitoring
For example, on DigitalOcean:
- Enable monitoring from the dashboard
- Set alert policies for 90% CPU/Memory usage
- Configure bandwidth threshold alerts
Regular Security Checks
Create a weekly security audit script:
sudo vi /usr/local/sbin/security-audit.sh
#!/bin/bash
LOG="/var/log/security-audit.log"
DATE=$(date '+%Y-%m-%d')
echo "Security Audit - $DATE" > $LOG
echo "======================" >> $LOG
# Check listening ports
echo "Open Ports:" >> $LOG
netstat -tulpn >> $LOG
# Check users with shell access
echo "Users with shell access:" >> $LOG
grep -v '/nologin\|/false' /etc/passwd >> $LOG
# Check sudo users
echo "Users with sudo access:" >> $LOG
getent group sudo >> $LOG
# Check failed logins
echo "Recent failed logins:" >> $LOG
grep "Failed password" /var/log/auth.log | tail -5 >> $LOG
Backup Strategies
While I’ll briefly touch on backup strategies here, I will have a complete article on effective backups for your VPS later in the series.
- Daily Application Backups:
# Create backup script
sudo vi /usr/local/sbin/backup.sh
#!/bin/bash
BACKUP_DIR="/backup"
DATE=$(date '+%Y-%m-%d')
# Backup web files
tar -czf $BACKUP_DIR/www-$DATE.tar.gz /var/www/
# Backup databases
mysqldump --all-databases > $BACKUP_DIR/mysql-$DATE.sql
# Cleanup old backups (keep 7 days)
find $BACKUP_DIR -type f -mtime +7 -delete
- System Configuration Backups:
# Key configurations to backup
/etc/nginx/
/etc/fail2ban/
/etc/ssh/
/etc/cron.d/
Schedule all monitoring tasks:
# Add to crontab
sudo crontab -e
# Resource checks every 5 minutes
*/5 * * * * /usr/local/sbin/check-resources.sh
# Log checks hourly
0 * * * * /usr/local/sbin/check-logs.sh
# Security audit weekly
0 0 * * 0 /usr/local/sbin/security-audit.sh
# Daily backups at 2 AM
0 2 * * * /usr/local/sbin/backup.sh
Conclusion
Securing your VPS is an ongoing process, not a one-time task. This guide has covered the essential aspects of VPS security, but remember that security is always evolving. Let’s wrap up with key takeaways and next steps.
Common Pitfalls to Avoid
- Security Through Obscurity
- Don’t rely solely on non-standard ports
- Always implement proper authentication
- Keep all security layers active
- Set and Forget
- Regularly review security logs
- Update security policies
- Test backup restoration
- Verify monitoring systems
- Overcomplicating Security
- Start with basics before advanced measures
- Document all security changes
- Keep configurations simple and maintainable
- Neglecting Updates
- Don’t disable automatic security updates
- Test updates in staging when possible
- Keep track of update logs
Next Steps in the Series
The next articles in this series will cover:
- Complete Guide to Server Backups: Local, Cloud and Disaster Recovery
Learn how to implement a comprehensive backup strategy to protect your data and ensure business continuity. - Nginx Configuration Guide: From Basic to Advanced with Security Best Practices and Proxy Setup
Master Nginx configuration, including reverse proxy setup, load balancing, security hardening, and advanced features for production environments. - Database Security Best Practices for Production Environments
Essential security measures for protecting your databases, including access control, encryption, and monitoring. - Deploying Your First Web Application Securely
A step-by-step guide to deploying web applications with security best practices baked in from the start.
Stay tuned for these upcoming articles that will help you build and maintain a robust, secure server infrastructure. Each article will provide practical, hands-on guidance with real-world examples and solutions.
Final Thoughts
Remember that server security is about finding the right balance between security and usability. While it’s possible to make a server extremely secure, it needs to remain functional for its intended purpose.
Stay informed about:
- Security bulletins for your OS
- Updates for installed packages
- New security threats and mitigation strategies
For ongoing support and updates:
- Join relevant security mailing lists
- Follow security blogs & social media & slack/discord groups
- Participate in system administration communities
- Keep documentation updated
The most secure server is one that’s regularly maintained, monitored, and updated. With the systems we’ve put in place, you now have a solid foundation for maintaining a secure VPS environment.
Remember: Security is not a destination; it’s a journey. Keep learning, stay vigilant, and regularly review your security practices.