LINESERVE
All tutorials
TutorialAll levels

How to Set Up a Firewall with UFW on Ubuntu 24.04

How to Set Up a Firewall with UFW on Ubuntu 24.04 By the end of this guide your Ubuntu 24.04 server will be dropping every unsolicited inbound packet, allowing only the ports you actually run, rate-limiting SSH against brute-force bots, and logging what it blocks. Every command below was run on a clean Ubuntu 24.04.4 […]

Stephen NdegwaPublished 12 min read
Share
How to Set Up a Firewall with UFW on Ubuntu 24.04

How to Set Up a Firewall with UFW on Ubuntu 24.04

By the end of this guide your Ubuntu 24.04 server will be dropping every unsolicited inbound packet, allowing only the ports you actually run, rate-limiting SSH against brute-force bots, and logging what it blocks. Every command below was run on a clean Ubuntu 24.04.4 server and the output you see is the real output that server printed, including the parts most guides skip: why the three ways of allowing SSH are not the same rule, exactly when the “this may disrupt your SSH connection” prompt appears (and when it silently does not), and what ufw limit actually does to the kernel.

UFW (“Uncomplicated Firewall”) is a front-end. It does not filter packets itself; it writes rules into Linux’s netfilter engine through iptables (which on 24.04 is really the nf_tables backend). You get a handful of readable commands instead of raw iptables syntax, without losing any of the power.

Prerequisites

  • An Ubuntu 24.04 server you can reach over SSH, with a sudo-capable user. If you have not done the groundwork yet, start with Initial Ubuntu 24.04 Server Setup: Users, SSH, Firewall and come back here.
  • The port numbers of the services you run. For a typical web server that is 22 (SSH), 80 (HTTP) and 443 (HTTPS).
  • A way back in if you lock yourself out. On a Lineserve Linux VPS that is the web console; on other hosts it may be a serial or VNC console. You will not need it if you follow the SSH step in order, but know where it is first.

Understand the starting state before you touch anything

UFW ships with Ubuntu 24.04. Check its version and its status before changing a thing:

$ ufw version
ufw 0.36.2
Copyright 2008-2023 Canonical Ltd.

$ sudo ufw status verbose
Status: inactive

Two things surprise people here. First, the firewall is inactive on a fresh install — nothing is being filtered yet. Second, and this is the part that reads like a contradiction:

$ systemctl is-enabled ufw
enabled

The systemd unit is enabled (it will start at every boot) while the firewall itself is inactive (it is not filtering). “Enabled” and “active” are different states. Once you run ufw enable, the ruleset persists across reboots automatically — there is no separate “make it permanent” step to remember.

Step 1: Set your default policies

A firewall is only as good as its default: what happens to a packet that matches no rule. The safe posture for a server is deny everything inbound, allow everything outbound.

$ sudo ufw default deny incoming
Default incoming policy changed to 'deny'
(be sure to update your rules accordingly)

$ sudo ufw default allow outgoing
Default outgoing policy changed to 'allow'
(be sure to update your rules accordingly)

On Ubuntu 24.04 these are already the shipped defaults — you can confirm it in the config file:

$ grep -E 'DEFAULT_(INPUT|OUTPUT|FORWARD)_POLICY' /etc/default/ufw
DEFAULT_INPUT_POLICY="DROP"
DEFAULT_OUTPUT_POLICY="ACCEPT"
DEFAULT_FORWARD_POLICY="DROP"

Running the two commands anyway is good practice: it makes the policy explicit and it is what your future self will grep the shell history for. Do not set a default-deny outgoing policy unless you know exactly what your server talks to — it will break package updates, DNS and NTP until you add outbound allow rules for each.

Step 2: Allow SSH before you enable the firewall

This is the one ordering mistake that locks people out of their own server. With deny incoming as the default, the moment you enable UFW without an SSH rule, your existing connection keeps working but your next one is dropped. Add the rule first:

$ sudo ufw allow OpenSSH
Rules updated
Rules updated (v6)

Now, the detail nearly every tutorial glosses over: there are three common ways to allow SSH and they are not the same rule. Add all three and ask UFW to show you what it recorded:

$ sudo ufw show added
Added user rules (see 'ufw status' for running firewall):
ufw allow OpenSSH
ufw allow 22/tcp
ufw allow 22
  • ufw allow OpenSSH uses an application profile — a named bundle that resolves to 22/tcp.
  • ufw allow ssh reads /etc/services, finds ssh maps to 22/tcp, and opens TCP only.
  • ufw allow 22 specifies no protocol, so UFW opens the port for both TCP and UDP.

That last one matters. allow 22 leaves UDP port 22 open for no reason. Prefer ufw allow OpenSSH (self-documenting) or ufw allow 22/tcp (explicit protocol) and never the bare port number for a TCP service.

You can see which application profiles exist. On a clean 24.04 image the list is short — only what is installed ships a profile:

$ sudo ufw app list
Available applications:
  OpenSSH

$ sudo ufw app info OpenSSH
Profile: OpenSSH
Title: Secure shell server, an rshd replacement
Description: OpenSSH is a free implementation of the Secure Shell protocol.

