DEV Community

Cover image for How I Automated Ubuntu Server Hardening with One Script
Tesleem Amuda
Tesleem Amuda

Posted on

How I Automated Ubuntu Server Hardening with One Script

Securing and Automating a Cloud Server

The first time I spun up a cloud server, I thought the hard part was over. It wasn't. The server was open to the world: root login enabled, port 22 broadcasting itself to every scanner on the internet, no firewall, and no certificate.

I checked /var/log/auth.log a few hours later and found hundreds of failed login attempts. That was the moment I understood why SSH hardening isn't optional.

Why SSH Hardening Matters

SSH is the front door to your server. By default, that door is wide open: port 22, password authentication allowed, and root login permitted. Automated bots scan millions of IP addresses every hour looking for exactly this.

Hardening SSH means closing that gap before anything else touches the server:

  • Disable root login: PermitRootLogin no
  • Disable password authentication and use keys only: PasswordAuthentication no
  • Move SSH off port 22—I use port 2247
  • Restrict login to one user: AllowUsers deploy
  • Disconnect idle sessions after 10 minutes:

    • ClientAliveInterval 300
    • ClientAliveCountMax 2

These five changes eliminate the vast majority of automated attacks. You're not invisible, but you're no longer an easy target.

What UFW Does

UFW—Uncomplicated Firewall—is Ubuntu's approachable interface to iptables. Rather than writing low-level rules, you express intent:

ufw default deny incoming
ufw default allow outgoing

ufw allow 2247/tcp   # SSH on custom port
ufw allow 80/tcp     # HTTP
ufw allow 443/tcp    # HTTPS

ufw --force enable
Enter fullscreen mode Exit fullscreen mode

Everything not explicitly allowed is blocked. That's the principle of least privilege applied to networking, and it's the same principle that governs good Kubernetes NetworkPolicies, IAM roles, and RBAC.

Start learning it here.

How Let's Encrypt Works

Let's Encrypt is a free, automated Certificate Authority trusted by major browsers. The flow is straightforward:

  1. You point a domain at your server's IP address.
  2. Certbot serves a challenge file over HTTP to prove that you control the domain.
  3. Let's Encrypt issues a certificate valid for 90 days.
  4. Certbot rewrites your Nginx configuration to serve HTTPS and redirect HTTP automatically.
  5. A scheduled task renews the certificate before it expires—no manual work needed.

The result is a free, automatically maintained HTTPS website.

The following command confirms that the renewal pipeline works without modifying your live certificate:

certbot renew --dry-run
Enter fullscreen mode Exit fullscreen mode

What Idempotency Means—and Why It Matters

Idempotent means that running the same operation twice produces the same result as running it once.

It sounds academic. It isn't.

In practice, it means:

  • If deploy already exists, skip it instead of returning an error.
  • If the certificate already exists, skip it instead of requesting a duplicate.
  • If packages are already installed, apt-get handles them gracefully.

Without idempotency, rerunning a setup script after a partial failure could lock you out of your own server. With it, you can run the script confidently after any interruption.

This habit carries forward into Ansible, Terraform, and every automation tool you'll use at a professional level.

The Script

The complete script is available here:

github.com/tesddev/server-bootstrap

On a fresh Ubuntu 22.04 server, one command does everything:

sudo bash setup.sh
Enter fullscreen mode Exit fullscreen mode

In order, the script:

  1. Updates the system and installs essential packages.
  2. Creates a non-root deploy user and copies in the SSH key.
  3. Hardens SSH by changing the port, disabling root login and password authentication, and setting an idle timeout.
  4. Configures UFW with an explicit allowlist.
  5. Installs Nginx and serves a custom page.
  6. Obtains a Let's Encrypt certificate using Certbot.

What You Get at the End

  • SSH restricted to key authentication on a non-standard port
  • A firewall permitting only necessary traffic
  • A live website at https://tes-devops.duckdns.org with a valid, automatically renewing certificate
  • A script you can run on any new server today or six months from now

The Real Lesson

Manual server setup doesn't scale, and more importantly, it doesn't reproduce reliably. The same steps performed by two different people on two different days will produce two subtly different servers.

Scripts don't have that problem.

Writing small, idempotent automation scripts is one of the most transferable habits in DevOps. Everything that follows—Ansible playbooks, Terraform modules, and Kubernetes manifests—is this same idea applied at a larger scale.

➡️ View the server-bootstrap repository on GitHub

Top comments (0)