Technology

VPS Hardening: SSH, Fail2ban and Security Updates

If you are running a VPS, SSH is usually the main door into your server. That also makes it a common target for bots and attackers scanning the internet. A default install might work for quick tests, but for anything that handles real users, payments or internal data, you need a deliberate hardening checklist. In this guide, we will walk through a Linux VPS hardening checklist: tightening sshd_config, configuring Fail2ban, enabling automatic security updates, and disabling direct root login – with checks to reduce the risk of locking yourself out.

The goal is not to turn you into a full-time security engineer. Instead, we will give you a practical, repeatable process you can apply on every new VPS or dedicated server. We will explain the “why” of each setting in simple terms, and then show you the exact configuration snippets. Document the steps you apply, then adapt and validate the checklist for each environment – whether you are hosting on a single VPS, multiple VPSs, or even colocated servers in a data center.

A Practical VPS Security Hardening Checklist

Before we dive into file edits and commands, it helps to see the big picture. For most Linux VPS setups, a minimal but strong SSH-focused hardening checklist looks like this:

  • Create a non-root user with sudo access.
  • Harden the SSH service via /etc/ssh/sshd_config.
  • Install and configure Fail2ban to block brute-force attempts.
  • Enable automatic security updates for the OS.
  • Disable direct root SSH logins once everything is tested.
  • Add a firewall and monitoring on top (we will point to deeper guides for these).

The examples cover Ubuntu 24.04 LTS, Debian 12 and the RHEL 9 family. Use current supported distribution packages and validate local configuration. Run server administration in a root Bash session; run key-generation commands on your own client. Choose only your distribution’s branch. Verify a working independent console/rescue path before access changes.

1. Create a Non-Root Sudo User

Hardening SSH properly is much easier when you stop logging in as root directly. Root has unlimited powers; a single mistake or stolen credential can be catastrophic. A safer pattern is:

  • Log in as a regular user.
  • Use sudo to run administrative commands when needed.

Step 1: Create the user

Create a new local administrator in your existing root session. The examples use admin; if that account already exists, inspect it rather than recreating it. Choose the appropriate distribution branch:

Ubuntu/Debian:

adduser admin &&
usermod -aG sudo admin

RHEL/AlmaLinux/Rocky 9:

useradd -m -U -s /bin/bash admin &&
passwd admin &&
usermod -aG wheel admin

Set a strong local password where your sudo/account policy requires it. This does not establish a working SSH password fallback. Confirm account status, home directory and login shell before relying on the user.

Step 2: Verify Sudo Access

On many distributions, members of the sudo or wheel group can run administrative commands via sudo:

# Inspect the real account and active sudoers policy.
id admin
visudo -c

Test that sudo works:

su - admin
sudo -k
sudo -v
sudo id
sudo -l
exit

Run those steps interactively in the admin shell, then return to the original root shell with exit. Depending on policy, sudo may request the admin password; successful sudo id reports UID 0. Verify the active sudo/wheel rule, not just group membership. Repeat the sudo test in a fresh key-authenticated SSH session before disabling root/password SSH. A broad sudo grant remains administrative power; NOPASSWD is not itself a security improvement.

Step 3: Set up SSH keys for the new user

Create a protected personal key on your client, preserving any existing key file. Ed25519 is a useful option where policy permits; it is not available in RHEL FIPS mode. Use a supported RSA/SHA-2 key/policy when required, rather than weakening the system crypto policy. Verify the server host-key fingerprint through a trusted independent channel.

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C 'admin-laptop' &&
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 admin@SERVER_ADDRESS

For manual installation, use the root session on the server and the actual admin home path. Append only the public key and preserve existing authorized_keys entries:

install -d -m 700 -o admin -g admin /home/admin/.ssh &&
touch /home/admin/.ssh/authorized_keys &&
chown admin:admin /home/admin/.ssh/authorized_keys &&
chmod 600 /home/admin/.ssh/authorized_keys &&
# Open this file and append only the public key; preserve existing entries.
vi /home/admin/.ssh/authorized_keys

