When you expose a server to the internet — whether it’s hosting your personal cloud, a website, or home services — security becomes critical. Default configurations are designed for convenience, not security. Without proper hardening, your server is vulnerable to brute force attacks, unauthorized access, and potential compromise.

💡 This article contains affiliate links. If you buy through them, we earn a small commission at no extra cost to you. Learn more.

This comprehensive guide walks you through essential security measures every self-hoster should implement: SSH key authentication, two-factor authentication, firewall configuration, automatic security updates, and additional hardening techniques that will significantly reduce your attack surface.

Why Server Hardening Matters

Out of the box, most Linux distributions allow password authentication over SSH, run unnecessary services, and have permissive firewall rules. Within minutes of exposing port 22 to the internet, you’ll see thousands of automated login attempts in your logs.

A compromised server can lead to:

  • Data theft — access to your personal files, emails, or sensitive information
  • Resource abuse — your server used for cryptocurrency mining or DDoS attacks
  • Lateral movement — attackers pivoting to other devices on your network
  • Reputation damage — your IP blacklisted for sending spam or hosting malware

Hardening isn’t paranoia; it’s essential baseline security. Let’s build those defenses.

Prerequisites

For this guide, you’ll need:

  • A Linux server (Ubuntu 22.04/24.04, Debian 11/12, or similar)
  • Root or sudo access
  • Basic command-line familiarity
  • An SSH client on your local machine

These steps work on cloud VPS, dedicated servers, or home servers. I’m using Ubuntu 24.04 LTS for examples, but commands are nearly identical across distributions.

Step 1: SSH Key Authentication

Password authentication is the weakest link. Even strong passwords can fall to brute force given enough time. SSH keys provide cryptographic authentication that’s virtually impossible to crack.

Generate an SSH Key Pair

On your local machine (not the server), generate an ED25519 key pair:

1
ssh-keygen -t ed25519 -C "your_email@example.com"

When prompted:

  • Save to the default location (~/.ssh/id_ed25519)
  • Set a strong passphrase (this protects the private key if your laptop is stolen)

This creates two files:

  • ~/.ssh/id_ed25519 — your private key (never share this)
  • ~/.ssh/id_ed25519.pub — your public key (safe to copy to servers)

Why ED25519? It’s modern, fast, and more secure than older RSA keys at equivalent key lengths. If compatibility with very old systems is needed, use RSA 4096-bit: ssh-keygen -t rsa -b 4096.

Copy Your Public Key to the Server

Use ssh-copy-id to install your public key:

1
ssh-copy-id -i ~/.ssh/id_ed25519.pub username@your-server-ip

Enter your password one last time. The tool appends your public key to ~/.ssh/authorized_keys on the server.

Manual method (if ssh-copy-id isn’t available):

1
cat ~/.ssh/id_ed25519.pub | ssh username@your-server-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Test Key-Based Login

Before disabling password authentication, verify key login works:

1
ssh -i ~/.ssh/id_ed25519 username@your-server-ip

You should authenticate without entering your server password (only the key passphrase if you set one). If this works, you’re ready to disable password auth.

Step 2: Disable Password Authentication

Now that key authentication works, lock the door on passwords.

Edit the SSH daemon configuration:

1
sudo nano /etc/ssh/sshd_config

