Securing a dedicated server that you manage over SSH involves layering multiple defenses: strong authentication, a locked-down configuration, a firewall, and intrusion detection with alerting. This guide walks through the full process step by step, from creating a non-root user to setting up Fail2ban with email notifications.
Table of Contents
- Creating a Non-Root User
- Generating and Deploying SSH Keys
- Disabling Password Authentication and Root Login
- Changing the Default SSH Port
- Setting Up ~/.ssh/config for Quick Access
- Fixing File and Directory Permissions
- Configuring the Firewall
- Installing and Configuring Fail2ban
- Advanced Fail2ban Hardening
- Email Alerts for Bans
---
1. Creating a Non-Root User
Never work directly as root. Create a dedicated user with sudo privileges instead.
adduser deploy
usermod -aG sudo deployadduser will prompt for a password and some optional info (you can leave those blank). usermod -aG sudo deploy adds the user to the sudo group, letting them run privileged commands.
Verify:
groups deployYou should see sudo listed among the groups.
---
2. Generating and Deploying SSH Keys
Password-based SSH login is vulnerable to brute-force attacks. Key-based authentication is far stronger.
Generate a key pair (on your local machine)
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/my_server_key-t ed25519— modern, fast, and secure algorithm (preferred over RSA)-a 100— increases the key derivation rounds, making brute-forcing the passphrase harder-f— output file path
This creates two files: my_server_key (private, never share this) and my_server_key.pub (public, safe to share).
Copy the public key to the server
ssh-copy-id -i ~/.ssh/my_server_key.pub deploy@server_ipThis appends your public key to ~/.ssh/authorized_keys on the server for the deploy user.
Manual method (if ssh-copy-id isn't available)
cat ~/.ssh/my_server_key.pub | ssh deploy@server_ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"Verify:
ssh -i ~/.ssh/my_server_key deploy@server_ipYou should log in without being asked for a password.
---
3. Disabling Password Authentication and Root Login
Once key-based login works, close the password-based door entirely.
Edit the SSH daemon config:
sudo nano /etc/ssh/sshd_configSet (or uncomment and change) these values:
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin noPasswordAuthentication no— disables login with a password entirely, forcing key-based authPermitRootLogin no— prevents anyone from logging in directly asroot, even with a key. Admins must log in as a regular user and usesudo
Restart SSH to apply:
sudo systemctl restart sshdImportant: Before closing your current session, open a second terminal and test that you can still log in with your key and non-root user. If something is misconfigured, you don't want to lock yourself out.
ssh -i ~/.ssh/my_server_key deploy@server_ip---
4. Changing the Default SSH Port
Moving off port 22 doesn't stop a targeted attacker, but it drastically cuts down the noise from automated bots that scan the internet for open port 22.
sudo nano /etc/ssh/sshd_configPort 2222Restart SSH:
sudo systemctl restart sshdBefore disconnecting, make sure your firewall allows the new port (see Section 7) — otherwise you'll lock yourself out.
Verify:
ssh -i ~/.ssh/my_server_key -p 2222 deploy@server_ip---
5. Setting Up ~/.ssh/config for Quick Access
Instead of typing the full SSH command every time, define a shortcut locally.
On your local machine, edit or create:
nano ~/.ssh/configAdd:
Host myserver
HostName server_ip
User deploy
Port 2222
IdentityFile ~/.ssh/my_server_key
IdentitiesOnly yesIdentitiesOnly yesensures SSH only tries the key you specified, rather than offering every key in your~/.sshfolder (avoids hitting the server'sMaxAuthTrieslimit and getting temporarily locked out)
Now you can simply run:
ssh myserver---
6. Fixing File and Directory Permissions
SSH is strict about permissions — if they're too open, it will silently refuse to use your keys or authorized_keys file.
On the server, for the logged-in user:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keysOn your local machine, for your private key:
chmod 600 ~/.ssh/my_server_key
chmod 644 ~/.ssh/my_server_key.pub| Path | Required permission | Meaning |
|---|---|---|
~/.ssh | 700 | Only the owner can read/write/execute (enter) the folder |
~/.ssh/authorized_keys | 600 | Only the owner can read/write |
| Private key file | 600 | Only the owner can read/write |
| Public key file | 644 | Owner can read/write, others can read |
Verify:
ls -la ~/.ssh---
7. Configuring the Firewall
Restrict which ports are reachable from the internet.
Using UFW (Ubuntu's firewall front-end)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp
sudo ufw enableIf you have a static home/office IP, restrict SSH access to just that IP:
sudo ufw allow from YOUR_STATIC_IP to any port 2222 proto tcpVerify:
sudo ufw status verboseWarning: Always allow your new SSH port before enabling UFW, and before changingPermitRootLogin/PasswordAuthenticationif you're doing all of this remotely. Test each change in a second session before closing your original one.
---
8. Installing and Configuring Fail2ban
Fail2ban watches your logs and automatically bans IP addresses that show repeated failed login attempts, by adding temporary firewall rules.
Install
sudo apt update
sudo apt install fail2ban -yCreate a local override file
Never edit jail.conf directly — it gets overwritten on package updates. Instead:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.localConfigure the SSH jail
sudo nano /etc/fail2ban/jail.local[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 YOUR_STATIC_IP
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600| Parameter | Meaning |
|---|---|
port | Which port gets blocked by the firewall when a ban happens (must match your actual SSH port) |
filter | Which regex ruleset (/etc/fail2ban/filter.d/sshd.conf) is used to recognize failed login attempts in the log |
logpath | Which log file to monitor |
maxretry | Number of failures tolerated before a ban |
findtime | Time window (seconds) in which those failures are counted |
bantime | How long (seconds) the IP stays banned |
Test the filter before relying on it:
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.confValidate the full config syntax:
sudo fail2ban-client -tStart and enable the service:
sudo systemctl enable --now fail2banVerify:
sudo systemctl status fail2ban
sudo fail2ban-client status sshdManually test a ban / unban:
sudo fail2ban-client set sshd banip 1.2.3.4
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 1.2.3.4---
9. Advanced Fail2ban Hardening
Aggressive detection mode
[sshd]
filter = sshd[mode=aggressive]Modes range from normal (default) to aggressive (catches more attack patterns, including slow/partial connection attempts).
Recidive jail — punish repeat offenders harder
This jail watches Fail2ban's own log and re-bans IPs that have already been banned multiple times, for a much longer period.
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 604800
findtime = 86400
maxretry = 3Progressive ban times (exponential backoff)
[DEFAULT]
bantime = 3600
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800
bantime.rndtime = 300Each repeat offense from the same IP doubles the ban duration (1h → 2h → 4h → ...), capped at 7 days, with a bit of randomness added so the exact expiry can't be predicted.
Verify current effective values:
sudo fail2ban-client get sshd bantime
sudo fail2ban-client get sshd maxretry---
10. Email Alerts for Bans
To get notified whenever Fail2ban bans an IP, you need a working mail transfer agent (MTA) on the server, plus a Fail2ban action that includes an email step.
Install Postfix and mail tools
sudo apt install postfix mailutils whois -yChoose "Internet Site" during setup. whois is required because the email action includes IP ownership lookup info.
Configure Postfix to relay through an external SMTP provider
Direct outbound email from a VPS/dedicated server is often blocked or lands in spam, so route mail through a provider (e.g. your domain's mail host, SendGrid, Mailgun).
sudo nano /etc/postfix/main.cfrelayhost = [smtp.yourprovider.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encryptCreate the credentials file:
sudo nano /etc/postfix/sasl_passwd[smtp.yourprovider.com]:587 you@yourdomain.com:YOUR_PASSWORDIf using Gmail as the relay, you cannot use your normal account password — you must generate an App Password (Google Account → Security → 2-Step Verification → App Passwords), since Gmail blocks direct password-based SMTP login for third-party apps.
Apply and secure the credentials file:
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfixTest that the server can send mail at all:
echo "Test message" | mail -s "Fail2ban test" you@yourdomain.comCheck /var/log/mail.log if it doesn't arrive:
sudo tail -f /var/log/mail.logLook for status=sent (delivered successfully), status=deferred (temporary failure, will retry), or status=bounced / SASL authentication failed (credentials or relay misconfigured).
Common pitfall: many hosting providers (DigitalOcean, Hetzner, OVH, etc.) block outbound port 25 by default to prevent spam. Use port 587 (submission, with authentication) for the relay instead.
Enable email notifications in Fail2ban
sudo nano /etc/fail2ban/jail.local[DEFAULT]
destemail = you@yourdomain.com
sender = you@yourdomain.com
mta = sendmail
action = %(action_mwl)sUse the same address for sender as the one you authenticate with in sasl_passwd — most SMTP relays (Gmail included) reject or rewrite mail where the "From" doesn't match the authenticated account.
action_mwl combines the firewall ban with an email containing WHOIS info and the matching log lines.
Restart and verify the jail is actually using the mail action:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client get sshd actionsYou should see something like:
iptables-multiport, sendmail-whois-linesIf you only see iptables-multiport, your [sshd] section likely has its own action = line overriding the [DEFAULT] one — remove it so it inherits from [DEFAULT].
Test end-to-end:
sudo fail2ban-client set sshd banip 1.2.3.4
mailq
sudo tail -n 30 /var/log/mail.log---
Summary Checklist
- [ ] Created a non-root sudo user
- [ ] Generated an Ed25519 SSH key pair and deployed the public key
- [ ] Verified key-based login works in a second session
- [ ] Disabled
PasswordAuthenticationandPermitRootLogin - [ ] Changed the default SSH port
- [ ] Set up
~/.ssh/configfor quick, consistent access - [ ] Fixed
.sshfolder and key file permissions (700 / 600) - [ ] Configured UFW to deny by default and allow only the SSH port (+ optionally restrict by source IP)
- [ ] Installed and configured Fail2ban with a working
jail.local - [ ] Tested the Fail2ban filter and full config syntax
- [ ] Added progressive ban times and a recidive jail
- [ ] Set up email alerts for bans, with a working relay and verified delivery
---
Conclusion
None of these measures is a silver bullet on its own — that's the point. SSH key authentication closes the door that password brute-forcing relies on. Disabling root login and moving off the default port cut down the attack surface and the noise. A firewall makes sure only the ports you actually need are reachable at all. Fail2ban adds an active layer on top, watching for abuse in real time and shutting it down automatically, while email alerts make sure you're not flying blind about what's happening to your server.
Layered together, these steps take a server from "wide open to anyone scanning the internet" to something that requires a stolen private key, physical access, or a genuinely novel exploit to compromise over SSH — a bar high enough to stop the overwhelming majority of automated attacks outright.
A few closing habits worth keeping:
- Test every change in a second session before closing the one you're already in. This is the single most common way people lock themselves out of their own server.
- Keep the system updated. None of this matters much if an unpatched vulnerability gives an attacker another way in entirely.
- Review your Fail2ban logs occasionally. They're a useful, low-effort window into who's probing your server and how often.
- Treat this as a baseline, not a finish line. Depending on what the server runs, you may also want to look into two-factor SSH authentication, centralized logging, or intrusion detection tools like CrowdSec as a next step.