How to Secure a New Ubuntu VPS: A Setup Checklist for Business Websites
A step-by-step hardening checklist for a fresh Ubuntu 26.04 or 24.04 LTS VPS that will host a business website, with copy-paste commands for SSH keys, ufw, unattended-upgrades, fail2ban, time sync, swap, monitoring and backups.

To secure a fresh Ubuntu LTS VPS before it hosts a business website: create a non-root user with sudo, log in with SSH keys only, disable password and root login over SSH, turn on the ufw firewall with only SSH, HTTP and HTTPS open, confirm automatic security updates are running, and add brute-force protection such as fail2ban. Then check time sync, add swap and basic monitoring, and take a tested backup before go-live.
The commands below are written for Ubuntu 26.04 LTS and also work on Ubuntu 24.04 LTS, with differences noted where they matter. They assume a new server you have just received from a provider, with SSH access as root or as a default user such as ubuntu. Work through the sections in order; the order matters, because a mistake in SSH or the firewall can lock you out.
Before you change any SSH or firewall setting, keep a second SSH session open to the server, and do not close it until you have confirmed you can open a brand-new session with the new settings. If something goes wrong, the open session is how you fix it. Most providers also offer a web-based or "rescue" console; find it now, before you need it.
1. Update the system and create a sudo user
Log in as root (or the provider's default user) and bring the system up to date:
`bash
sudo apt update && sudo apt full-upgrade -y
sudo reboot # if a new kernel was installed
`
If /var/run/reboot-required exists after the upgrade, a reboot is needed for the new kernel or libraries to take effect.
Create a personal administrative user. Use a real name rather than admin, which is among the first usernames automated scanners try:
`bash
sudo adduser alex
sudo usermod -aG sudo alex
`
adduser asks for a password. Set a strong one: you will no longer use it to log in over SSH, but sudo will ask for it. On Ubuntu 26.04 the default sudo command is provided by sudo-rs, a Rust reimplementation, according to the Ubuntu 26.04 LTS release notes. Membership of the sudo group works the same way.
If the provider is running the site for several people, create one account per person. Shared accounts make it impossible to tell who did what, and impossible to remove one person's access cleanly.
2. Set up SSH key authentication
SSH keys replace passwords with a key pair: the private key stays on your computer and the public key goes on the server. Ubuntu's OpenSSH server documentation recommends Ed25519 keys.
On your own computer (not the server), create a key if you do not already have one, and protect it with a passphrase:
`bash
ssh-keygen -t ed25519 -C "alex@laptop"
`
Copy the public key to the new user on the server:
`bash
ssh-copy-id [email protected]
`
Replace the IP address with your server's. If your provider already put your key in root's authorized_keys and password login is disabled, ssh-copy-id cannot log in as the new user yet. In that case, copy it across on the server as root:
`bash
sudo mkdir -p /home/alex/.ssh
sudo cp /root/.ssh/authorized_keys /home/alex/.ssh/
sudo chown -R alex:alex /home/alex/.ssh
sudo chmod 700 /home/alex/.ssh
sudo chmod 600 /home/alex/.ssh/authorized_keys
`
Now, from a new terminal, confirm you can log in as the new user with the key, and that sudo works:
`bash
sudo whoami # should print: root
`
Do not continue until this works.
3. Disable password and root login over SSH
Ubuntu's /etc/ssh/sshd_config begins with Include /etc/ssh/sshd_config.d/*.conf, and for each setting sshd uses the first value it reads. That has a practical consequence: many cloud images ship a file such as /etc/ssh/sshd_config.d/50-cloud-init.conf that sets PasswordAuthentication yes, and it will override anything you write lower down in the main file. The reliable fix is a drop-in file whose name sorts first:
`bash
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers alex
EOF
`
Adjust AllowUsers to list every account that needs SSH access, separated by spaces, or remove that line if you would rather not restrict by name.
Check the configuration is valid, then confirm the effective values before restarting:
`bash
sudo sshd -t
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|allowusers)'
`
The second command should show permitrootlogin no, passwordauthentication no and so on. If it does not, another file is setting the value first; look in /etc/ssh/sshd_config.d/.
Apply the change:
`bash
sudo systemctl restart ssh
`
With your existing session still open, open a new terminal and log in again as your user. Also confirm the server now refuses password logins:
`bash
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]
# expected: Permission denied (publickey).
`
A note on changing the SSH port. Moving SSH off port 22 reduces log noise from automated scans but does not add real security, and it adds a way to lock yourself out. Since Ubuntu 22.10, OpenSSH uses systemd socket activation; on 24.04 and later the socket's port is generated from the Port setting in sshd_config. If you do change it, allow the new port in ufw first, run sudo systemctl daemon-reload and sudo systemctl restart ssh.socket, and test from a new session before removing the old rule. For most business sites, keys-only authentication on port 22 is the better trade-off.
4. Turn on the ufw firewall
ufw is Ubuntu's front end to the kernel firewall. Its manual page warns that enabling it flushes the firewall chains and can drop existing connections such as SSH, so allow SSH before you enable it:
`bash
sudo apt install ufw # usually already installed
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
`
OpenSSH is an application profile provided by the openssh-server package; sudo ufw app list shows the profiles available. Once Nginx is installed you can use sudo ufw allow 'Nginx Full' instead of the two port rules.
Open nothing else to the internet unless you have a specific reason. Databases (MySQL, PostgreSQL, Redis) and application servers (for example a Node.js app on port 3000) should listen on 127.0.0.1 and be reached through Nginx or an SSH tunnel, not opened in the firewall. Check what is listening:
`bash
sudo ss -tulpn
`
Anything bound to 0.0.0.0 or [::] other than SSH, HTTP and HTTPS deserves a second look, even with the firewall in place.
One known gotcha: Docker publishes container ports by writing its own firewall rules, which can bypass ufw. If you run Docker, bind published ports to 127.0.0.1 (for example -p 127.0.0.1:3000:3000).
Many VPS providers also offer a network firewall in their control panel. Using both is reasonable: the provider firewall blocks traffic before it reaches the server, and ufw protects you if the provider rules are ever changed.
5. Automatic security updates
According to Ubuntu's automatic updates documentation, the unattended-upgrades package is installed by default on Ubuntu Server and applies security updates automatically. Do not assume; check:
`bash
cat /etc/apt/apt.conf.d/20auto-upgrades
`
You should see:
`text
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
`
If the file is missing or the values are 0, enable it:
`bash
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
`
Test what it would do:
`bash
sudo unattended-upgrade -v --dry-run
`
Some updates, kernel updates in particular, only take effect after a reboot. You can let the server reboot itself at a quiet time by setting these in /etc/apt/apt.conf.d/50unattended-upgrades:
`text
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
`
Whether to allow automatic reboots is a business decision. For a single server running a shop, a few minutes of downtime at 4am is usually better than running an unpatched kernel for weeks, but make sure your application starts again on boot (enable its systemd service) before you turn this on. Logs are in /var/log/unattended-upgrades/.
6. Brute-force protection: fail2ban or alternatives
With password login disabled, SSH brute-force attempts cannot succeed, but they still fill logs and use resources. fail2ban watches logs and temporarily bans IP addresses that fail repeatedly.
`bash
sudo apt install fail2ban
`
On Ubuntu, the package ships /etc/fail2ban/jail.d/defaults-debian.conf, which enables the sshd jail and reads the systemd journal. Put your own overrides in /etc/fail2ban/jail.local rather than editing the shipped files:
`bash
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# Never ban your own office or VPN address (edit or remove):
# ignoreip = 127.0.0.1/8 ::1 198.51.100.25
[sshd]
enabled = true
EOF
sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
`
If fail2ban-client status sshd reports the jail with a filter and actions, it is working. If the service fails to start, check sudo journalctl -u fail2ban before relying on it.
Alternatives. If you would rather not run another daemon, ufw has built-in rate limiting: sudo ufw limit OpenSSH replaces the plain allow rule and, per the ufw manual, denies an IP address that attempts 6 or more connections within 30 seconds. A provider network firewall that only allows SSH from your office or VPN addresses is stronger than either. fail2ban can also protect web applications, such as WordPress login pages, with extra filters, but that is beyond a baseline setup.
7. Time sync, swap and basic monitoring
Time sync. Accurate time matters for TLS certificates, logs, two-factor codes and scheduled jobs. Check it:
`bash
timedatectl
`
Look for System clock synchronized: yes. On Ubuntu 26.04, chrony replaces systemd-timesyncd as the default time daemon and uses authenticated Network Time Security (NTS) with Ubuntu's time servers, per the release notes; chronyc tracking shows its status. On 24.04, systemd-timesyncd is the default. Setting the server timezone to UTC (sudo timedatectl set-timezone UTC) keeps logs easy to compare across systems.
Swap. Small VPS plans often have no swap, so a memory spike (a WordPress import, an npm build) kills processes outright. A modest swap file acts as a safety margin, not as extra RAM:
`bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h
`
Size it to your plan; 1 to 2 GB is typical for a small server. If a site regularly uses swap, the server needs more memory.
Monitoring basics. At a minimum:
- An external uptime check on the site's home page and checkout or contact page, alerting by email or chat. External checks catch problems the server cannot report, such as the server being down.
- Disk space alerts. Full disks are a common cause of outages.
df -hshows usage; most providers can alert on it. - A habit of reading logs after changes:
sudo journalctl -p err -blists errors since boot, andsystemctl --failedlists failed services. - Package and reboot status.
ls /var/run/reboot-requiredtells you if a reboot is pending.
8. Backups and the go-live checklist
Take backups before the site goes live, not after the first incident. A sound arrangement has two layers:
- 1Provider snapshots of the whole server, for fast rollback after a bad change.
- 2Off-server backups of the website files and database, stored with a different provider or account, so a compromised or deleted server does not take its backups with it.
The UK NCSC's small business guide on backing up data recommends keeping a backup separate from your main systems and making backups a routine. For a database, dump it rather than copying live files, for example mysqldump --single-transaction for MySQL or pg_dump for PostgreSQL, then copy the dump off the server. Most importantly, restore a backup to a test server at least once, so you know the process works and how long it takes. A maintenance routine that covers this is described in the website maintenance guide for small business owners.
Before pointing DNS at the server, run through this list:
| Check | Command or action | Expected result | ||
|---|---|---|---|---|
| System up to date | sudo apt update && apt list --upgradable | Nothing important pending | ||
| Named sudo user works | ssh alex@server, then sudo whoami | root | ||
| Root SSH login refused | ssh root@server | Permission denied | ||
| Password SSH login refused | ssh -o PubkeyAuthentication=no alex@server | Permission denied | ||
| Effective SSH settings | `sudo sshd -T \ | grep -Ei 'permitroot\ | passwordauth'` | Both no |
| Firewall active | sudo ufw status verbose | Deny incoming; only SSH, 80, 443 allowed | ||
| Nothing unexpected listening | sudo ss -tulpn | Databases and app servers on 127.0.0.1 only | ||
| Security updates automatic | cat /etc/apt/apt.conf.d/20auto-upgrades | Both values "1" | ||
| Brute-force protection | sudo fail2ban-client status sshd or sudo ufw status shows LIMIT | Jail active, or rate limit in place | ||
| Clock synchronised | timedatectl | System clock synchronized: yes | ||
| Swap present | free -h | Swap total above 0 | ||
| Uptime monitoring | External check configured | Alert received on a test | ||
| Backups | Snapshot plus off-server backup taken | Test restore completed | ||
| Provider console access | Log in to the provider's web console | You can reach a login prompt |
This baseline is a starting point, not the finish line. Next come the web server, TLS certificates and the application itself; if you are deploying a Node.js or Next.js app, the next step is putting it behind Nginx with a process manager. Keep a written record of what you changed and why, so that the next person who logs in, or you in six months, can tell intentional configuration from accidents. Treat changes to the server like changes to the site, tested first and released deliberately, as described in the staging vs production deployment workflow.
If you would rather not own server patching, backups and monitoring yourself, website maintenance covers that work on an ongoing basis, from the first hardening pass through to monthly updates and restore tests.
Related posts

Shared vs VPS vs Managed Hosting for a Small Business Website or Store
A plain comparison of shared hosting, VPS and managed hosting or PaaS for small business sites and online stores: responsibilities, isolation and performance, the signs a WooCommerce store or Next.js app has outgrown shared hosting, GDPR data residency, and a decision table.
Read article →

Deploy a Next.js 16 App on a VPS with Nginx, systemd or PM2, and HTTPS
A working guide to running Next.js 16 on your own VPS: Node.js LTS, build-time versus runtime environment variables, a systemd unit and PM2 alternative, an Nginx server block with certbot HTTPS, the standalone output option, logs, and a two-port release script.
Read article →

Business Email Compromise and Invoice Fraud: How Small Businesses Stop It
A defensive guide to business email compromise and invoice fraud for small businesses: how the scams work at a high level, the red flags, the payment and email controls that stop them, and what to do in the first hours after a fraudulent transfer in the US, UK and EU.
Read article →
Author
Anushka Dahanayake
Anushka Dahanayake builds SEO-focused websites, e-commerce platforms, dashboards, and automation systems for businesses worldwide.
