Initial Ubuntu Server Setup: Users, SSH, and a Firewall

Before Nginx, before Magento, before anything else goes on a server — the server itself needs to be locked down. A fresh Ubuntu box from most providers boots with a single root account, password authentication over SSH, and no firewall. That’s fine for the first few minutes after it boots, and a bad idea for every minute after that: the moment a server has a public IP, automated bots start trying root/password combinations against it within minutes.

This is the exact setup we run on every server before installing anything user-facing on it — the same process we followed before setting up Nginx on our own production box. It’s written so you can follow it top to bottom on a brand-new server with no prior hardening experience; each step explains what it does and why, and includes how to check it actually worked before moving on.

What you’ll need

  • A fresh server running Ubuntu 24.04 or 26.04 LTS (26.04 “Resolute Raccoon” is the current release; 24.04 is still fully supported and this guide applies equally to both), with a public IP address
  • The server’s root password or an initial SSH key, from whoever provisioned it (your hosting provider’s dashboard usually shows this once, right after the server is created)
  • A terminal on your own computer — Terminal on macOS/Linux, or Windows Terminal / PowerShell / WSL on Windows all work fine, since they all ship a modern SSH client
  • About 30–45 minutes, and access to your hosting provider’s web console as a fallback (explained in Troubleshooting below) in case an SSH change locks you out

You do not need an SSH key pair already generated — Step 3 covers creating one if you don’t have one yet.

Step 1: Connect to the Server

From your own terminal, connect as root using the IP address your provider gave you:

ssh root@your-server-ip

The first time you connect to a given server, SSH will show a fingerprint and ask whether to continue connecting. This is normal for a first connection — type yes and press Enter. You’ll then be asked for the root password (or, if your provider set you up with a key already, you’ll be logged in directly).

If the connection is refused or times out entirely, double-check the IP address in your provider’s dashboard and confirm the server has finished booting — a server can take a minute or two after creation before SSH is actually listening.

Step 2: Update the System

Before changing any configuration, bring the package list and installed packages up to date:

apt update && apt upgrade -y

apt update refreshes the list of available package versions; apt upgrade -y actually installs any newer versions, answering “yes” automatically so it doesn’t stop and wait for input. This can take a few minutes on a brand-new server. If it finishes by telling you a reboot is required (you’ll see a message about it, or a file at /var/run/reboot-required), reboot before continuing:

reboot

Give it a minute, then reconnect with the same ssh root@your-server-ip command as before.

Step 3: Create a Non-Root User

Working as root day to day is how one typo or one compromised session takes down an entire server. Create a regular user and grant it admin rights through sudo instead, so day-to-day work happens as a normal account that has to explicitly opt into elevated commands:

adduser deploy

Replace deploy with whatever username you actually want to use — it’ll be what you type before @your-server-ip from now on. adduser will ask you to set and confirm a password, then prompt for full name, room number, and so on — all of that is optional; you can press Enter through each one, then Y to confirm at the end.

Now add that user to the sudo group, which is what grants permission to run commands as root via sudo:

usermod -aG sudo deploy

Verify the group actually took:

groups deploy

The output should list sudo alongside deploy. If it doesn’t, re-run the usermod command — a typo in the username is the usual cause.

Step 4: Set Up SSH Key Authentication

Passwords can be guessed or brute-forced; SSH keys can’t be, practically speaking. This step gets your new user logging in with a key instead of a password, which is what makes it safe to disable password login entirely in Step 6.

If you don’t already have an SSH key pair on the computer you’re connecting from, generate one there — not on the server. Open a new terminal window on your own machine (leave the server session open) and run:

ssh-keygen -t ed25519 -C "[email protected]"

Press Enter to accept the default file location, and optionally set a passphrase (recommended, but not required). This creates two files: id_ed25519 (your private key — never share this) and id_ed25519.pub (your public key — safe to share, this is what goes on servers).

Now copy your public key to the server, still from your own machine:

ssh-copy-id deploy@your-server-ip

It’ll ask for the deploy user’s password (the one you set in Step 3) one last time, then install your public key for that user automatically.

