Cloud

The Ubuntu server basics we wish someone had handed us on day one

CLD

Our first DigitalOcean droplet ran for a couple of weeks with the default root SSH login still enabled before we got around to locking it down properly. Nothing bad happened, but it easily could have, and the fact that it easily could have is exactly why we're writing this down now rather than after something actually goes wrong.

The checklist

  • Create a non-root user with sudo access, disable root SSH login entirely.
  • Set up ufw and only open the ports actually needed, 22, 80, 443.
  • Install fail2ban so repeated failed login attempts get blocked automatically.
  • Set unattended-upgrades for security patches so we're not manually tracking CVEs.

None of this is advanced, it's genuinely basic server hygiene, but it wasn't obvious to us coming from a world of shared hosting where all of this was handled invisibly by someone else, usually without us even knowing it was happening.

Why root SSH login is the first thing to fix

Root is the one username every automated attack against a server already knows exists, which means any bot scanning the internet for exposed SSH ports doesn't need to guess a username, only a password, cutting the actual attack surface in half compared to needing to guess both. Creating a named user with sudo privileges and disabling root login entirely means an attacker now has to guess both a username and a password, and fail2ban means they only get a handful of guesses before being locked out for a while.

We didn't understand how exposed this left us until reading through our own server's auth logs for the first time and seeing a steady trickle of failed root login attempts, dozens a day, from IPs all over the world, automated and untargeted, just scanning for exactly this kind of oversight. That was the moment this stopped feeling like optional advice and started feeling like an obvious thing to fix immediately.

Setting up the firewall properly

ufw, Ubuntu's simplified firewall tool, made this less intimidating than we expected. The default-deny posture, block everything, then explicitly allow only what's needed, felt like the right mental model even before we fully understood the underlying rules ufw is generating for us. We limited ourselves to exactly three open ports for a typical web server, SSH, HTTP, and HTTPS, and closed everything else, including a couple of ports we hadn't realized were open by default from the base server image.

fail2ban and unattended-upgrades, the two we almost skipped

Both of these felt like they belonged to "advanced sysadmin territory" before we actually looked into them, and both turned out to be a single package install and a short config file. fail2ban watches log files for repeated failed login attempts and temporarily bans the offending IP, which handles the automated brute-force scanning we found in our logs without us needing to do anything ongoing. unattended-upgrades applies security patches automatically on a schedule, which matters because a server that's never patched is a server that's slowly accumulating known, exploitable vulnerabilities that get worse the longer they sit unaddressed.

What we deliberately left off this list

We're not running anything more elaborate than this yet, no intrusion detection system, no security auditing tooling beyond what's built into these basics, and we're aware that's a real gap for anything handling genuinely sensitive data. For the kind of small business client sites we're hosting, mostly brochure sites and light e-commerce, this baseline feels proportionate to the actual risk, rather than under- or over-engineered for what we're protecting. We'd revisit this list without hesitation for a client handling anything more sensitive, financial data, health information, where the cost of a breach would be dramatically higher than what we're weighing here.

Turning this into a repeatable setup

We've started keeping this checklist as an actual runbook we follow, in order, on every new droplet before it touches a domain or any real traffic, rather than relying on memory to catch every step. It takes maybe twenty minutes total once you've done it a couple of times, twenty minutes that used to feel like an annoying delay before the "real" work of setting up a site, and now feels like an obviously necessary part of that same work, not separate from it.

What we'd add if we were starting a client-facing product today

If we were hosting something more exposed than a client's brochure site, a product with user accounts and payment information, say, we'd add a few more items without much hesitation: SSH key-based authentication instead of password login entirely, removing password guessing as an attack vector altogether rather than just slowing it down; a proper log aggregation and alerting setup so we'd know about a suspicious pattern in near real time rather than only discovering it during an occasional manual log review; and probably a managed database service rather than running MySQL directly on the same box as the web server, to reduce the blast radius if the web-facing side of the server were ever compromised.

None of that felt proportionate to what we're actually running right now, but it's useful to have thought through where the next tier of seriousness starts, so we're not caught flat-footed reasoning about it for the first time under pressure if a future client's needs genuinely call for it.

The auth log habit we picked up

Beyond the one-time setup, we started actually glancing at auth logs periodically now, not obsessively, but often enough to notice if the pattern of failed login attempts changes meaningfully, a sudden spike, attempts against a username other than the standard scanning noise, anything that looks less like background internet noise and more like someone specifically probing this one server. It's a small habit, a couple of minutes every week or two, but it's the kind of low-effort vigilance that turns "we would have found out eventually" into "we noticed within a day," which matters a lot more than it sounds like it should when something actually does go wrong.

Server hardening as an ongoing thing, not a one-time task

The temptation after setting all of this up once is to consider the server "secured" and move on, but Ubuntu itself releases new versions, packages get deprecated, and best practices shift over time in ways that this checklist, frozen as a document, won't automatically reflect. We're treating this less as a finished checklist and more as a living one, something we revisit and update whenever we read about a new class of attack or a new tool that's become the accepted standard, rather than assuming the version we wrote down this year will still be complete and correct a couple of years from now.

Where we learned most of this

None of this came from formal training, it came from a mix of DigitalOcean's own community tutorials, a couple of security-focused blog posts we found while researching the specific "why is my server getting so many failed login attempts" question, and honestly a decent amount of trial and error on our own droplet before we trusted the same steps on a client's server. We'd recommend the same path to anyone else making this same jump from shared hosting, read a couple of good tutorials, apply them to a low-stakes test server first, and only then roll the same steps out to something a client actually depends on.

Setting up swap on the smallest droplets

We learned this one after a WP-CLI command on our smallest droplet, one gigabyte of RAM, got silently killed mid-run with no error message beyond the SSH session just returning to a prompt. It took a while to realize the kernel's out-of-memory killer had ended the process rather than anything actually going wrong with our command. Adding a few gigabytes of swap space, fallocate to create the file, chmod it down to root-only, mkswap and swapon to activate it, then an entry in /etc/fstab so it survives a reboot, gave the box enough breathing room to handle the occasional memory spike without crashing outright. It's not a substitute for right-sizing the droplet if we're consistently running out of memory, but as a safety net against the occasional one-off spike it's cheap insurance, a few minutes of setup on a box that otherwise has no memory cushion at all.

Log rotation, the thing that quietly fills a disk

We found out the hard way that Ubuntu's default logging setup doesn't automatically bound how large every log file gets for every service, and one particularly chatty application log on an older droplet grew until it had eaten most of the remaining disk space, which we only noticed when a deploy failed with a cryptic "no space left on device" error that had nothing obviously to do with logs. logrotate was already installed and already handling most of the standard system logs, but the application-specific log wasn't covered by any of its default configuration. A short config file in /etc/logrotate.d, rotate weekly, keep four weeks, compress the old ones, fixed it permanently, and we've since gotten in the habit of checking whether logrotate covers any new log file a piece of software starts writing, rather than assuming it's handled automatically just because logrotate is already running on the box.

Automated snapshots as a cheap backup layer

Beyond the security checklist itself, we turned on DigitalOcean's automated weekly snapshots for every droplet running a client site, which costs a small percentage of the droplet's monthly price and gives us a full disk image we can restore from if something goes catastrophically wrong, a bad update, a botched manual change late at night, a compromised server we'd rather rebuild from a known-good state than try to clean by hand. It's not a substitute for actual application-level backups, we still take separate database dumps for anything with real data in it, but as a last-resort "undo the entire server" button it's the cheapest insurance we've bought all year, and restoring from one during a test run took less than fifteen minutes from click to a fully working droplet again.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients