Hardening SSH Server Security on Ubuntu VPS

Hardening SSH Server Security on Ubuntu VPS

What You’ll Need

  • A clean Ubuntu 22.04 or 24.04 LTS server running on Hetzner VPS or Contabo VPS
  • DigitalOcean as an alternative cloud provider
  • Namecheap for managing your custom domain or reverse DNS hostnames
  • A local terminal with ssh-keygen and OpenSSH installed
  • Root access or a user account with sudo privileges

Table of Contents


Step 1: User Account Creation and SSH Key Authentication

When you provision a fresh server instance on Hetzner VPS or DigitalOcean, you are often handed a default root account accessible via password. Leaving default entry points open exposes your system to automated botnets scanning IP ranges around the clock.

First, log into your server as root:

ssh root@YOUR_SERVER_IP

Create a new dedicated system user account with administrative access. Replace sysadmin with your chosen username:

adduser sysadmin

Fill in the prompt details or hit enter to leave them default. Next, grant this user root privileges by adding them to the sudo group:

usermod -aG sudo sysadmin

Now configure key-based authentication. Modern cryptographic standards mandate Ed25519 key pairs due to their superior performance and security profile compared to legacy RSA keys. On your local machine (not the VPS), open your terminal and run:

ssh-keygen -t ed25519 -C "admin-server-key"

Save the file in the default directory (~/.ssh/id_ed25519) and set a strong passphrase for your private key.

To transfer the public key to your new user account on the remote host, execute ssh-copy-id from your local machine:

ssh-copy-id -i ~/.ssh/id_ed25519.pub sysadmin@YOUR_SERVER_IP

If ssh-copy-id is unavailable on your local host, perform the manual setup on the remote host:

su - sysadmin
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys

Paste your public key string into ~/.ssh/authorized_keys, save, and enforce strict file permissions:

chmod 600 ~/.ssh/authorized_keys

Verify you can log in from a new local terminal session without using the root account:

ssh -i ~/.ssh/id_ed25519 sysadmin@YOUR_SERVER_IP

Whether you host workers for Building Production Web Scraping Pipelines With Python or custom enterprise tools, isolating system access to non-root users is your primary defense line.

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.


Step 2: Configuring OpenSSH Server Daemon for Maximum Hardening

Now that key-based access works for a non-root user, it is time to lock down the OpenSSH daemon configuration. Instead of directly editing /etc/ssh/sshd_config, Ubuntu supports modular drop-in configurations inside /etc/ssh/sshd_config.d/.

Create a custom configuration drop-in named 50-hardening.conf:

sudo nano /etc/ssh/sshd_config.d/50-hardening.conf

Paste the following complete configuration block:

Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PermitEmptyPasswords no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
MaxAuthTries 3
MaxSessions 2
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers sysadmin
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

Let us review what each configuration parameter achieves:

  • Port 2222: Shifts SSH off standard port 22, cutting out background automated scanner noise.
  • PermitRootLogin no: Completely disables direct root logins over SSH.
  • PasswordAuthentication no: Enforces strict key-based authentication, rendering password bruteforce attacks ineffective.
  • AllowUsers sysadmin: Restricts SSH access exclusively to explicitly specified user accounts.
  • MaxAuthTries 3: Drops connections after three failed authentication attempts.
  • ClientAliveInterval 300 and ClientAliveCountMax 2: Closes idle sessions automatically after 10 minutes of inactivity.
  • Cryptographic restrictions (KexAlgorithms, Ciphers, MACs): Limits handshake negotiations strictly to secure modern ciphers, removing legacy SHA-1 and weak Diffie-Hellman groups.

Before applying changes, test your SSH configuration syntax to prevent locking yourself out:

sudo sshd -t

If no output returns, your configuration syntax is correct. Keep your current SSH session open! Open a separate terminal window and restart the SSH daemon service:

sudo systemctl restart ssh

Test your modified access parameter on your alternative port from your local computer:

ssh -p 2222 -i ~/.ssh/id_ed25519 sysadmin@YOUR_SERVER_IP

