Linux · Self-Hosting

Hardening a Self-Hosted Server

The security hardening checklist I run on every server I deploy — ufw, fail2ban, SSH key enforcement, HSTS headers, and service isolation. No security theater, just working controls.

⏱ 25 min 📊 Intermediate 📅 Updated April 2026
📖 Also read: Practical Cybersecurity for Self-Hosters — the principles behind these hardening steps, plus current 2026 threat landscape.

⚠️ Warning

These commands modify system security settings. Test on a non-production server first. If you're SSH'd into the server you're hardening, keep a second terminal open. A misconfigured firewall or SSH can lock you out.

Prerequisites

A fresh Ubuntu 24.04 LTS installation with root or sudo access. This guide assumes a headless server (no GUI). All commands are run as a non-root user with sudo privileges.

Pre-Flight Checklist

Create a non-root sudo user
Verify SSH access with that user
Have console access (for recovery if locked out)
Note your server's public IP

Phase 1: Initial System Lockdown

Start with updates and essential security packages.

Phase 1 — System Updates
sudo apt update && sudo apt upgrade -y
Reading package lists... Done
Building dependency tree... Done
sudo apt install -y ufw fail2ban unattended-upgrades apt-listchanges
✓ Security packages installed

Phase 2: Firewall (UFW)

UFW (Uncomplicated Firewall) is a user-friendly frontend for iptables. Default policy: deny everything incoming, allow everything outgoing.

Phase 2 — UFW Configuration
# Set defaults BEFORE enabling
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# If you need non-standard SSH port:
# sudo ufw allow 2222/tcp
sudo ufw enable
Command may disrupt existing ssh connections. Proceed? (y|n) y
Firewall is active and enabled on system startup
sudo ufw status verbose
Status: active
Default: deny (incoming), allow (outgoing)
To Action From
-- ------ ----
22/tcp (SSH) ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere

Rate Limiting with UFW

Limit brute-force attempts on SSH:

UFW Rate Limiting
sudo ufw limit ssh/tcp
Rules updated
# This limits connections to 6 attempts in 30 seconds

Phase 3: SSH Hardening

SSH is your primary attack surface. These changes significantly reduce exposure.

⚠️ Critical: Keep a Second Terminal Open

Test each SSH change in a new terminal before closing your current session. If you lock yourself out, you'll need console access to fix it.

If you do lock yourself out: it happens. I've done it. More than once. The first time feels like a disaster. The fifth time is just "oh, I need to reboot into recovery mode again." Every sysadmin has a story about the firewall rule that ate SSH. You're not incompetent — you're learning a system that doesn't forgive small mistakes. Document what you changed, fix it from the console, and move on. The lesson sticks better than any tutorial.

Step 1: Key-Based Authentication

Disable password auth entirely. Generate an SSH key on your local machine if you haven't:

Local Machine — Generate SSH Key
# On your local machine, not the server
ssh-keygen -t ed25519 -C "[email protected]"
Generating public/private ed25519 key pair.
Enter file in which to save the key (~/.ssh/id_ed25519):
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip
✓ Key copied to server

Step 2: Harden SSH Configuration

Edit /etc/ssh/sshd_config:

/etc/ssh/sshd_config
# Disable root login
PermitRootLogin no
# Disable password authentication
PasswordAuthentication no PubkeyAuthentication yes
# Limit authentication attempts
MaxAuthTries 3 MaxSessions 2
# Disable forwarding if not needed (uncomment if you need tunnels)
AllowTcpForwarding no X11Forwarding no
# Change default port (optional, security through obscurity)
# Port 2222
# Limit to specific users only
AllowUsers your_username

Apply changes:

Apply SSH Changes
sudo sshd -t
✓ Configuration test passed (no output = good)
sudo systemctl restart sshd
# DO NOT close your current terminal
# Open a NEW terminal and test: ssh user@server_ip

Phase 4: Fail2Ban

Fail2Ban scans log files and bans IPs showing malicious signs. It catches brute-force attempts that slip past UFW rate limiting.

Phase 4 — Fail2Ban Setup
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo fail2ban-client status
Status: active

Configure Fail2Ban

Create a local configuration file:

/etc/fail2ban/jail.local
[DEFAULT]
# Ban for 1 hour after 3 failed attempts within 10 minutes
bantime = 3600 findtime = 600 maxretry = 3
# Backend for file monitoring
backend = systemd
# Email notifications (optional, requires mail setup)
destemail = [email protected] sendername = Fail2Ban mta = sendmail action = %(action_mwl)s [sshd] enabled = true port = ssh filter = sshd logpath = %(sshd_log)s maxretry = 3
Restart Fail2Ban
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
|- Actions
|- Currently banned: 0
|- Total banned: 0

Did this guide help?

Your answers shape what we write next.

Phase 5: Automatic Security Updates

Unattended-upgrades automatically installs security updates. For a self-hosted server, this is essential.

Phase 5 — Auto-Updates
sudo dpkg-reconfigure -plow unattended-upgrades
Configuring unattended-upgrades
Automatically download and install stable updates?

Verify the configuration:

/etc/apt/apt.conf.d/50unattended-upgrades
# Security updates only
Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; // "${distro_id}:${distro_codename}-updates"; };
# Auto-remove unused dependencies
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true";
# Auto-reboot if required (schedule for 2 AM)
Unattended-Upgrade::Automatic-Reboot "true"; Unattended-Upgrade::Automatic-Reboot-Time "02:00";

Phase 6: Security Headers (Apache/Nginx)

If you're running a web server, add these headers to harden the browser security posture.

For Apache

/etc/apache2/conf-available/security.conf
Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set X-XSS-Protection "1; mode=block" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# HSTS (only after confirming HTTPS works)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Enable Apache Security Headers
sudo a2enmod headers
sudo a2enconf security
sudo apache2ctl configtest
sudo systemctl reload apache2

Phase 7: File Permissions & AIDE

Set proper permissions and monitor for unauthorized changes.

Phase 7 — File Integrity
sudo apt install -y aide
sudo aideinit
Generating database...
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Run daily checks via cron
echo "0 3 * * * root /usr/bin/aide --check" | sudo tee /etc/cron.d/aide-check

Secure Sensitive Files

File Permissions
sudo chmod 600 /etc/ssh/sshd_config
sudo chmod 600 /etc/fail2ban/jail.local
sudo chmod 700 /root
sudo chmod 750 /home/*

Phase 8: Log Monitoring

Logs are your visibility. Configure log rotation and basic monitoring.

Phase 8 — Log Setup
sudo apt install -y logrotate
# View auth logs
sudo tail -f /var/log/auth.log
# View fail2ban logs
sudo tail -f /var/log/fail2ban.log
# View ufw logs
sudo tail -f /var/log/ufw.log

Verification Checklist

Post-Hardening Verification

SSH only accepts keys (test: ssh -o PasswordAuthentication=no user@host)
UFW is active (sudo ufw status)
Fail2Ban is running (sudo systemctl status fail2ban)
Auto-updates configured (cat /etc/apt/apt.conf.d/20auto-upgrades)
Security headers present (curl -I https://yourdomain.com)
No root login (grep PermitRootLogin /etc/ssh/sshd_config)

Security Scan

Run a quick audit with Lynis:

Lynis Security Scan
sudo apt install -y lynis
sudo lynis audit system --quick
[+] Initializing program
[+] Performing tests in category: Authentication
...
Hardening index: [####......] 72/100

What's Next?

Aim for a hardening index above 70. Review Lynis suggestions and implement relevant ones. Re-run the scan monthly after updates.

Next Steps

Join the conversation.

Questions, experiences, or ideas — we're listening.