A fresh Ubuntu Server install listens on more ports than most people expect. SSH is open, and anything you install later, a web server, a database, a VPN, tends to open its own door and leave it open. UFW, the Uncomplicated Firewall, is Ubuntu's friendly front end to the kernel firewall, and it turns "I should set up a firewall someday" into a ten-minute job. This guide covers a safe default setup for a remote server you reach over SSH, where one wrong order can lock you out of your own machine.

Before you start

You need a server running Ubuntu (20.04 or newer works the same way here), a non-root user with sudo, and an SSH connection that already works. If you are on a cloud VPS, note that your provider's security group or cloud firewall is a separate layer: UFW rules do not replace it, and it does not replace UFW. Both must allow SSH, or you are locked out anyway.

The warning that saves the most weekends: never enable UFW before explicitly allowing SSH. Its default policy denies all incoming traffic, so if you enable first and add the SSH rule second, your current session keeps working but the next login fails, and on a remote VPS that means a rescue console or a support ticket.

Step 1: Install UFW and check its state

UFW ships with Ubuntu Server by default, but check anyway:

TEXT
sudo apt update
sudo apt install ufw -y
sudo ufw status verbose

On a fresh server the answer is Status: inactive: no rules enforced yet. Do not enable it yet.

Step 2: Set the default policies

Set the defaults before adding any allow rules, so every port you do not name explicitly stays closed:

TEXT
sudo ufw default deny incoming
sudo ufw default allow outgoing

This is the sane default for a server: it can fetch updates and reach out, but nobody on the internet can initiate a connection to a port you have not opened. Run sudo ufw status verbose again and confirm the defaults read deny (incoming) and allow (outgoing).

Step 3: Allow SSH first, then verify it

This is the step that matters most. Allow SSH before anything else:

TEXT
sudo ufw allow OpenSSH

The OpenSSH profile is just a named shortcut for port 22/tcp, kept in /etc/ufw/applications.d/openssh-server. If you moved SSH to a custom port, allow that port instead, for example sudo ufw allow 2222/tcp, and skip the profile. Confirm the rule is listed:

TEXT
sudo ufw show added

You should see a rule allowing 22/tcp (or your custom port). If you do not see it, stop here and fix it before continuing.

Step 4: Enable UFW

Now, and only now, enable the firewall:

TEXT
sudo ufw enable

UFW warns that enabling may disrupt existing SSH connections. That warning is about new connections, not your current one; your established session stays up. Still, do not close your terminal yet. Open a second terminal and start a new SSH session to the server. If the new login works, the firewall is correct. If it fails, your first session is still alive to fix the rules.

Then check the live state:

TEXT
sudo ufw status verbose

You should see Status: active with the SSH rule and the default policies.

Step 5: Open only the ports you actually use

Add rules one service at a time. A web server needs HTTP and HTTPS (if you run Caddy, its automatic certificates still need those two ports reachable; see the Caddy web server playbook for the rest of that setup):

TEXT
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

UFW also ships application profiles: list them with sudo ufw app list, then allow one by name, for example sudo ufw allow 'Nginx Full'. Profiles are convenient, but a port number is unambiguous; when in doubt, use the port.

For a VPN on the same box, the pattern is identical: allow the VPN's UDP port (for example, WireGuard's 51820/udp, as in the WireGuard VPN server guide) and nothing else. Every open port should map to a service you can name; if you cannot name it, close it.

Step 6: Rate-limit SSH against brute force

SSH on the open internet gets probed constantly. UFW can throttle repeat connection attempts from one address:

TEXT
sudo ufw limit OpenSSH

This inserts a rate-limiting rule that refuses connections from an IP after several recent attempts. It is not a replacement for key-based login or for disabling password authentication, both of which belong in a full SSH hardening audit, but it cuts the noise dramatically, and it costs nothing.

Step 7: Manage rules without fear

Rules change as the server's job changes. To remove a rule, delete by number for precision:

TEXT
sudo ufw status numbered
sudo ufw delete 2

Deleting by number avoids the ambiguity of typing a rule twice. To see what the firewall is actually blocking, turn on logging at the low level, which records blocked packets without flooding the disk:

TEXT
sudo ufw logging on

Check it with sudo grep UFW /var/log/ufw.log; entries marked [UFW BLOCK] are the firewall doing its job, and the first place to look when a new service cannot be reached.

To see the real listening state behind the firewall, compare UFW's view with the kernel's:

TEXT
sudo ss -tulpn

Anything in ss output that is not in your UFW rules is either localhost-only or a service you forgot to firewall.

Three traps that catch real servers

First, Docker bypasses UFW. Docker inserts its own iptables rules ahead of UFW's, so a container published with -p 8080:80 is reachable even if UFW denies 8080. If you run Docker on a UFW-protected host, either bind containers to localhost (-p 127.0.0.1:8080:80) or manage Docker's iptables integration explicitly.

Second, UFW and IPv6. UFW manages IPv6 rules only if IPV6=yes in /etc/default/ufw, which is the Ubuntu default. If someone set it to no, your IPv6 listeners are unfiltered. Check the file after any template change from a VPS provider.

Third, enabling UFW twice, or scripting ufw --force enable in a provisioning script before the SSH rule, reproduces the lockout from the introduction at scale. In automation, always order the playbook: defaults, SSH allow, then enable, then the rest.

Verify the whole setup

Before you call it done, run this checklist from a second machine, not just from the server itself:

  1. A new SSH login succeeds.
  2. sudo ufw status verbose shows Status: active, SSH allowed, defaults deny incoming and allow outgoing.
  3. A port scan of a closed port (for example with nmap -Pn <server-ip>) shows it filtered, not open.
  4. Your web service, if any, still answers on 80 and 443.

If all four pass, you have a firewall that does its job and stays out of your way. Revisit it whenever you install a new service: open its port deliberately, or leave it closed by default.

Sources

  • Ubuntu Community Help Wiki, UFW: https://help.ubuntu.com/community/UFW
  • UFW manual page: https://manpages.ubuntu.com/manpages/noble/en/man8/ufw.8.html
  • Ubuntu Server Guide, firewall: https://documentation.ubuntu.com/server/how-to/security/firewalls/