Port:
  22/tcp

Install Nginx or Apache and they add their own profiles (Nginx Full, Apache Full, and so on). Until then, do not expect a web profile to exist.

Step 3: Enable the firewall

With the SSH rule in place, turn it on:

$ sudo ufw enable
Firewall is active and enabled on system startup

Here is behaviour worth knowing, because it is inconsistent by design. When you run ufw enable from an SSH session, UFW prints a safety prompt first:

Command may disrupt existing ssh connections. Proceed with operation (y|n)?

When you run it from a local console, a serial console, or an out-of-band agent, that prompt does not appear — the firewall just enables silently. This is not random: UFW only prompts if it detects it is running under SSH (the check is ufw.util.under_ssh() in its source). So do not rely on the prompt as your seatbelt; on a rescue console it will not be there. The seatbelt is Step 2 — allow SSH first, every time. If you are scripting the setup and want to skip the prompt deliberately, use sudo ufw --force enable.

Check the result:

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere
22/tcp (OpenSSH (v6))      ALLOW IN    Anywhere (v6)

Notice the (v6) line. You did nothing to get it, which brings us to a point most 24.04 guides get wrong.

Step 4: You do not need to “turn on” IPv6

Older tutorials tell you to edit /etc/default/ufw and set IPV6=yes. On Ubuntu 24.04 that is already the default:

$ grep IPV6 /etc/default/ufw
IPV6=yes

Every rule you add is applied to both IPv4 and IPv6 automatically — that is why the commands above each printed a line and a (v6) line. This is convenient but has one consequence you will meet in Step 7 when you delete a rule by number. Keep it in mind.

Step 5: Open the ports your services need

Now open the rest. You can allow by service name, by port, by port and protocol, or by port range:

$ sudo ufw allow http
Rule added
Rule added (v6)

$ sudo ufw allow 443/tcp
Rule added
Rule added (v6)

$ sudo ufw allow 6000:6007/tcp
Rule added
Rule added (v6)

A range uses a colon and requires an explicit protocol — ufw allow 6000:6007 without /tcp or /udp is rejected. You can also explicitly deny a port, which is only meaningful if your default were allow or if a broader rule above would otherwise permit it:

$ sudo ufw deny 23/tcp
Rule added
Rule added (v6)

Step 6: Restrict a port to specific source IPs

Opening a database or admin port to the entire internet is how databases end up ransomed. Scope it to the addresses that should reach it. Restrict Postgres to one admin box and one internal subnet:

$ sudo ufw allow from 203.0.113.10 to any port 5432 proto tcp
Rule added

$ sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp
Rule added

Look closely: each printed one “Rule added”, not the two you saw earlier. Source-restricted rules that name an IPv4 address or CIDR block are IPv4-only — there is no (v6) twin, because 203.0.113.10 is an IPv4 address. If your admin hosts also reach the server over IPv6, add a second rule with the IPv6 source. This asymmetry trips people who assume UFW always mirrors v4 and v6.

Step 7: Rate-limit SSH against brute-force bots

A public SSH port gets hammered by credential-stuffing bots within minutes of coming online. UFW’s limit verb throttles repeat connections from the same address. Replace the plain SSH allow with a limited one:

$ sudo ufw delete allow OpenSSH
Rule deleted
Rule deleted (v6)

$ sudo ufw limit ssh
Rule added
Rule added (v6)

In the status output the action changes from ALLOW to LIMIT:

$ sudo ufw status verbose
...
To                         Action      From
--                         ------      ----
22/tcp                     LIMIT IN    Anywhere

What does “limit” actually mean? Guides tend to hand-wave it. Here is the exact netfilter rule UFW generated, pulled straight from the kernel:

$ sudo iptables -S | grep -i 22
-A ufw-user-input -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name DEFAULT --mask 255.255.255.255 --rsource
-A ufw-user-input -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 30 --hitcount 6 --name DEFAULT --mask 255.255.255.255 --rsource -j ufw-user-limit
-A ufw-user-input -p tcp -m tcp --dport 22 -j ufw-user-limit-accept

The threshold is --seconds 30 --hitcount 6: if a single source IP opens 6 or more new connections within 30 seconds, further attempts are sent to the ufw-user-limit chain, which logs and rejects them. It uses the kernel’s recent module to track offenders. This slows brute-forcing to a crawl without ever blocking a human who mistypes a password twice. Do not limit your web ports, though — a busy site legitimately opens far more than six connections in 30 seconds and you would throttle real visitors.

Step 8: List and delete rules by number

To edit rules, number them:

$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere
[ 2] 80/tcp                     ALLOW IN    Anywhere
[ 3] 443/tcp                    ALLOW IN    Anywhere
[ 4] 6000:6007/tcp              ALLOW IN    Anywhere
[ 5] 23/tcp                     DENY IN     Anywhere
[ 6] OpenSSH (v6)               ALLOW IN    Anywhere (v6)
[ 7] 80/tcp (v6)                ALLOW IN    Anywhere (v6)
[ 8] 443/tcp (v6)               ALLOW IN    Anywhere (v6)
[ 9] 6000:6007/tcp (v6)         ALLOW IN    Anywhere (v6)
[10] 23/tcp (v6)                DENY IN     Anywhere (v6)

Now the IPv6 consequence from Step 4 becomes concrete. All the IPv4 rules are numbered first, then all the IPv6 rules. If you delete the telnet deny at position 5:

$ sudo ufw delete 5
Rule deleted

…only the IPv4 rule is gone. Its IPv6 twin is still there, renumbered. Delete by number removes exactly one rule, one address family. If you instead delete by specification, UFW removes both families at once:

$ sudo ufw delete allow 6000:6007/tcp
Rule deleted
Rule deleted (v6)

The practical rule: delete by spec, not by number, whenever you can. Deleting by number is fine for one-offs, but always re-run ufw status numbered afterwards, because every deletion renumbers everything below it — the number you wanted a second ago is now pointing at a different rule.

Step 9: Turn on and tune logging

UFW logs to /var/log/ufw.log. Logging is on at low by default; you can dial it between off, low, medium, high and full:

$ sudo ufw logging medium
Logging enabled

$ sudo ufw logging low
Logging enabled

low records blocked packets and a few matched rules — the right level for most servers. medium and high add rate-limited logging of all packets and invalid states, which is useful when you are actively debugging a rule but noisy for daily use. Blocked packets appear like this (a real entry from the test server, which had been public for minutes):

$ sudo grep 'UFW BLOCK' /var/log/ufw.log | tail -1
... kernel: [UFW BLOCK] IN=ens5 OUT= MAC=... SRC=3.253.227.233 DST=172.31.20.207 LEN=76 ... PROTO=TCP SPT=443 DPT=54180 ...

SRC is who tried, DPT is the port they aimed at. If you are ever unsure whether the firewall is doing its job, this file is the proof.

Verify it works

Two commands confirm your posture. First, the human-readable view:

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

Status: active and Default: deny (incoming) are the two lines that matter. Second, from another machine, confirm a closed port is actually refused and an open one answers — for example nc -zv your.server.ip 80 should connect and nc -zv your.server.ip 5432 should hang or be refused from an address you did not allow. If a port you expected to be open is refused, re-check ufw status numbered for a DENY rule sitting above your ALLOW — UFW evaluates top to bottom and the first match wins.

Troubleshooting

“I enabled UFW and now I cannot SSH in.” You enabled before allowing SSH, or you allowed the wrong port because you run SSH on a non-standard port. If you have console access (the Lineserve VPS web console, or your provider’s serial console), log in there and run sudo ufw allow 22/tcp — or your custom port — then sudo ufw reload. This is exactly why Step 2 comes before Step 3.

“My rule is there but traffic is still blocked.” Order matters. A DENY rule higher in the numbered list beats an ALLOW below it. Run sudo ufw status numbered, and if a deny is in the way, delete it and re-add the allow so it lands in the right place. Also confirm you opened the correct protocol — a service on UDP will not match a tcp-only rule.

“I deleted a rule but the port is still open.” You almost certainly deleted only the IPv4 rule by number and the IPv6 twin is still allowing traffic (Step 8). Run sudo ufw status numbered, find the (v6) line, and delete it too — or in future delete by spec.

Starting over: disable and reset

To switch the firewall off without losing your rules:

$ sudo ufw disable
Firewall stopped and disabled on system startup

$ sudo ufw status
Status: inactive

To wipe every rule back to the shipped defaults, use reset. It prompts for confirmation and backs up your existing rule files first:

$ sudo ufw reset
Resetting all rules to installed defaults. Proceed with operation (y|n)? y
Backing up 'user.rules' to '/etc/ufw/user.rules.20260712_155353'
...

The timestamped backups in /etc/ufw/ mean a reset is recoverable — you can copy an old user.rules back if you reset by mistake. To reset non-interactively in a script, sudo ufw --force reset.

That is a complete, tested firewall: default-deny inbound, SSH allowed and rate-limited, only your service ports open, IPv6 covered, and logging on. Pair it with the rest of your hardening checklist in 8 Essential Security Steps for Every Production VPS, and if you are deploying an app behind it, Deploying a Node.js App to Production on a VPS: The Complete Guide shows where the firewall fits in the stack. Windows admins working the other side of the same problem will want How to Enable ICMP (Ping) on Windows Public Firewall.

Run it on infrastructure that stays in your region

A firewall protects the server; where that server lives protects your users and your data-residency story. Lineserve Linux VPS plans start at the S1 (1 vCPU, 2 GB RAM, SSD, dedicated IPv4) and run Ubuntu 24.04 in locally hosted regions in Nairobi, Dar es Salaam and Lagos — so your traffic and your data stay in-country, and you are billed in KES, TZS or NGN with no forex surprises. Spin one up from the console at console.lineserve.net and you can walk through every command above on your own server in minutes. New to it? Getting Started with Lineserve Cloud: Launch Your First VPS takes you from signup to a running VPS.