On SELinux systems, restore the appropriate labels with restorecon -R /home/admin/.ssh, using the actual path. Keep the private key on the client. Test from a separate terminal:

ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 -p 22 admin@SERVER_ADDRESS

If you plan to share access with teammates, we strongly recommend setting up a structured key workflow instead of sharing one key. You can find a detailed, step‑by‑step process in our guide on SSH key management and access sharing on a VPS.

2. Harden SSH with sshd_config

Verify both a new key-authenticated admin session and sudo before access restrictions. Keep your original session open and confirm a working independent recovery console. Inspect the active service/socket mode, configuration-management owner, Include paths and Match blocks; an open session alone is not a recovery guarantee.

Step 1: Back Up the Active SSH Configuration

umask 077 &&
backup_dir=$(mktemp -d /root/ssh-change.XXXXXX) &&
cp -a /etc/ssh "$backup_dir/ssh" &&
printf 'Private configuration backup: %s\n' "$backup_dir"

Inventory and separately back up active Include files outside /etc/ssh and service/socket drop-ins you will change. Record any newly created files. Rollback must restore the original files and remove only overrides introduced by this change; copying the old main file over a newly active drop-in is insufficient. Do not replace the full configuration with a small policy example.

Step 2: Optional – Change the SSH port

Changing the port can reduce attempts aimed only at port 22; scanning can still discover the service. It is optional. During migration, keep the existing port and new port together until a new external key-login and sudo test succeeds:

Port 22
Port 2222

Remember to:

  • Open the new port for actual management sources in provider and host firewalls, covering IPv4/IPv6. Keep the old allowance until external tests pass.
  • On RHEL 9, inspect semanage port -l. If 2222 is unassigned, add semanage port -a -t ssh_port_t -p tcp 2222; semanage comes from policycoreutils-python-utils. Do not blindly reassign another service’s label or disable SELinux.
  • Inspect systemctl status ssh.service ssh.socket on Ubuntu/Debian or systemctl status sshd.service on RHEL; check the real listener using ss -ltnp.
  • Validate syntax and effective policy, then apply through the correct service/socket path. Test ssh -p 2222 admin@SERVER_ADDRESS and sudo before removing the old port. Use both ports in Fail2ban during migration.

Ubuntu 24.04 Noble uses a socket generator when socket activation is enabled. Its package README documents this port/address update sequence; Ubuntu 22.10–23.10 migrated socket drop-ins differ. For daemon mode, use the later service branch instead:

sshd -t &&
systemctl daemon-reload &&
systemctl restart ssh.socket &&
systemctl cat ssh.socket &&
ss -ltnp

Step 3: Use Current Protocol and Crypto Defaults

Modern OpenSSH uses protocol 2; the old Protocol directive is not a useful hardening control and can be ignored. Preserve supported distribution crypto defaults instead of copying an outdated algorithm allowlist.

OpenSSH 8.5 renamed PubkeyAcceptedKeyTypes to PubkeyAcceptedAlgorithms; the old name remains an alias. The list controls signatures. ssh-rsa means RSA/SHA-1 here, not every RSA key. OpenSSH 8.8 disables that signature by default; compatible RSA keys can use RSA/SHA-2. Do not replace modern defaults with ssh-ed25519,ssh-rsa.

Step 4: Restrict which users can log in via SSH

Manage one intentional user allowlist in the correct global scope before any Match block; do not append blindly at the end. Most scalar settings use the first obtained value, while AllowUsers/AllowGroups can accumulate. Include files are read where encountered, with glob expansion in lexical order. Inspect existing rules and verify effective policy:

AllowUsers admin

If multiple existing accounts need access, edit that same list to include only their tested names. Do not stack this as another unexplained alternative:

AllowUsers admin deploy backup

A group allowlist is an alternative: AllowGroups sshusers after creating the group and verifying memberships with id. When both user and group allowlists apply, both conditions must pass, along with applicable deny rules. An existing Match block may select a different policy.

Step 5: Disable password-based SSH authentication

This example requires public-key authentication only. Confirm new admin key login and sudo first. If you intentionally use PAM-based MFA, design that method sequence instead of overwriting it with this key-only policy:

PubkeyAuthentication yes
AuthenticationMethods publickey
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2

A no-password setting alone does not prove the intended authentication policy. PAM account/session checks remain relevant, as do account status, key permissions and Match rules. The base policy includes PermitRootLogin no, so complete root-removal prerequisites before applying it.

Step 6: Verify the Root Login Restriction

The base policy above already includes this active restriction. It is not an inactive placeholder. Confirm the non-root key/sudo and console checks before any reload or socket restart:

PermitRootLogin no

Again, do not reload or restart SSH until you have tested your non-root user’s SSH key login and sudo access.

Step 7: Other useful hardening options

Treat numeric values as policy examples to test with your clients. MaxAuthTries is per connection; LoginGraceTime limits time to authenticate. ClientAlive settings probe an unresponsive client, not human inactivity. A responsive client may remain connected while its user is idle. The base policy’s relevant values are:

MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2

Step 8: Validate and reload SSH

Inspect Include ordering and Match scope before testing. Place intended global options in the correct global location; a new filename or a last line is not proof of precedence. Check every affected real user/source/address/port context. Replace these documentation addresses and hostname with the values sshd actually sees:

sshd -t &&
sshd -T -C user=admin,host=client.example,addr=198.51.100.10,laddr=203.0.113.10,lport=22 &&
sshd -T -C user=root,host=client.example,addr=198.51.100.10,laddr=203.0.113.10,lport=22

A syntax test succeeds only with exit status 0. The effective output should show publickey as the required method, disabled password/keyboard-interactive methods, the intended root policy and allowlists. This still does not prove key/PAM/group access, firewall reachability or the listener. For authentication-policy changes in the normal service path, choose one branch:

Ubuntu/Debian:

sshd -t &&
systemctl reload ssh.service

RHEL 9:

sshd -t &&
systemctl reload sshd.service

Test fresh external admin key login and sudo, plus rejection of root and unauthorized accounts/sources, before closing old sessions or removing the old port. Use the socket-specific branch for socket address/port changes. If verification fails, stop; use the confirmed console to restore recorded files and remove only new overrides, validate syntax again and apply the correct unit path. A running old session does not prove new access works.

If you want a broader, layered view of SSH and OS hardening, we also recommend our article The Calm, No‑Drama Guide: How to Secure a VPS Server, where we connect SSH security to firewalls, backups and monitoring.

3. Limit Brute‑Force Attempts with Fail2ban

Even with good sshd_config settings, bots will still try to connect and guess credentials. Fail2ban reads your logs and automatically bans IP addresses that show suspicious patterns, such as many failed SSH logins in a short period.

Step 1: Install Fail2ban

Ubuntu/Debian with the journal backend:

apt update &&
apt install fail2ban python3-systemd

Registered RHEL 9, with CodeReady Builder access:

subscription-manager repos --enable "codeready-builder-for-rhel-9-$(arch)-rpms" &&
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm &&
dnf install fail2ban python3-systemd

AlmaLinux/Rocky 9-compatible systems with their CRB repository:

dnf install dnf-plugins-core &&
dnf config-manager --set-enabled crb &&
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm &&
dnf install fail2ban python3-systemd

Configure and validate the jail before enabling it. Package installation may apply distribution defaults; inspect those rather than assuming an empty policy. The startup commands after the configuration section perform the explicit validation.

fail2ban-client -t

Step 2: Create a local configuration

Put local settings in /etc/fail2ban/jail.d/sshd.local rather than copying the whole package jail.conf. Keep package updates available and review existing local overrides.

vi /etc/fail2ban/jail.d/sshd.local

This example uses journald and an installed nftables-multiport action. Choose a supported action for the actual firewall manager and verify IPv4/IPv6 enforcement. Do not combine a systemd backend with a file logpath:

[sshd]
enabled = true
backend = systemd
port = 22
filter = sshd
banaction = nftables-multiport
maxretry = 5
findtime = 600
bantime = 3600

For a real file log instead, replace the backend in the same jail with polling and its actual logpath. /var/log/auth.log or /var/log/secure exists only if the logging stack produces it. The journal backend uses the filter’s journalmatch:

[sshd]
backend = polling
logpath = /var/log/auth.log
journalmatch =

Step 3: Whitelist your office or VPN IPs

Only exempt actual trusted stable management/VPN addresses as seen by the server. Exemptions bypass bans; do not copy documentation or broad private ranges into production as a shortcut. The minimal baseline is:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1

Add only verified addresses, accounting for NAT/VPN and shared networks. During SSH port migration use the actual port list, such as 22,2222, then remove the old port only after successful testing.

Step 4: Restart Fail2ban and verify jails

fail2ban-client -t &&
systemctl enable fail2ban &&
systemctl restart fail2ban &&
fail2ban-client status sshd

An active jail is not proof that it reads the intended events or that bans affect traffic. Test captured failure/success log lines with fail2ban-regex, then use a controlled separate source with console recovery available to verify a real ban and unban. For a false positive use fail2ban-client set sshd unbanip ACTUAL_IP and fix the cause.

Step 5: Tune ban policies over time

Fail2ban is not a “set it and forget it” tool. In the first days, watch your logs and jails:

  • The example threshold is 5 matching failures within 600 seconds, with a 3600-second ban; tune from real events and false positives.
  • Review source addresses, filter matches and action results before adding broad blocks. Distributed sources and valid stolen credentials may bypass this defense.
  • Where supported, bantime.increment with an explicit bantime.maxtime can increase repeat bans; it is not automatically enabled or a universal recommendation.

Fail2ban can protect more than just SSH, such as web panel logins or web applications. For example, in another article we show how to combine Nginx rate limiting and Fail2ban to protect WordPress logins from bots. If that is relevant to you, check out our guide on stopping wp-login.php and XML‑RPC brute‑force attacks with Fail2ban.

4. Enable Automatic Security Updates

SSH and Fail2ban reduce the chance of someone getting in, but you also need to keep the underlying operating system patched. Many critical vulnerabilities (kernel, OpenSSH, glibc, etc.) are fixed via security updates. If you forget to apply them, your server may remain vulnerable for months.

There are two main patterns:

  • Automatic security updates for OS packages.
  • Manual or controlled updates for application-level changes (e.g., major PHP or database upgrades).

Automatic security updates on Ubuntu/Debian

On Ubuntu, installing unattended-upgrades can enable automatic updates by default. Before installation, review the distribution’s default policy and existing APT timers as part of your maintenance plan. Then install the package and verify the effective source and package selection:

apt update &&
apt install unattended-upgrades apt-listchanges

Review the packaged and effective policy; do not assume periodic installation is still disabled after the package is installed. Ubuntu 24.04 and Debian 12 have different security repository metadata; preserve and validate their own source rules:

apt-config dump
apt-cache policy
unattended-upgrade --dry-run --debug
systemctl list-timers 'apt-daily*'

50unattended-upgrades normally controls allowed origins/package and reporting policy; 20auto-upgrades normally controls periodic activation. The reconfiguration wizard does not demonstrate correct origins. Verify the merged APT configuration and dry-run allowed origins; do not use a shared Ubuntu-Security label recipe for Debian and Ubuntu. These are configuration file contents, not shell commands:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Unattended-Upgrade::Automatic-Reboot "false";

After reviewing the intended policy, manage periodic activation with:

dpkg-reconfigure --priority=low unattended-upgrades

You can also configure:

  • Test email delivery if using update notifications.
  • Review unused-dependency removal against actual applications.
  • The sample disables automatic reboot; monitor reboot requirements and schedule them. An opt-in Automatic-Reboot-Time is not a substitute for a recovery plan, active-session handling and post-reboot checks.

Automatic security updates on AlmaLinux / Rocky / RHEL

For the DNF 4-based RHEL/AlmaLinux/Rocky 9 branch, use dnf-automatic. Other major releases, including DNF 5 systems, need their own package/timer instructions:

dnf install dnf-automatic

Edit the configuration file:

nano /etc/dnf/automatic.conf

Key options to review:

[commands]
upgrade_type = security
apply_updates = yes
random_sleep = 0

[emitters]
emit_via = stdio

The standard dnf-automatic.timer follows this configuration. Other install/download/notify timers can override the operation; avoid scheduling multiple competing variants:

systemctl enable --now dnf-automatic.timer &&
systemctl list-timers 'dnf-automatic*' &&
systemctl cat dnf-automatic.timer

Inspect the scheduled service logs:

journalctl -u dnf-automatic.service

Balancing automation and stability

Choose an automation policy from workload requirements and available recovery. Security metadata is required for security-only filtering, and OS security updates may include web servers, databases or runtimes; they do not automatically exclude your application stack. Timer-level random delays can remain even with random_sleep=0.

  • Inspect the actual package/source selection and stage critical changes.
  • Plan service restarts and reboots as needed, with a tested rollback or rebuild path.
  • Check application transactions, SSH, database/queue health and logs, not only an HTTP 200 response.
  • Monitor update failure, missing scheduled runs and required reboot. For email reporting use the documented [emitters]/[email] settings and test actual delivery.

If you are still building your backup strategy, our guide on how to apply the 3‑2‑1 backup strategy on your VPS shows how to automate reliable, off‑site backups.

5. Safely Disable Direct Root Login

Once new admin key access, sudo and console recovery are verified, enforce the intended root restriction. This removes one direct administrative login path; it does not remove every way to obtain root.

Why disabling root login matters

Root is a known username. Attackers know it exists and will try to guess its password. Even if you are using keys, disabling direct root login gives you:

  • Named administrator accounts separate entry identity from privileged work.
  • Sudo can log privileged command invocations; a root shell’s internal commands are not automatically all separate audit events.
  • Administrative credentials and sudo policy still need protection; disabling direct root SSH alone does not prevent account compromise or destructive commands.

Step 1: Confirm your sudo user works perfectly

Before touching root login, double-check from your local machine:

ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 -p 22 admin@SERVER_ADDRESS
# In the new server session:
sudo -k
sudo -v
sudo id
sudo -l

You should be able to use sudo without problems. If this fails, fix it now – do not proceed.

Step 2: Edit sshd_config to disable root login

Open the SSH configuration:

sudo nano /etc/ssh/sshd_config

Find the PermitRootLogin line and set it to:

PermitRootLogin no

Inspect all active Include and Match contexts and their precedence, not just a line count in one file. Commented lines are inactive; repeated active scalar settings can leave an earlier value effective, and matching conditional rules can change the result. Verify root and admin contexts with the earlier -T -C procedure.

Step 3: Test configuration and reload

Repeat the earlier syntax/effective-policy checks and choose the correct service branch; the socket port-change sequence is separate:

sshd -t &&
sshd -T -C user=admin,host=client.example,addr=198.51.100.10,laddr=203.0.113.10,lport=22 &&
sshd -T -C user=root,host=client.example,addr=198.51.100.10,laddr=203.0.113.10,lport=22

For Ubuntu/Debian service policy changes:

sshd -t &&
systemctl reload ssh.service

For RHEL 9:

sshd -t &&
systemctl reload sshd.service

Now, try logging in as root from a new terminal:

ssh -p 22 root@SERVER_ADDRESS

New root SSH access should be rejected while new admin key login and sudo still work. Use the actual final port in both tests. Existing sessions, console access and other privilege mechanisms are separate; confirm the effective policy as well as the connection result.

Step 4: Adjust your operational habits

After disabling root SSH, make sure your operational workflows follow these principles:

  • Use personal accounts/keys and a documented removal/rotation process.
  • Review sudo and SSH events in the configured journal/auth.log/secure pipeline; protect log retention and access.
  • Update automation to use tested non-root identities and narrowly appropriate sudo permissions; validate backup/deploy workflows before removing their old access.

For further access design, our FIDO2 keys and SSH CA guide discusses hardware-backed keys and certificate workflows. Choose supported policy for your clients and recovery requirements.

6. Extra Layers: Firewall, Monitoring and Infrastructure Choices

SSH policy, log-based bans, controlled updates and named administrative access are complementary layers. Add a verified firewall and monitoring plan rather than treating completion of a checklist as a guarantee.

Add a host-based firewall