If ssh-copy-id isn’t available (it’s missing on some minimal setups, including stock Windows), do the same thing manually. On your own machine, print your public key:

cat ~/.ssh/id_ed25519.pub

Copy the single line it prints. Then, in your existing root session on the server, run:

mkdir -p /home/deploy/.ssh
echo "paste-your-public-key-here" >> /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Those exact permissions matter — SSH silently refuses to use a key if the .ssh directory or authorized_keys file is writable by anyone other than the owner.

Step 5: Test the New Login Before Changing Anything Else

This is the step it’s tempting to skip and the one that saves you from getting locked out. Keep your current root session open, and in a brand-new terminal window, connect as your new user:

ssh deploy@your-server-ip

You should land in a shell without being asked for a password (only your key’s passphrase, if you set one — that’s checked locally on your own machine, not by the server). Then confirm sudo actually works:

sudo whoami

It’ll ask for the deploy user’s password (not root’s) and should print root. If either of those doesn’t work, do not close your original root session — go back and re-check Steps 3 and 4 using that still-open root connection as your way in.

Step 6: Lock Down SSH Itself

With key-based login for your new user confirmed working, disable the two things that account for almost all automated SSH attacks: logging in as root directly, and logging in with a password at all. Edit the SSH daemon’s config (using either your root session or your new sudo user with sudo nano):

nano /etc/ssh/sshd_config

