Server security isn't a single tool or a single configuration change — it's layers. Each layer addresses a different attack vector, and the combination of all layers is what makes a server genuinely hardened. No single measure is sufficient on its own, but together they raise the cost of an attack to the point where most automated bots and opportunistic attackers move on to easier targets.
This guide covers the OS-level security hardening you're responsible for on a self-managed Ubuntu 24.04 VPS. We'll work through SSH lockdown, firewall rules, intrusion prevention, kernel hardening, and audit logging — everything between you and the infrastructure layer.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $8.30/mo
Want fully managed hosting? — we handle everything
The Security Layer Model: What MassiveGRID Handles vs. What You Handle
Before we start configuring anything, it's worth understanding what's already protected and where your responsibilities begin. Security is a shared responsibility model — your hosting provider secures the infrastructure, and you secure the operating system and applications.
What MassiveGRID handles (infrastructure layer):
- DDoS protection: 12 Tbps of DDoS mitigation capacity at the network edge. Volumetric attacks (UDP floods, SYN floods, amplification attacks) are absorbed before they reach your VM.
- Network isolation: VMs are isolated at the hypervisor level. Other tenants on the same physical host cannot access your VM's memory, storage, or network traffic.
- Hardware redundancy: Proxmox HA cluster automatically migrates your VM if a physical node fails. Ceph 3x replication protects your storage against drive failures.
- Physical security: Data centers with biometric access, surveillance, and 24/7 on-site staff.
What you handle (OS and application layer):
- SSH access control and authentication
- Firewall rules (which ports are open, who can connect)
- Intrusion detection and prevention
- OS and application security updates
- User accounts and privilege management
- Application-level security (SQL injection, XSS, etc.)
- Encryption of data at rest and in transit
This guide covers your side of the equation. If you haven't yet set up your VPS with a non-root user and basic firewall, complete our Ubuntu VPS initial setup guide first — this guide builds on that foundation.
SSH Hardening: Your First Line of Defense
SSH is the front door to your server. By default, it accepts password authentication on port 22 from any IP address in the world. Every one of those defaults is a weakness.
Generate an Ed25519 SSH Key Pair
Ed25519 keys are shorter, faster, and more secure than the older RSA keys. Generate a key pair on your local machine (not the server):
ssh-keygen -t ed25519 -C "your_email@example.com"
When prompted for a file location, press Enter to accept the default (~/.ssh/id_ed25519). Set a strong passphrase — this encrypts the private key on your local disk, so even if someone steals the key file, they can't use it without the passphrase.
Copy your public key to the server:
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@YOUR_SERVER_IP
This appends your public key to ~/.ssh/authorized_keys on the server. Test that key-based authentication works before proceeding:
ssh -i ~/.ssh/id_ed25519 deploy@YOUR_SERVER_IP
If you can log in without entering your server password (though you may need to enter your key passphrase), key authentication is working.
Harden the SSH Daemon Configuration
Now we lock down the SSH daemon itself. Edit the configuration file:
sudo nano /etc/ssh/sshd_config
Make the following changes. Find each directive (some may be commented out with #) and set them to these values:
# Change the default port (use any unused port between 1024-65535)
Port 2222
# Disable root login entirely
PermitRootLogin no
# Disable password authentication (key-only)
PasswordAuthentication no
# Disable empty passwords
PermitEmptyPasswords no
# Use only SSH protocol 2
Protocol 2
# Limit authentication attempts per connection
MaxAuthTries 3
# Set connection timeout (seconds of inactivity before disconnect)
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable X11 forwarding (not needed on a server)
X11Forwarding no
# Disable TCP forwarding unless you specifically need it
AllowTcpForwarding no
# Only allow specific users to connect via SSH
AllowUsers deploy
# Use strong key exchange algorithms, ciphers, and MACs
KexAlgorithms sshd_config curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
Important: Do NOT restart SSH yet if you changed the port. First, update your firewall rules to allow the new port.
Update UFW for the New SSH Port
# Allow the new SSH port
sudo ufw allow 2222/tcp
# Remove the old SSH rule
sudo ufw delete allow OpenSSH
# Verify
sudo ufw status
Now test the configuration file for syntax errors:
sudo sshd -t
If no errors are reported, restart the SSH service:
sudo systemctl restart ssh
Critical: Do NOT close your current SSH session. Open a new terminal window and test the connection with the new port:
ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@YOUR_SERVER_IP
If this works, you can safely close the original session. If it doesn't, your original session is still open and you can revert your changes.
Create an SSH Config Shortcut
Typing the full command every time is tedious. On your local machine, edit ~/.ssh/config:
Host myserver
HostName YOUR_SERVER_IP
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519
Now you can simply run:
ssh myserver
UFW Firewall: Detailed Configuration
In the initial setup guide, we configured basic UFW rules. Now let's add more granular controls.
Rate Limiting
UFW has a built-in rate limiting feature that blocks IPs making more than 6 connection attempts in 30 seconds. Apply it to your SSH port:
sudo ufw limit 2222/tcp
This replaces the simple "allow" rule with one that includes rate limiting. It's effective against brute-force scripts that try thousands of passwords.
Service-Specific Rules
Only open ports that your server actively uses. Here are common rules for different server roles:
# Web server (HTTP + HTTPS)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Mail server
sudo ufw allow 25/tcp # SMTP
sudo ufw allow 587/tcp # SMTP submission
sudo ufw allow 993/tcp # IMAPS
# Database (allow only from specific IP)
sudo ufw allow from 10.0.0.5 to any port 5432 # PostgreSQL from app server only
# Deny a specific abusive IP
sudo ufw deny from 198.51.100.0/24
View and Manage Rules
# List rules with numbers (for deletion)
sudo ufw status numbered
# Delete a specific rule by number
sudo ufw delete 3
# Reset all rules to default (careful!)
sudo ufw reset
Enable Logging
sudo ufw logging medium
Logs are written to /var/log/ufw.log. Medium verbosity logs both blocked and allowed connections that match rate limits, which is useful for detecting suspicious activity.
Fail2Ban: Automated Intrusion Prevention
Fail2Ban monitors log files for repeated authentication failures and automatically bans offending IP addresses by adding firewall rules. It's essential for any internet-facing server.
Install Fail2Ban
sudo apt install -y fail2ban
Create a Local Configuration
Never edit /etc/fail2ban/jail.conf directly — it gets overwritten on updates. Create a local override:
sudo nano /etc/fail2ban/jail.local
Add the following configuration:
[DEFAULT]
# Ban duration: 1 hour for first offense
bantime = 3600
# Time window for counting failures
findtime = 600
# Number of failures before ban
maxretry = 5
# Email notifications (optional — requires mailutils)
# destemail = admin@example.com
# sender = fail2ban@example.com
# action = %(action_mwl)s
# Use UFW as the ban action
banaction = ufw
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 7200
findtime = 300
This configuration:
- Bans IPs for 1 hour by default (2 hours for SSH specifically)
- Looks at a 5-minute window for SSH (10 minutes default)
- Triggers after 3 failed SSH attempts (5 for other services)
- Uses UFW to implement the bans (so they appear in
ufw status)
Enable and Start Fail2Ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Verify It's Working
# Check overall status
sudo fail2ban-client status
# Check the SSH jail specifically
sudo fail2ban-client status sshd
Example output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 47
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 1
|- Total banned: 5
`- Banned IP list: 198.51.100.23
Manually Unban an IP
If you accidentally ban yourself or need to unban a legitimate IP:
sudo fail2ban-client set sshd unbanip 198.51.100.23
Advanced: Progressive Ban Times
For persistent attackers, you can configure Fail2Ban to increase ban duration on repeat offenses. Add a recidive jail to /etc/fail2ban/jail.local:
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 604800 # 1 week
findtime = 86400 # within 1 day
maxretry = 3 # after being banned 3 times
This watches the Fail2Ban log itself. If an IP gets banned 3 times in 24 hours, it gets a 1-week ban. This deals effectively with persistent brute-force bots.
Automatic Security Updates
We covered basic unattended-upgrades in the initial setup guide. Here's the more detailed production configuration.
Edit the unattended-upgrades configuration:
sudo nano /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}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
// Automatically reboot if needed (e.g., kernel updates)
Unattended-Upgrade::Automatic-Reboot "true";
// Reboot at a specific time to minimize impact
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
// Remove unused kernel packages after update
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
// Remove unused auto-installed dependencies
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Don't automatically fix interrupted dpkg
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
// Email notifications for updates (optional)
// Unattended-Upgrade::Mail "admin@example.com";
// Unattended-Upgrade::MailReport "on-change";
Set the automatic update schedule:
sudo nano /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
Verify the configuration is valid:
sudo unattended-upgrades --dry-run --debug
AppArmor: Mandatory Access Control
AppArmor is enabled by default on Ubuntu 24.04. It restricts what each application can do — even if an application is compromised, AppArmor limits the damage by confining it to a predefined set of permissions.
Check AppArmor status:
sudo aa-status
You should see output showing profiles in "enforce" mode. For example:
apparmor module is loaded.
53 profiles are loaded.
37 profiles are in enforce mode.
...
16 profiles are in complain mode.
...
4 processes have profiles defined.
4 processes are in enforce mode.
Enforce mode means AppArmor actively blocks disallowed actions. Complain mode means it logs violations but doesn't block them (useful for testing). You want your production services in enforce mode.
Install additional utilities for managing AppArmor profiles:
sudo apt install -y apparmor-utils
To put a profile that's in complain mode into enforce mode:
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
To see what violations a profile would catch (without blocking):
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
After testing, switch it back to enforce mode. AppArmor works silently in the background — once configured, you rarely need to interact with it unless you're deploying new services.
Kernel Parameter Hardening
The Linux kernel exposes tunable parameters through /proc/sys/ that control network behavior, memory management, and security features. Many default values prioritize compatibility over security. Let's tighten them.
Edit the sysctl configuration:
sudo nano /etc/sysctl.d/99-security.conf
Add the following parameters:
# ===== Network Security =====
# Enable reverse path filtering (prevent IP spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP broadcast requests (prevent smurf attacks)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Ignore bogus ICMP error responses
net.ipv4.icmp_ignore_bogus_error_responses = 1
# Disable source routing (prevent routing manipulation)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Don't accept ICMP redirects (prevent MITM attacks)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Don't send ICMP redirects (not a router)
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Log martian packets (packets with impossible source addresses)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# Enable TCP SYN cookies (protect against SYN flood attacks)
net.ipv4.tcp_syncookies = 1
# ===== Memory Protection =====
# Restrict access to kernel pointers in /proc
kernel.kptr_restrict = 2
# Restrict access to dmesg
kernel.dmesg_restrict = 1
# Restrict use of BPF (Berkeley Packet Filter)
kernel.unprivileged_bpf_disabled = 1
# Enable ASLR (Address Space Layout Randomization)
kernel.randomize_va_space = 2
# Restrict ptrace (process tracing) to parent processes only
kernel.yama.ptrace_scope = 1
# ===== File System =====
# Protect hard links and symbolic links
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
# Protect FIFOs and regular files in world-writable directories
fs.protected_fifos = 2
fs.protected_regular = 2
Apply the changes without rebooting:
sudo sysctl --system
Verify a specific parameter is set:
sudo sysctl net.ipv4.tcp_syncookies
Expected output: net.ipv4.tcp_syncookies = 1
Shared Memory Protection
Shared memory (/run/shm and /dev/shm) can be used by attackers to stage exploits because it's world-writable and executable by default. Restrict it by adding options to your /etc/fstab:
sudo nano /etc/fstab
Add or modify the shared memory entry:
none /run/shm tmpfs defaults,noexec,nosuid,nodev 0 0
The key options:
noexec— prevents executing binaries from shared memorynosuid— prevents setuid/setgid bits from being honorednodev— prevents device files from being created
Remount to apply changes without rebooting:
sudo mount -o remount /run/shm
Verify the mount options:
mount | grep shm
You should see noexec,nosuid,nodev in the options list.
Audit Logging with auditd
The Linux Audit system provides detailed logging of security-relevant events — file access, system calls, authentication events, and privilege escalation. It's invaluable for forensic analysis after a security incident and for compliance requirements.
Install auditd
sudo apt install -y auditd audispd-plugins
Configure Audit Rules
Create a custom rules file:
sudo nano /etc/audit/rules.d/hardening.rules
Add the following rules:
# Monitor changes to user/group files
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
# Monitor changes to sudoers
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# Monitor SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
# Monitor login/logout events
-w /var/log/auth.log -p wa -k auth_log
-w /var/log/faillog -p wa -k logins
-w /var/log/lastlog -p wa -k logins
# Monitor cron configuration
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /etc/cron.daily/ -p wa -k cron
-w /etc/cron.hourly/ -p wa -k cron
-w /etc/cron.weekly/ -p wa -k cron
-w /etc/cron.monthly/ -p wa -k cron
# Monitor kernel module loading
-w /sbin/insmod -p x -k modules
-w /sbin/rmmod -p x -k modules
-w /sbin/modprobe -p x -k modules
# Monitor network configuration changes
-w /etc/hosts -p wa -k hosts
-w /etc/network/ -p wa -k network
-w /etc/netplan/ -p wa -k network
# Make the audit configuration immutable (requires reboot to change)
-e 2
The -w flag watches a file/directory, -p wa means trigger on writes (w) and attribute changes (a), and -k assigns a searchable key to each rule.
Restart auditd to load the rules:
sudo systemctl restart auditd
Querying Audit Logs
# Search for SSH config changes
sudo ausearch -k sshd_config
# Search for authentication events in the last hour
sudo ausearch -k auth_log -ts recent
# Generate a summary report
sudo aureport --summary
# See failed authentication attempts
sudo aureport --auth --failed
Note that the -e 2 rule at the bottom makes the audit configuration immutable — you'll need to reboot the server to change audit rules after this is enabled. Remove that line during initial configuration and testing, and add it back once your rules are finalized.
Security Hardening Checklist
Use this summary table to verify each hardening step is complete:
| Category | Action | Verification Command |
|---|---|---|
| SSH | Ed25519 key authentication only | grep "PasswordAuthentication" /etc/ssh/sshd_config |
| SSH | Root login disabled | grep "PermitRootLogin" /etc/ssh/sshd_config |
| SSH | Non-default port | grep "^Port" /etc/ssh/sshd_config |
| SSH | MaxAuthTries set to 3 | grep "MaxAuthTries" /etc/ssh/sshd_config |
| Firewall | UFW enabled, default deny incoming | sudo ufw status verbose |
| Firewall | SSH rate limiting enabled | sudo ufw status | grep LIMIT |
| Intrusion Prevention | Fail2Ban running with SSH jail | sudo fail2ban-client status sshd |
| Updates | Automatic security updates enabled | systemctl status unattended-upgrades |
| AppArmor | Profiles in enforce mode | sudo aa-status |
| Kernel | Sysctl hardening applied | sudo sysctl net.ipv4.tcp_syncookies |
| Shared Memory | noexec,nosuid,nodev on /run/shm | mount | grep shm |
| Audit | auditd running with custom rules | sudo auditctl -l |
Disable Unnecessary Services
Every running service is a potential attack surface. On a fresh Ubuntu 24.04 VPS, several services may be running that you don't need. List all active services:
sudo systemctl list-units --type=service --state=running
Common services to consider disabling if you don't use them:
# Disable Avahi (mDNS/DNS-SD — not needed on a server)
sudo systemctl stop avahi-daemon
sudo systemctl disable avahi-daemon
# Disable CUPS (printing — definitely not needed on a server)
sudo systemctl stop cups
sudo systemctl disable cups
# Disable ModemManager (modem management — not needed in a VM)
sudo systemctl stop ModemManager
sudo systemctl disable ModemManager
Before disabling any service, verify what it does and whether anything depends on it:
systemctl show -p Description --value SERVICE_NAME
systemctl list-dependencies --reverse SERVICE_NAME
Secure /tmp Directory
The /tmp directory is world-writable and a common staging ground for exploits. If /tmp is not already a separate partition, you can secure it by adding mount options. Check your current /tmp mount:
mount | grep /tmp
If /tmp is part of the root filesystem, you can create a dedicated tmpfs mount. Add to /etc/fstab:
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,size=1G 0 0
Then remount:
sudo mount -o remount /tmp
The noexec flag prevents executing binaries from /tmp, blocking a common class of privilege escalation attacks where malware is downloaded to /tmp and executed.
Ongoing Security Maintenance
Hardening isn't a one-time event. Build these habits into your server management routine:
- Weekly: Review
/var/log/auth.logfor unusual login attempts. Checksudo fail2ban-client status sshdfor banned IPs. - Monthly: Run
sudo apt update && sudo apt upgrademanually to catch non-security updates. Reviewsudo aureport --summaryfor anomalies. - Quarterly: Rotate SSH keys. Review firewall rules and remove any that are no longer needed. Audit user accounts with
cat /etc/passwd | grep -v nologin | grep -v false. - After any incident: Review audit logs with
sudo ausearch -ts today. Check for unauthorized cron jobs withsudo crontab -landls /etc/cron.d/. Verify no new users were created.
Want Dedicated Resource Isolation?
On a shared VPS, your virtual CPU cores are shared with other tenants on the same physical host. While hypervisor-level isolation prevents cross-VM access, a Dedicated VPS (VDS) gives you exclusively allocated CPU cores. No noisy neighbors, no CPU steal time, and a hardware-level guarantee that your compute resources are yours alone. This matters for security-sensitive workloads where consistent performance prevents timing-based side-channel attacks.
Prefer Managed Security?
If maintaining firewall rules, monitoring Fail2Ban, applying kernel patches, and reviewing audit logs sounds like a full-time job — it partially is. MassiveGRID's Managed Dedicated Cloud Servers include proactive security management: OS hardening, firewall configuration, intrusion detection, security patching, and 24/7 monitoring by a team of engineers. You focus on your application; they keep the server secure.