Find and modify these lines (uncomment by removing # if needed):

PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes

Additional hardening in the same file:

PermitRootLogin no
PubkeyAuthentication yes
PermitEmptyPasswords no
X11Forwarding no
MaxAuthTries 3
MaxSessions 2

Here’s what these do:

  • PermitRootLogin no — forces use of a regular user account with sudo (limits blast radius)
  • MaxAuthTries 3 — only three authentication attempts before disconnect
  • X11Forwarding no — disables graphical forwarding (reduce attack surface)

Save and exit (Ctrl+X, Y, Enter).

Restart SSH to apply changes:

1
sudo systemctl restart sshd

⚠️ Don’t close your current SSH session yet! Open a new terminal and test login to ensure you didn’t lock yourself out. If the new connection works with your key, you’re good. If not, you still have the old session to fix the config.

Step 3: Set Up Two-Factor Authentication

SSH keys are excellent, but adding 2FA creates a second layer: “something you have” (the key) plus “something you know” (the 2FA code). Even if your private key is stolen, an attacker can’t login without the time-based code.

Install Google Authenticator PAM Module

1
2
sudo apt update
sudo apt install libpam-google-authenticator

Configure 2FA for Your User

Run the setup as your regular user (not root):

1
google-authenticator

Answer the prompts:

  • Time-based tokens? Yes
  • Update .google_authenticator? Yes
  • Scan the QR code with your authenticator app (Google Authenticator, Authy, or 1Password)
  • Save the emergency scratch codes somewhere safe
  • Disallow multiple uses? Yes
  • Increase time window? No (unless you have clock sync issues)
  • Rate limiting? Yes

This creates ~/.google_authenticator with your secret and settings.

Enable 2FA in SSH

Edit the PAM configuration:

1
sudo nano /etc/pam.d/sshd

Add this line at the top:

auth required pam_google_authenticator.so

Comment out the common-auth line to prevent password fallback:

# @include common-auth

Save and exit.

Now edit SSH config:

1
sudo nano /etc/ssh/sshd_config

Add or modify:

ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

What this does: Requires both pubkey (your SSH key) AND keyboard-interactive (the 2FA code). You can’t login with just one or the other.

Restart SSH:

1
sudo systemctl restart sshd

Test 2FA Login

Open a new terminal and connect:

1
ssh username@your-server-ip

You’ll be prompted for your verification code after key authentication. Enter the 6-digit code from your authenticator app.

If it works, congrats — your server now requires both key and 2FA. If not, use your existing session to debug the config.

Tip: Keep your emergency scratch codes in a password manager. Each can be used once if you lose access to your authenticator app.

Step 4: Configure a Firewall with UFW

Uncomplicated Firewall (UFW) is a user-friendly frontend for iptables. Default-deny with explicit allowlist is the gold standard.

Install UFW

1
sudo apt install ufw

Set Default Policies

Deny all incoming, allow all outgoing:

1
2
sudo ufw default deny incoming
sudo ufw default allow outgoing

Allow SSH (Critical!)

Before enabling the firewall, allow SSH or you’ll lock yourself out:

1
sudo ufw allow 22/tcp comment 'SSH'

If you run SSH on a non-standard port (good practice), use that port instead:

1
sudo ufw allow 2222/tcp comment 'SSH custom port'

Allow Additional Services

Only open ports for services you’re actually running:

1
2
3
4
5
6
7
8
9
# HTTP and HTTPS for web servers
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Example: Plex media server
sudo ufw allow 32400/tcp comment 'Plex'

# Example: WireGuard VPN
sudo ufw allow 51820/udp comment 'WireGuard'

Enable UFW

1
sudo ufw enable

Confirm when prompted. Check status:

1
sudo ufw status verbose

You should see your allowed ports and default deny policy.

Rate limiting SSH (optional but recommended):

1
2
sudo ufw delete allow 22/tcp
sudo ufw limit 22/tcp comment 'SSH rate limited'

This allows 6 connection attempts from an IP within 30 seconds, then blocks that IP temporarily. Slows down brute force attacks.

Step 5: Automatic Security Updates

Manually updating servers is error-prone. Automatic security updates reduce the window of vulnerability for critical exploits.

Install Unattended Upgrades

1
sudo apt install unattended-upgrades

Configure Automatic Updates

Edit the config:

1
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Ensure these lines are uncommented:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";

Automatic-Reboot — set to true if you want the server to reboot automatically after kernel updates (usually at night). For production, set a time:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

Save and exit.

Enable the service:

1
sudo dpkg-reconfigure -plow unattended-upgrades

Select “Yes” to enable automatic updates.

Check logs to verify it’s working:

1
sudo tail -f /var/log/unattended-upgrades/unattended-upgrades.log

Step 6: Additional Hardening Measures

Disable Unused Services

List running services:

1
sudo systemctl list-unit-files --state=enabled

Disable anything you don’t need. For example, if you’re not using Bluetooth:

1
2
sudo systemctl disable bluetooth.service
sudo systemctl stop bluetooth.service

Common candidates: cups (printing), avahi-daemon (mDNS), ModemManager.

Change SSH Port (Security Through Obscurity)

Not a replacement for real security, but changing SSH from port 22 to something random dramatically reduces automated attack noise.

Edit SSH config:

1
sudo nano /etc/ssh/sshd_config

Change:

Port 2222

Update your firewall:

1
2
3
sudo ufw allow 2222/tcp
sudo ufw delete allow 22/tcp
sudo systemctl restart sshd

Connect using the new port:

1
ssh -p 2222 username@your-server-ip

Set Up Fail2Ban

Fail2Ban monitors logs and bans IPs with repeated failed login attempts. Even with SSH keys and 2FA, it’s good defense-in-depth.

Install:

1
sudo apt install fail2ban

Create a local config:

1
sudo nano /etc/fail2ban/jail.local

Add:

1
2
3
4
5
6
7
8
9
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true
port = 22
logpath = /var/log/auth.log

Adjust port if you changed your SSH port. This bans IPs for 1 hour after 5 failed attempts in 10 minutes.

Start and enable:

1
2
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Check banned IPs:

1
sudo fail2ban-client status sshd

Enable Kernel Hardening with sysctl

Edit sysctl configuration:

1
sudo nano /etc/sysctl.conf

Add these security settings:

# IP Spoofing protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0

# Ignore send redirects
net.ipv4.conf.all.send_redirects = 0

# Disable source packet routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0

# Log Martians
net.ipv4.conf.all.log_martians = 1

# Ignore ICMP ping requests
net.ipv4.icmp_echo_ignore_all = 1

# Ignore Broadcast Request
net.ipv4.icmp_echo_ignore_broadcasts = 1

# SYN flood protection
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5

Apply changes:

1
sudo sysctl -p

Secure Shared Memory

Shared memory can be exploited. Mount it with noexec:

1
sudo nano /etc/fstab

Add:

tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0

Remount:

1
sudo mount -o remount /run/shm

Use AppArmor or SELinux

Ubuntu ships with AppArmor enabled by default. Verify:

1
sudo aa-status

You should see profiles loaded and enforced. Don’t disable it unless absolutely necessary.

For Debian/CentOS users, consider enabling SELinux for additional mandatory access controls.

Step 7: Monitor and Audit

Security is ongoing. Set up basic monitoring:

Check for Root Logins

1
sudo grep "session opened for user root" /var/log/auth.log

Should be empty if PermitRootLogin no is working.

Review Failed SSH Attempts

1
sudo grep "Failed password" /var/log/auth.log | tail -20

You’ll see automated attacks. With key-only auth and Fail2Ban, these are just noise.

Set Up LogWatch

LogWatch emails daily summaries of log activity:

1
sudo apt install logwatch

Configure email (requires mail server or external SMTP):

1
sudo nano /etc/cron.daily/00logwatch

Add:

1
/usr/sbin/logwatch --output mail --mailto you@example.com --detail high

Enable Auditd for Advanced Logging

For compliance or high-security needs:

1
2
3
sudo apt install auditd
sudo systemctl enable auditd
sudo systemctl start auditd

Creates detailed logs of system calls, file access, and user activity.

Hardware Considerations

If you’re building or upgrading a self-hosted server, invest in reliability and security:

  • UPS (Uninterruptible Power Supply) — protects against power loss corruption (search Amazon)
  • Hardware firewall — dedicated device like a mini PC running pfSense or OPNsense for network segmentation
  • Encrypted drives — LUKS encryption for disk-level protection (NVMe SSDs for performance)

Common Mistakes to Avoid

  • Locking yourself out — always test SSH config changes in a separate session before disconnecting
  • Forgetting 2FA backup codes — store emergency codes securely
  • Opening all ports “just in case” — only allow what you actively use
  • Ignoring updates — automatic security updates prevent easy exploits
  • Using weak sudo passwords — your user password is now the weakest link; make it strong
  • No backups — security and backups go hand-in-hand; test restore procedures

Testing Your Security

After hardening, audit your work:

Port Scan from External IP

Use GRC ShieldsUP or nmap from another machine:

1
nmap -sS -sV -O your-server-ip

Only your intentionally open ports should appear.

SSH Brute Force Test

Intentionally fail login attempts from a different IP:

1
ssh wrong-user@your-server-ip

After maxretry attempts, Fail2Ban should block the source IP.

Check for Known Vulnerabilities

1
2
sudo apt install lynis
sudo lynis audit system

Lynis performs a comprehensive security audit and suggests improvements.

Going Further

This guide covers essential hardening, but security is a spectrum:

  • Intrusion Detection Systems (IDS)Wazuh or OSSEC for advanced threat detection
  • VPN-only access — hide SSH behind WireGuard; no public SSH port at all
  • Hardware security keys — use YubiKey for 2FA instead of TOTP apps
  • Containerization — isolate services in Docker with AppArmor profiles
  • Network segmentation — separate IoT, guest, and server networks with VLANs
  • CrowdSec — collaborative intrusion prevention that shares threat intelligence

Conclusion

A hardened Linux server won’t be compromised by script kiddies running automated scans. You’ve implemented:

✅ SSH key authentication with password auth disabled
✅ Two-factor authentication for SSH access
✅ Firewall with default-deny and explicit allow rules
✅ Automatic security updates
✅ Fail2Ban to block repeated attack attempts
✅ Kernel hardening and service minimization

Security is a mindset, not a checklist. Stay informed about vulnerabilities affecting your software stack. Subscribe to security mailing lists for your distribution. Review logs periodically. Test your backups.

Your self-hosted services are now protected by multiple layers of defense. Sleep better knowing that your server can withstand the constant barrage of internet threats.

What hardening steps have you implemented? Any favorite security tools I didn’t cover? Share your setup in the comments or reach out on Twitter @selfhostwise.


Looking for more self-hosting guides? Check out our tutorials section for step-by-step walkthroughs of popular homelab services.