Find these two settings (they may already exist commented out with a #, or not exist at all) and set them exactly like this:

PermitRootLogin no
PasswordAuthentication no

Save and exit (in nano: Ctrl+O, Enter, then Ctrl+X). Before restarting SSH, check the config file for syntax errors — a typo here can lock out every route into the server:

sshd -t

No output means the syntax is valid. If it prints an error, it’ll name the line number — go back into the file and fix it before continuing. Once it’s clean, apply the change:

systemctl restart ssh

Don’t close your current sessions yet. Open one more new terminal window and test a fresh connection:

ssh deploy@your-server-ip

That should still work exactly as before. Then confirm root login is actually blocked:

ssh root@your-server-ip

This should now be refused (Permission denied) rather than prompting for a password. Once both checks pass, it’s safe to close your original root session — from here on, the server is only reachable as your non-root user, with a key.

Step 7: Set Up a Basic Firewall

Ubuntu ships with ufw (Uncomplicated Firewall) pre-installed — it just isn’t turned on by default. Enabling it blocks every incoming port except the ones you explicitly allow. Allow SSH before turning it on, or you’ll cut off your own access:

sudo ufw allow OpenSSH
sudo ufw enable

It’ll warn that this may disrupt existing SSH connections — type y to continue; the rule you just added covers this. Confirm the firewall is active and only SSH is open:

sudo ufw status verbose

You should see Status: active and an OpenSSH rule allowing incoming connections, with default policies of deny (incoming) and allow (outgoing). Everything else stays closed until you open it deliberately — for example, once a web server is installed, you’d run sudo ufw allow 'Nginx Full' to open ports 80 and 443.

Step 8: Install fail2ban

fail2ban watches log files for repeated failed login attempts and temporarily bans the source IP at the firewall level — a second layer that catches the (much smaller) set of attempts that get past key-only SSH, and slows down anything trying to brute-force other services later.

sudo apt install fail2ban -y
sudo systemctl enable --now fail2ban

Confirm it’s running and watching SSH:

sudo fail2ban-client status
sudo fail2ban-client status sshd

The default configuration (a few failed attempts within 10 minutes triggers a 10-minute ban) is reasonable to leave as-is. If you ever ban your own IP by mistake — typing your own password wrong a few times counts — unban it with:

sudo fail2ban-client set sshd unbanip your-ip-address

Custom settings (ban duration, retry limits, which services to watch) go in /etc/fail2ban/jail.local, which you create yourself rather than editing the default config directly — that keeps your changes intact across package updates. Worth revisiting once other services, like a web server, are running.

Step 9: Turn On Automatic Security Updates

So security patches land even on the days nobody’s watching:

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

Choose “Yes” when the prompt appears. This installs security patches automatically as they’re released, without touching full package version upgrades — it won’t surprise you with a major version bump of something you’re relying on. You can check it’s active with:

cat /etc/apt/apt.conf.d/20auto-upgrades

Both lines in that file should end in "1".

Step 10: Set the Hostname and Timezone

Small, but it makes logs, cron output, and certificate/log timestamps far easier to reason about later — especially once you’re comparing timestamps across a server, an application, and your own machine:

sudo hostnamectl set-hostname your-server-name
sudo timedatectl set-timezone UTC

UTC is the sane default for a server regardless of where you or your users are — it sidesteps daylight saving shifts entirely and keeps log timestamps unambiguous. Confirm both took effect:

hostnamectl
timedatectl

Optional: Add Swap Space

If this is a smaller server (1–2GB of RAM is common on entry-level plans), a swap file gives the kernel somewhere to fall back to under memory pressure instead of killing processes outright. Check whether you already have any:

sudo swapon --show
free -h

If that comes back empty, create a 2GB swap file:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Make it permanent so it survives a reboot, by adding it to /etc/fstab:

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Skip this step entirely on a server with plenty of RAM already — swap on an already memory-rich box mostly just masks problems you’d rather notice.

Optional: Change the Default SSH Port

Automated bots scan the entire internet for port 22 constantly — moving SSH to a different port doesn’t make the server more secure against a targeted attack (a port scan finds it in seconds), but it does cut out almost all of that background noise from your logs, which makes anything worth actually looking at easier to spot. It’s a convenience, not a substitute for the steps above.

If you want it, edit /etc/ssh/sshd_config again and add or change:

Port 2222

Pick any unused port above 1024. Before restarting SSH, open the new port in the firewall and only then remove the old rule — in that order, so you’re never without a working path in:

sudo ufw allow 2222/tcp
sudo sshd -t
sudo systemctl restart ssh

Test a fresh connection on the new port before closing your current session or removing the old firewall rule:

ssh deploy@your-server-ip -p 2222

Once that works, remove the now-unneeded rule for port 22:

sudo ufw delete allow OpenSSH

Every command from here on out needs -p 2222 added to it (or an entry in your own machine’s ~/.ssh/config so you don’t have to remember).

Checklist

Before installing anything else, you should be able to say yes to all of these:

  • System packages are fully updated
  • A non-root user exists with working sudo access
  • That user logs in with an SSH key, no password prompt
  • ssh root@your-server-ip is refused
  • sudo ufw status verbose shows active, with only the ports you actually need open
  • fail2ban is running and watching SSH
  • Automatic security updates are enabled
  • Hostname and timezone are set

Troubleshooting

Locked out over SSH entirely. Almost every hosting provider (DigitalOcean, Linode, Hetzner, AWS Lightsail, and so on) offers a browser-based console into the server — look for “Console” or “VNC” in your server’s dashboard. This connects you directly, bypassing SSH entirely, so you can log in as root there and fix whatever broke (usually a typo in sshd_config, or PasswordAuthentication no applied before a key was actually working).

“Permission denied (publickey)” when connecting as your new user. Almost always a permissions or path issue — re-check that ~/.ssh is 700 and ~/.ssh/authorized_keys is 600, both owned by that user (not root), and that you copied the .pub file’s contents, not the private key.

Banned your own IP with fail2ban. Use the fail2ban-client set sshd unbanip command from Step 8, run from a working session (your provider’s console works here too if SSH itself is what’s banned).

Forgot which user has sudo. getent group sudo (as root, via the console if needed) lists every user in the sudo group.

Wrapping Up

At this point the server has a non-root sudo user, key-only SSH access with root login disabled, a default-deny firewall with only SSH open, brute-force protection, automatic security patching, and a sane hostname and timezone. That’s the baseline we put on every server before installing anything user-facing on top of it.

If your hosting provider supports it, this is also a good point to take a snapshot or backup image of the server — it gives you a clean, hardened starting point to restore from if a later change ever goes badly wrong, rather than repeating all of the above from scratch.

Next up: installing and configuring Nginx on top of this.

Need help setting up or migrating your store to a production-ready server? Get in touch with our team — server setup and store migration is one of our core services.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *