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

  1. Creating a Non-Root User
  2. Generating and Deploying SSH Keys
  3. Disabling Password Authentication and Root Login
  4. Changing the Default SSH Port
  5. Setting Up ~/.ssh/config for Quick Access
  6. Fixing File and Directory Permissions
  7. Configuring the Firewall
  8. Installing and Configuring Fail2ban
  9. Advanced Fail2ban Hardening
  10. Email Alerts for Bans

---

1. Creating a Non-Root User

Never work directly as root. Create a dedicated user with sudo privileges instead.

bash
adduser deploy
usermod -aG sudo deploy

adduser 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:

bash
groups deploy

You 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)

bash
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

bash
ssh-copy-id -i ~/.ssh/my_server_key.pub deploy@server_ip

This appends your public key to ~/.ssh/authorized_keys on the server for the deploy user.

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

bash
cat ~/.ssh/my_server_key.pub | ssh deploy@server_ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Verify:

bash
ssh -i ~/.ssh/my_server_key deploy@server_ip

You 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:

bash
sudo nano /etc/ssh/sshd_config

Set (or uncomment and change) these values:

text
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin no
  • PasswordAuthentication no — disables login with a password entirely, forcing key-based auth
  • PermitRootLogin no — prevents anyone from logging in directly as root, even with a key. Admins must log in as a regular user and use sudo

Restart SSH to apply:

bash
sudo systemctl restart sshd
Important: 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.
bash
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.

bash
sudo nano /etc/ssh/sshd_config
text
Port 2222

Restart SSH:

bash
sudo systemctl restart sshd

Before disconnecting, make sure your firewall allows the new port (see Section 7) — otherwise you'll lock yourself out.

Verify:

bash
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:

bash
nano ~/.ssh/config

Add:

text
Host myserver
    HostName server_ip
    User deploy
    Port 2222
    IdentityFile ~/.ssh/my_server_key
    IdentitiesOnly yes
  • IdentitiesOnly yes ensures SSH only tries the key you specified, rather than offering every key in your ~/.ssh folder (avoids hitting the server's MaxAuthTries limit and getting temporarily locked out)

Now you can simply run:

bash
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:

bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

On your local machine, for your private key:

bash
chmod 600 ~/.ssh/my_server_key
chmod 644 ~/.ssh/my_server_key.pub
PathRequired permissionMeaning
~/.ssh700Only the owner can read/write/execute (enter) the folder
~/.ssh/authorized_keys600Only the owner can read/write
Private key file600Only the owner can read/write
Public key file644Owner can read/write, others can read

Verify:

bash
ls -la ~/.ssh

---

7. Configuring the Firewall

Restrict which ports are reachable from the internet.

Using UFW (Ubuntu's firewall front-end)

bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp
sudo ufw enable

If you have a static home/office IP, restrict SSH access to just that IP:

bash
sudo ufw allow from YOUR_STATIC_IP to any port 2222 proto tcp

Verify:

bash
sudo ufw status verbose
Warning: Always allow your new SSH port before enabling UFW, and before changing PermitRootLogin/PasswordAuthentication if 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

bash
sudo apt update
sudo apt install fail2ban -y

Create a local override file

Never edit jail.conf directly — it gets overwritten on package updates. Instead:

bash
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Configure the SSH jail

bash
sudo nano /etc/fail2ban/jail.local
ini
[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
ParameterMeaning
portWhich port gets blocked by the firewall when a ban happens (must match your actual SSH port)
filterWhich regex ruleset (/etc/fail2ban/filter.d/sshd.conf) is used to recognize failed login attempts in the log
logpathWhich log file to monitor
maxretryNumber of failures tolerated before a ban
findtimeTime window (seconds) in which those failures are counted
bantimeHow long (seconds) the IP stays banned

Test the filter before relying on it:

bash
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

Validate the full config syntax:

bash
sudo fail2ban-client -t

Start and enable the service:

bash
sudo systemctl enable --now fail2ban

Verify:

bash
sudo systemctl status fail2ban
sudo fail2ban-client status sshd

Manually test a ban / unban:

bash
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

ini
[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.

ini
[recidive]
enabled  = true
filter   = recidive
logpath  = /var/log/fail2ban.log
bantime  = 604800
findtime = 86400
maxretry = 3

Progressive ban times (exponential backoff)

ini
[DEFAULT]
bantime           = 3600
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 604800
bantime.rndtime   = 300

Each 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:

bash
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

bash
sudo apt install postfix mailutils whois -y

Choose "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).

bash
sudo nano /etc/postfix/main.cf
text
relayhost = [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 = encrypt

Create the credentials file:

bash
sudo nano /etc/postfix/sasl_passwd
text
[smtp.yourprovider.com]:587 you@yourdomain.com:YOUR_PASSWORD

If 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:

bash
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfix

Test that the server can send mail at all:

bash
echo "Test message" | mail -s "Fail2ban test" you@yourdomain.com

Check /var/log/mail.log if it doesn't arrive:

bash
sudo tail -f /var/log/mail.log

Look 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

bash
sudo nano /etc/fail2ban/jail.local
ini
[DEFAULT]
destemail = you@yourdomain.com
sender    = you@yourdomain.com
mta       = sendmail
action    = %(action_mwl)s

Use 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:

bash
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client get sshd actions

You should see something like:

text
iptables-multiport, sendmail-whois-lines

If 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:

bash
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 PasswordAuthentication and PermitRootLogin
  • [ ] Changed the default SSH port
  • [ ] Set up ~/.ssh/config for quick, consistent access
  • [ ] Fixed .ssh folder 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.