Securing entry points is critical when running specialized backends like Parsing Inbound Support Tickets with OpenAI, where compromised host systems can lead to exposed credentials or leaked API tokens.


Step 3: Implementing Fail2ban and UFW Firewall Rules

With OpenSSH locked down, set up a network firewall alongside an automated intrusion prevention framework. Ubuntu includes ufw (Uncomplicated Firewall) out of the box.

First, define the default incoming and outgoing network policies:

sudo ufw default deny incoming
sudo ufw default allow outgoing

Allow your non-standard SSH port through the firewall prior to enabling it:

sudo ufw allow 2222/tcp comment 'Custom SSH Port'

If your host hosts additional public services like web servers, allow those ports as well:

sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

Enable the firewall and confirm its current operational status:

sudo ufw enable
sudo ufw status verbose

Next, install fail2ban to monitor system logs and drop IP addresses displaying malicious behavior:

sudo apt update
sudo apt install fail2ban -y

Fail2ban reads operational parameters from .local override files rather than default configuration files. Create a custom local jail definition:

sudo nano /etc/fail2ban/jail.local

Write the complete configuration file:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
bantime  = 1h
findtime = 10m
maxretry = 3
backend  = auto

[sshd]
enabled  = true
port     = 2222
logpath  = %(sshd_log)s
backend  = %(sshd_backend)s
maxretry = 3
findtime = 600
bantime  = 86400

Start and enable the Fail2ban service:

sudo systemctl start fail2ban
sudo systemctl enable fail2ban

Verify that Fail2ban is active and tracking your SSH daemon rules:

sudo fail2ban-client status sshd

This defense stack ensures that bad actors attempting port scans or brute force attempts are blocked at the kernel network boundary. Secure networking forms the base required for complex distributed architectures, such as Setting Up PostgreSQL Read Replicas with Docker.


Step 4: Two-Factor Authentication with Google Authenticator PAM

To build a multi-layered security model, configure Pluggable Authentication Modules (PAM) to demand a Time-based One-Time Password (TOTP) during authentication.

Install the Google Authenticator PAM module package on your server:

sudo apt install libpam-google-authenticator -y

Run the interactive setup initialization under your designated user account (sysadmin):

google-authenticator

Answer the interactive configuration prompts as follows:

Do you want authentication tokens to be time-based? (y/n) y

Scan the printed QR code using your preferred TOTP app (Google Authenticator, Authy, or 1Password) and copy down the emergency scratch codes in a secure location. Complete the setup responses:

Do you want me to update your "~/.google-authenticator" file? (y/n) y
Do you want to disallow multiple uses of the same authentication token? (y/n) y
By default, a new token is generated every 30 seconds... Increase window size? (y/n) n
Do you want to enable rate-limiting? (y/n) y

Next, configure PAM to evaluate TOTP rules during SSH logins. Open the PAM SSH configuration file:

sudo nano /etc/pam.d/sshd

Add the following module directive to the top of the file:

auth required pam_google_authenticator.so nullok

The nullok flag allows users who have not yet configured 2FA to log in without being blocked immediately. Remove nullok once all valid administrative accounts finish setting up TOTP devices.

Now update your OpenSSH drop-in file to request both public key and TOTP verification parameters:

sudo nano /etc/ssh/sshd_config.d/50-hardening.conf

Append the following two lines to the end of your file:

KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

Test your configuration for syntax errors:

sudo sshd -t

Restart the OpenSSH daemon service:

sudo systemctl restart ssh

Open a new local terminal window and attempt to establish a session:

ssh -p 2222 -i ~/.ssh/id_ed25519 sysadmin@YOUR_SERVER_IP

Your system will prompt you for your local SSH key passphrase, followed by a request for your 2FA verification code:

Authenticated with partial success.
Verification code:

Type the current 6-digit code from your authenticator app to complete the authentication sequence.


Getting Started

Securing your infrastructure begins with a clean environment on reliable cloud providers. Get started with these platforms:

Taking these four hardening steps locks down standard entry points, enforces strong cryptography, mitigates brute-force vectors with Fail2ban, and adds multi-factor protection to your cloud servers.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online