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 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:
| |
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:
| |
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):
| |
Test Key-Based Login
Before disabling password authentication, verify key login works:
| |
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:
| |
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:
| |
⚠️ 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
| |
Configure 2FA for Your User
Run the setup as your regular user (not root):
| |
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:
| |
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:
| |
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:
| |
Test 2FA Login
Open a new terminal and connect:
| |
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
| |
Set Default Policies
Deny all incoming, allow all outgoing:
| |
Allow SSH (Critical!)
Before enabling the firewall, allow SSH or you’ll lock yourself out:
| |
If you run SSH on a non-standard port (good practice), use that port instead:
| |
Allow Additional Services
Only open ports for services you’re actually running:
| |
Enable UFW
| |
Confirm when prompted. Check status:
| |
You should see your allowed ports and default deny policy.
Rate limiting SSH (optional but recommended):
| |
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
| |
Configure Automatic Updates
Edit the config:
| |
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:
| |
Select “Yes” to enable automatic updates.
Check logs to verify it’s working:
| |
Step 6: Additional Hardening Measures
Disable Unused Services
List running services:
| |
Disable anything you don’t need. For example, if you’re not using Bluetooth:
| |
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:
| |
Change:
Port 2222
Update your firewall:
| |
Connect using the new port:
| |
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:
| |
Create a local config:
| |
Add:
| |
Adjust port if you changed your SSH port. This bans IPs for 1 hour after 5 failed attempts in 10 minutes.
Start and enable:
| |
Check banned IPs:
| |
Enable Kernel Hardening with sysctl
Edit sysctl configuration:
| |
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:
| |
Secure Shared Memory
Shared memory can be exploited. Mount it with noexec:
| |
Add:
tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0
Remount:
| |
Use AppArmor or SELinux
Ubuntu ships with AppArmor enabled by default. Verify:
| |
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
| |
Should be empty if PermitRootLogin no is working.
Review Failed SSH Attempts
| |
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:
| |
Configure email (requires mail server or external SMTP):
| |
Add:
| |
Enable Auditd for Advanced Logging
For compliance or high-security needs:
| |
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:
| |
Only your intentionally open ports should appear.
SSH Brute Force Test
Intentionally fail login attempts from a different IP:
| |
After maxretry attempts, Fail2Ban should block the source IP.
Check for Known Vulnerabilities
| |
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.