A firewall enforces which ports on your VPS are reachable from the internet. Even if you have cloud-level security, we strongly recommend a host firewall as a second layer:

  • UFW for simple, human-friendly rules on Ubuntu/Debian.
  • firewalld on many RHEL-based systems.
  • iptables or nftables for advanced setups.

At minimum:

  • Allow the actual SSH port only from intended management sources where feasible.
  • Expose HTTP/HTTPS only when needed; inventory other real services.
  • Verify IPv4/IPv6, provider and host policy, required network control traffic and container/forwarded paths; do not use a blanket deny recipe without that scope.

For a full tutorial with examples, see our guide on how to configure a firewall on your VPS with ufw, firewalld or iptables.

Set up basic monitoring and alerts

Security is not just about preventing incidents; it is also about detecting them quickly. Monitoring helps you notice:

  • SSH login anomalies (sudden logins from unusual IPs).
  • Resource spikes that may indicate abuse or malware.
  • Services going down after an automatic update.

We like combining metrics, logs and uptime checks. A good starting point is our article on how to set up VPS monitoring and alerts with Prometheus, Grafana and Uptime Kuma.

Choose infrastructure that supports security

Infrastructure choices affect recovery and isolation. For DCHost or any other provider, verify the actual service contract and controls:

  • Supported OS images, security update lifecycle and responsibility for administration.
  • Working console/rescue access, provider network controls and their interaction with host policy.
  • Backup/restore capability and isolation requirements; a dedicated or colocated system still needs its own administration and recovery plan.

When expanding to more nodes, a dedicated server or colocation, adapt access identities, network policy and recovery for each environment. Reusing the checklist does not by itself validate those differences.

Putting It All Together: Your Reusable VPS Hardening Checklist

Security hardening does not have to be a one‑off, stressful project. Once you convert it into a checklist, you can apply it calmly to every new VPS, dedicated server or environment you deploy. Here is a concise version of the process we covered:

  • Verify console recovery and record current configuration/rollback files.
  • Create the administrator, personal key and working sudo path; test a new external session.
  • Review Include/Match and effective policy before applying the correct service/socket branch; test allowed and rejected access.
  • Verify Fail2ban events and actual firewall action rather than jail status alone.
  • Validate update sources, package selection, maintenance/reboot and alert delivery.
  • Document and rehearse backup/recovery, logging and monitoring.

Record the exact distribution, package version, policy and test results in your runbook. Recheck after image upgrades or configuration-management changes. For adjacent topics, browse the DCHost English blog index and our VPS security guide.

Start with the controls you can verify and maintain for this workload. New successful SSH/sudo sessions, deliberate rejection tests, working bans, timely updates and tested recovery provide concrete evidence that the hardening plan behaves as intended.

Official Sources

Frequently Asked Questions

Disabling direct root SSH removes one administrative login path and supports named entry accounts. Verify a new admin key login, usable sudo and independent console recovery before applying PermitRootLogin no. It does not prevent compromise of a sudo-capable account or remove other root privilege paths. Sudo can log command invocations, but commands inside an interactive root shell are not automatically separate audit events.

No. A different port may reduce attempts aimed only at port 22, but a scan can still find SSH. If you migrate, keep the old and new ports until new external key login and sudo tests pass. Update provider and host firewalls, IPv4/IPv6 scope, SELinux labels where applicable and Fail2ban ports. Ubuntu 24.04 socket activation requires its documented socket-generator procedure rather than assuming a service reload updates the listener.

The starting example is maxretry 5, findtime 600 seconds and bantime 3600 seconds. A ban may be triggered by five matching failures within that window. Tune from actual events and false positives; verify the correct log backend, filter and firewall action. Exempt only trusted stable management sources and test a real ban/unban from a controlled source with console recovery available.

No single automation policy suits every workload. Validate the actual allowed repositories, selected packages, advisory metadata and timer behavior. Security updates can include application runtimes, databases and web servers; security-only filtering does not automatically stage those separately. Test critical changes, monitor failures and reboot requirements, and maintain a proven recovery path. This guide disables unattended automatic reboot by default and leaves maintenance decisions explicit.
↑