UFW Firewall Setup (2026): A Guide With Real Measurements

UFW Firewall Setup (2026): A Guide With Real Measurements

A UFW firewall setup takes five minutes: set the default policies, allow SSH, open the web server ports, switch it on. Every guide says that, and it’s true. What almost no guide covers is what happens afterwards. How much the firewall actually catches. Whether the log tells the truth. Whether the rules you piled up over a year of running the server still do anything.

We looked at this on our own production server, which runs Ubuntu 24.04 and ufw 0.36.2 and has been on the internet for months. We went through the last seven days of the kernel journal and the firewall’s packet counters, and we also tested the server from a machine at a different provider.

Three findings up front, because the rest of the article is built on them:

1. In seven days the log holds 34,636 block entries, from 6,613 distinct sources, aimed at 10,442 distinct ports. That still isn’t everything that got dropped, just a sample. In a 15-minute follow-up measurement the firewall dropped 377 packets and logged 47, about one in eight.

2. Five of our rules have no effect, because a rule higher up always decides first. Two of them are blocks against specific IP addresses. They’re in the ruleset, ufw status lists them, and they do nothing.

3. Port 22 is open, yet it has 2,370 blocks. Only 4 of them were connection attempts.

The guide comes first, then the findings in detail. If you only need the commands, there’s a compact checklist at the end.

A server behind a glowing protective wall with three open gates, red particles bouncing off everywhere else

What Is UFW?

UFW stands for Uncomplicated Firewall. It isn’t a firewall of its own. It’s a front end for the packet filter that already lives in the Linux kernel. Canonical built UFW for Ubuntu, where it comes preinstalled, and it works the same way on Debian, Linux Mint and most derivatives.

The kernel filters packets with Netfilter. The classic tools for feeding rules to Netfilter are iptables and, more recently, nft (nftables). Both are powerful and hard to read. A simple HTTPS allow rule in iptables looks like this:

iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT

In UFW it’s this:

ufw allow 443/tcp

UFW translates that into the matching kernel rules for IPv4 and IPv6 at once and stores them so they survive a reboot. On our server the backend is no longer classic iptables, by the way. It’s nftables behind a compatibility layer:

$ iptables -V
iptables v1.8.10 (nf_tables)

For you as a user nothing changes. The commands are the same.

A simple control panel sitting on top of a complex machine of pipes and valves, connected by clean cables

Why Does a Server Need a Firewall at All?

A server that only runs a web server and SSH should, in theory, have three open ports. In practice far more programs listen on the network than you’d expect. We counted on our main server while writing this:

ss -tlnH | awk '{print $4}' | grep -v "127.0.0\|\[::1\]"

Result: 21 TCP services listening on a public address, plus 46 more on loopback only. The 21 include Next.js dev servers, Node services started on * instead of 127.0.0.1, and a synthesizer daemon nobody remembers starting. Without a firewall, all 21 would be reachable from the internet.

That’s the real job of a host firewall. It isn’t mainly about the one service you open on purpose. It’s about the fifteen you opened by accident. How to bind services correctly in the first place is covered in What Is a Reverse Proxy.

UFW Firewall Setup: Step by Step

These steps apply to Ubuntu and Debian. Every command needs root, either as root or prefixed with sudo.

Step 1: Install and Check the Status

UFW comes preinstalled on Ubuntu but is not active. On Debian you usually have to install it first:

apt update && apt install ufw
ufw status

Right after installation UFW reports Status: inactive. That’s not an error. UFW never switches itself on, because on a remote server that could cut your own connection instantly.

Step 2: Set the Default Policies

The basic rule for any server firewall is to deny everything incoming, allow everything outgoing, then open specific exceptions.

ufw default deny incoming
ufw default allow outgoing

“Allow outgoing” means your server can download updates, send mail and call APIs. “Deny incoming” means nobody from outside can open a new connection to it, except on the ports you’re about to allow. Replies to connections your server opened itself still get through. The kernel’s connection tracking (conntrack) takes care of that, and we’ll come back to it.

There’s a third policy, routed, for forwarded traffic. On a normal server it’s set to deny and can stay that way. It’s also where the Docker trap lives, which we show further down.

Step 3: Allow SSH Before You Switch Anything On

This is the most important sentence in the guide. If you enable UFW on a remote server without allowing SSH first, you lock yourself out. The session you’re working in keeps running because it already exists. The next one won’t connect.

ufw allow OpenSSH
# or, equivalently, if SSH runs on the default port:
ufw allow 22/tcp comment 'SSH'

OpenSSH is an application profile. Packages that ship network services drop these profiles into /etc/ufw/applications.d/. To see which ones your system has:

ufw app list

On our machine that includes OpenSSH, Nginx Full, Nginx HTTPS, Postfix and Dovecot Secure IMAP. The advantage of profiles is that if SSH moves to another port, the profile gets updated and the rule stays correct.

If you’ve moved SSH to a different port, allow that port instead of 22. And if you always work from a fixed address, you can restrict SSH to it:

ufw allow from 203.0.113.5 to any port 22 proto tcp comment 'SSH office only'

We showed with real attack numbers why SSH keys matter more than any firewall rule for port 22 in What Is SSH?.

Step 4: Allow the Services That Should Be Public

For a web server:

ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'

Or via the profile:

ufw allow 'Nginx Full'

Always specify the protocol. ufw allow 443 without /tcp opens the port for TCP and UDP. That can be intentional, since HTTP/3 runs over UDP 443. On our server the UDP 443 rule really has accepted around 42,000 packets, because Caddy speaks HTTP/3. Port 80 is different: no protocol anyone needs runs over UDP there, yet the rule ufw allow 80 still let 23 UDP packets through. Harmless, since nothing listens on it, but not what was intended either.

The comment '...' parts aren’t cosmetic. A year from now you won’t remember why port 22005 is open. We do, because it’s written next to the rule.

Step 5: Switch It On

ufw enable

UFW asks for confirmation because existing SSH connections might be interrupted. If you did step 3, nothing happens. Afterwards, open a second terminal and reconnect before closing the first one. If the new connection works, you’re fine. If it doesn’t, you can still type ufw disable in the first window.

UFW stays active after a reboot. You can confirm that with:

systemctl is-enabled ufw
grep ENABLED /etc/ufw/ufw.conf

Step 6: Check the State

ufw status verbose

The header on our server looks like this:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

When you’re editing individual rules, the numbered view is handier:

ufw status numbered

And before you actually apply a change, you can preview it without it taking effect:

ufw --dry-run allow 8080/tcp

--dry-run prints the kernel rules UFW would generate. It’s the fastest way to understand what a command really does. We used it repeatedly for this article so we wouldn’t change anything on the production server.

Managing Rules: Delete, Insert, Limit

Deleting a Rule

There are two ways. By number:

ufw status numbered
ufw delete 4

Or by repeating the original rule with delete in front:

ufw delete allow 80

The second way is safer. Numbers shift after every deletion, so if you delete two rules by number in a row, the second one hits the wrong rule.

Order Matters: The First Matching Rule Wins

This is the principle people overlook most often with UFW, and it’s behind our second finding. UFW evaluates rules top to bottom, and the first rule that matches decides. Nothing below it is looked at for that packet.

Several stacked filter layers, a glowing packet stopping at the top one while the lower layers stay dark

New rules added with ufw allow or ufw deny are appended to the end. So if you want to block an IP address that already has access to an open port, the block has to go to the top:

ufw insert 1 deny from 198.51.100.23 comment 'scanner'

Without insert 1, the block ends up below the allow rules for 22, 80 and 443. For those ports it’s never consulted.

Protecting SSH With limit

Besides allow, UFW has limit. The generated rules show what it does:

$ ufw --dry-run limit 22/tcp
-A ufw-user-input -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set
-A ufw-user-input -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 30 --hitcount 6 -j ufw-user-limit
-A ufw-user-input -p tcp --dport 22 -j ufw-user-limit-accept

In plain terms: anyone opening 6 or more new connections to port 22 within 30 seconds gets rejected. That slows down primitive brute-force scripts. It doesn’t replace a tool like fail2ban, which counts failed logins rather than connections and bans for hours rather than seconds. On our server fail2ban has issued 1,930 bans so far. Why even that helps less than you’d hope is something we worked out in Linux Server Setup.

Be careful with limit if you automate over SSH yourself. Deployment tools that open a fresh connection for every step hit six connections in 30 seconds faster than any attacker.

Don’t Forget IPv6

On current installs, /etc/default/ufw contains IPV6=yes. UFW then creates every rule twice, once for IPv4 and once for IPv6. You’ll recognise the IPv6 copies by the (v6) lines in ufw status.

If it says IPV6=no, UFW filters IPv4 only. If your server has an IPv6 address, and nearly every cloud server does, it’s then completely unfiltered over IPv6. That’s one of the few places where UFW can quietly do the wrong thing. Check it once:

grep IPV6 /etc/default/ufw

For a sense of how much arrives over IPv6: since the last reboot, the IPv6 default policy on our server has dropped 24,061 packets. The IPv4 policy over the same period dropped 4.1 million. Scanners hit IPv6 far less, because the address space is too large to sweep blindly. Less isn’t zero, though.

What the Firewall Actually Catches in a Week

Now for the part guides leave out. We pulled the block entries from the last seven days of the kernel journal:

journalctl -k --since "7 days ago" --no-pager | grep "UFW BLOCK" > ufwb.txt
wc -l < ufwb.txt

34,636 entries. Between 4,577 and 6,051 per day, roughly 4,900 on average. The most frequently targeted ports:

PortBlocksWhat usually runs there
84434,011alternative HTTPS, admin panels
222,370SSH (open on our server, more below)
231,748Telnet
33061,631MySQL/MariaDB
30001,177Node, Grafana, dev servers
8080500HTTP proxies, Tomcat
8000364Python/Django dev servers
4443353alternative HTTPS
8081341proxies, admin panels
9000309PHP-FPM, Portainer, MinIO

The top 10 together account for only 12,804 of the 34,586 entries with a destination port, a little over a third. The rest is spread across 10,442 different ports. That’s what a mass scan looks like: somebody systematically knocking on every door to see whether anything answers.

The list is a fairly accurate picture of what people leave open by mistake: databases, dev servers on 3000 and 8000, admin panels on 8443 and 9000, and still Telnet. Exactly the services where “I’m just testing this quickly” becomes permanent. On our server, several Node services around port 3000 really do listen on every address. The firewall is the only reason that doesn’t matter.

The Log Is a Sample, Not a Count

Reading these numbers, it’s easy to assume that’s everything the firewall stopped. It isn’t. The log rule shows why:

$ iptables -S ufw-after-logging-input
-A ufw-after-logging-input -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW BLOCK] "

--limit 3/min --limit-burst 10 means that under sustained traffic UFW logs at most three packets per minute, with a buffer of ten for short bursts. Everything beyond that is still dropped, just no longer written down. That’s sensible, because without a cap a single scan could fill the disk with logs. It also means the log count is capped from above and tells you nothing about the real volume.

We measured how big the gap is. For 15 minutes we compared the packet counter of the dropping default policy with the number of log lines in the same window:

start=2026-09-29 07:10:47  policy_drops=377  journal=47

377 packets dropped, 47 logged. The log shows roughly one packet in eight. Extrapolated, that’s about 36,000 dropped packets a day, while just under 5,000 a day reach the log. The long-term counter fits: the default policy has dropped around 4.1 million IPv4 packets, or about 45,000 a day over the 91 days since the last reboot.

A large stream of red particles flowing past a small funnel that catches only a thin trickle into a notebook

The practical consequence: if you want to know how much your firewall catches, read the packet counters, not the log.

iptables -L INPUT -v -n -x | head -1
# Chain INPUT (policy DROP 4102283 packets, 450230683 bytes)

And if an analysis is based on the log, say “port X is attacked most”, it’s describing a throttled sample. The ranking is roughly right. The absolute numbers aren’t.

You control logging with ufw logging (off, low, medium, high, full). low is the default and is enough. Higher levels also log allowed packets and produce a lot of data very quickly.

Port 22 Is Open, So Why 2,370 Blocks?

This is the finding that surprised us most. Port 22, which we explicitly allow, has 2,370 block entries. How can a firewall block something it’s supposed to allow?

The answer is in the TCP flags of the blocked packets:

grep "DPT=22 " ufwb.txt | grep -o "\(SYN\|ACK\|RST\|FIN\|PSH\)[ A-Z]*URGP" | sort | uniq -c | sort -rn
   2054 ACK PSH URGP
    302 ACK PSH FIN URGP
      9 RST URGP
      4 SYN URGP

A new connection attempt always starts with a SYN packet. Of the 2,370 blocks, only 4 were such attempts. The other 2,366 were data packets (ACK PSH) and teardown attempts (FIN, RST) belonging to connections the kernel no longer knew about.

Here’s how that happens. The kernel keeps every connection in its connection tracking table (conntrack). When a connection dies because fail2ban banned the other side, because a timeout expired, or because the attacker killed their script mid-handshake, the entry disappears. If the other side keeps sending packets after that, they don’t match any known connection. The kernel classifies them as INVALID, and UFW drops INVALID packets at the very start of its chain, before the port rules are even checked:

-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
-A ufw-before-input -m conntrack --ctstate INVALID -j DROP

That also explains a second finding: 81 SYN packets to port 443 were blocked even though 443 is open. On an open port, the only way for a packet to end up in the UFW log is through this INVALID rule. All 81 came from a handful of address ranges with almost identical characteristics (same packet length, same TCP window size of 14600). That’s the signature of a scanner crafting its own packets instead of using the normal network stack.

The lesson: a block on an allowed port doesn’t mean the rule is wrong. If you don’t know that, you start tinkering with rules that work fine. In total, the INVALID rule on our server has dropped 83,093 packets since the firewall was last loaded.

The Most Persistent Knocker Was One of Ours

On port 3306 (MySQL), 1,565 of the 1,631 blocks came from a single address. Around the clock, 20 to 33 logged attempts every hour, for seven days. We looked the address up: it’s another server we run ourselves. Some service there is apparently still trying to reach a database on a server that stopped offering it to the outside a long time ago.

That’s not an attack. It’s leftover configuration that would never have surfaced without this analysis. The service on the other end hasn’t had an answer in weeks, and nobody noticed. We haven’t traced which service it is yet. It’s on our list now.

That’s why the analysis is worth doing even if you’re not worried about attackers: a firewall also sees what your own infrastructure is getting wrong. To find the top senders:

grep -o "SRC=[0-9.]*" ufwb.txt | sort | uniq -c | sort -rn | head

Five Rules That Do Nothing

ufw status shows which rules exist. It doesn’t show whether they ever do anything. For that you have to go one level down, to the packet counters of the kernel rules:

iptables -L ufw-user-input -v -n -x --line-numbers

The first column, pkts, counts how many packets a rule has matched. On our server it looked like this (shortened, IP addresses replaced with documentation addresses):

num    pkts  target  prot  source          destination
1    117923  ACCEPT  tcp   0.0.0.0/0       tcp dpt:22
2    167277  ACCEPT  tcp   0.0.0.0/0       tcp dpt:80
3    251145  ACCEPT  tcp   0.0.0.0/0       tcp dpt:443
4         0  ACCEPT  tcp   0.0.0.0/0       tcp dpt:80
6         0  ACCEPT  tcp   0.0.0.0/0       tcp dpt:443
8         0  ACCEPT  tcp   198.51.100.10   tcp dpt:22
13        0  DROP    all   192.0.2.44
14        0  DROP    all   192.0.2.170

Seven of the 17 rules sit at zero. Two of those are unremarkable: no matching traffic arrived in four days. The other five cannot change anything, no matter how much traffic arrives. They fall into three groups:

Duplicate allows (lines 4 and 6). At some point someone typed ufw allow 80 although ufw allow 80/tcp already existed. UFW accepts that without comment, because formally it’s a different rule (no protocol versus TCP). For TCP the first one always wins. The second is never reached.

An exception that’s already allowed (line 8). Restricting SSH to a specific address is a good idea. It just doesn’t help if SSH is already open to everyone further up. The specific rule has no effect, and worse, it gives anyone reading the rules the impression that SSH is restricted.

Blocks that come too late (lines 13 and 14). These are two IP blocks created with ufw deny from ..., without insert. They sit below the allow rules for 22, 80 and 443. If one of these addresses wanted to reach the website or SSH, lines 1 to 3 would wave it through before the block is ever consulted. On every other port the default policy would have rejected it anyway. So the block changes the outcome in no case at all. The zero counter only tells you that neither address tried another port since the last reload. Had one done so, the block would have matched, but the default policy would have done exactly the same thing anyway.

This is the most unpleasant kind of mistake, because it’s reassuring. ufw status neatly lists the block as DENY IN. Anyone checking whether the address is blocked sees: yes. The counter column says otherwise.

The fix is simple:

ufw delete deny from 192.0.2.44
ufw insert 1 deny from 192.0.2.44 comment 'blocked, reason: ...'

And as a habit: a few days after adding a block, look at the counters. A block with zero hits is either unnecessary or in the wrong place. Either way it needs another look.

One caveat if you try this: the counters reset to zero whenever UFW reloads, so after ufw reload, after any rule change and after a reboot. Our last reload was four days before the measurement. A rule on port 80 with zero hits after four days of production traffic is unambiguous. After four minutes it wouldn’t be.

The Docker Trap: Why UFW Doesn’t Protect Some Containers

We’ve already measured this in detail in two articles, so here’s just the summary. If you use Docker, you need to know it anyway.

Docker bypasses UFW. If a container publishes a port with the usual -p 8080:80 syntax, or "8080:80" in Compose, that port is reachable from the internet even though UFW never allowed it. Docker writes its own rules into the kernel’s FORWARD chain, and they apply before UFW is asked. ufw status shows none of this.

A firmly locked main gate while a small side door in the same wall stands open and a container crate slides through

We proved it with a throwaway container in Linux Server Setup and measured it again for Compose files in our Docker Compose Example. Both times: HTTP 200 from an unrelated server, although UFW denied the port.

The fix is to bind container ports to loopback:

ports:
  - "127.0.0.1:8080:80"   # local only, a reverse proxy forwards traffic

All containers on our server are bound this way. As a check for this article we tested again from a machine in Helsinki, one port that should be open and five that should be closed:

443   OPEN
5050  BLOCKED
9800  BLOCKED
3860  BLOCKED
631   BLOCKED
8443  BLOCKED

Ports 5050, 9800 and 3860 belong to services listening on all addresses, not bound to loopback. They’re blocked anyway, because they’re ordinary processes rather than Docker containers. That’s exactly where the line runs: for ordinary services UFW protects reliably. For Docker containers, only if they’re bound correctly.

Always test from outside. A curl from the server to its own public IP goes over the loopback interface and never passes through the firewall. That test always says “reachable” and proves nothing. A second server, a phone on mobile data or an online port scanner will do.

UFW vs. iptables vs. nftables vs. firewalld

A common question is whether to use UFW at all or go straight to something “proper”.

UFWiptables / nftables directlyfirewalld
Audiencesingle servers, beginner to expertspecial cases, routers, complex setupsRed Hat, Fedora, CentOS, Rocky
Syntaxufw allow 443/tcplong rule chainszones and services
IPv4 + IPv6both automaticallymaintained separately (iptables) or inet (nftables)both automatically
Persistencebuilt inyour jobbuilt in
PreinstalledUbuntueverywhere (as a kernel tool)RHEL family

UFW isn’t a beginner firewall you replace with something better later. The rules it generates are the same kernel rules you’d write by hand. For a single server with a few public services there’s no technical reason to switch to raw iptables. If you need more, you can add your own rules in /etc/ufw/before.rules without giving up UFW.

Don’t mix firewall tools. If you use UFW and also set iptables rules by hand, you have two places where rules come from, and ufw status only shows one of them. Fedora and RHEL systems ship with firewalld. Installing UFW there is possible but not sensible. We compared which distribution suits a server in Linux Server Distributions.

We do have one exception ourselves: fail2ban doesn’t write its bans through UFW but directly into its own nftables table (inet f2b-table). That’s deliberate and works, because fail2ban manages its own rules. You do need to know about it, though, or you’ll look for banned addresses in ufw status and not find them.

Is UFW Enough to Protect a Server?

No. UFW does exactly one job: it decides which ports are reachable from outside. That job matters, and without it our server would expose 21 services instead of three. On the ports that are open, though, UFW protects nothing. If you run SSH with passwords, you’re vulnerable despite the firewall. Same if you run an outdated web application on port 443.

A firewall is the outer layer. Behind it you need:

  • SSH with keys only, root password login disabled (What Is SSH?)
  • automatic security updates that actually take effect (see our finding in Linux Server Setup)
  • services bound to loopback and exposed only through a reverse proxy (nginx as a Reverse Proxy)
  • backups that have been restored at least once

Rented servers often have a provider firewall in front of the server as well. Hetzner, Netcup and most cloud providers offer one for free. It doesn’t replace UFW, but it complements it well: whatever it drops never reaches the server at all. What a VPS actually is and how it differs from a dedicated server is covered in What Is a VPS?.

Checklist: UFW Firewall Setup

In the order we’d do it:

# 1. Install (Debian) or verify (Ubuntu)
apt install ufw
grep IPV6 /etc/default/ufw          # must be "yes"

# 2. Default policies
ufw default deny incoming
ufw default allow outgoing

# 3. SSH FIRST
ufw allow OpenSSH                    # or: ufw limit 22/tcp comment 'SSH'

# 4. Public services, always with protocol and comment
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw allow 443/udp comment 'HTTP/3'  # only if your web server speaks HTTP/3

# 5. Enable, then reconnect in a SECOND terminal
ufw enable

# 6. Verify
ufw status verbose

After that, as routine maintenance:

  • Test from outside, never from the server itself.
  • Always add blocks with insert 1, otherwise they sit behind the allow rules.
  • Read the packet counters, not just ufw status: iptables -L ufw-user-input -v -n -x. Check any rule with zero hits.
  • Bind Docker ports to 127.0.0.1. Otherwise UFW doesn’t see them.
  • Treat the log as a sample. For volumes, use the counters.

What We Didn’t Measure

So the numbers are read correctly:

  • One server, one week. Which ports get hit depends on the provider’s network, the IP’s history and chance. On another server the top 10 will look different. The pattern (thousands of ports, databases and dev servers near the top) shows up everywhere, though.
  • The “one in eight” ratio comes from 15 minutes of measurement in the morning and roughly matches the long-term figure (closer to one in nine there, assuming the policy counter has run undisturbed since the reboot; we didn’t verify that separately). During heavy scanning the gap is larger, because the log limit of three per minute is fixed and the traffic isn’t.
  • The per-rule packet counters only run since the firewall was last loaded, four days in our case. For ports 80 and 443 that’s conclusive. For rarely used ports, such as a game server rule, it wouldn’t be.
  • We changed nothing on the firewall, not even the dead rules. Every command that would have changed something ran with --dry-run. Cleanup happens separately and deliberately, not on the side while writing an article.

Frequently Asked Questions

How do I set up a UFW firewall?

Install it (apt install ufw; it’s already there on Ubuntu), then set ufw default deny incoming and ufw default allow outgoing, allow SSH first (ufw allow OpenSSH), then the public services such as ufw allow 80/tcp and ufw allow 443/tcp, and finally run ufw enable. Afterwards reconnect in a second terminal to make sure SSH still works.

Is UFW active by default on Ubuntu?

No. UFW comes preinstalled on Ubuntu but inactive. ufw status reports Status: inactive right after installation. That’s deliberate: a firewall that switches itself on could cut the SSH connection to a remote server.

How do I open a port in UFW?

With ufw allow PORT/PROTOCOL, for example ufw allow 8080/tcp. Always include the protocol; without /tcp or /udp UFW opens both. To allow a single address only: ufw allow from 203.0.113.5 to any port 8080 proto tcp.

How do I delete a UFW rule?

Either get the number with ufw status numbered and run ufw delete NUMBER, or repeat the original command with delete in front, e.g. ufw delete allow 8080/tcp. The second way is safer because numbers shift after every deletion.

Why doesn’t my UFW block against an IP address work?

Because UFW applies the first matching rule, and ufw deny appends the block at the end. If an allow rule for port 80 or 443 comes first, the address gets through there before the block is ever consulted. Put blocks at the top with ufw insert 1 deny from ADDRESS. Whether a rule fires is shown by iptables -L ufw-user-input -v -n -x in the pkts column.

Does UFW protect Docker containers?

Not with the default configuration. Container ports published with -p 8080:80 are reachable from the internet even if UFW doesn’t allow them, because Docker inserts its rules ahead of UFW’s. The fix: bind ports to loopback (127.0.0.1:8080:80) and put a reverse proxy in front.

Why does the UFW log show blocks on ports that are open?

Because UFW drops packets that don’t belong to any known connection (state INVALID), and it does so before checking the port rules. These are usually stragglers from connections that were aborted or banned. On our server, only 4 of the 2,370 blocks on the open SSH port were real connection attempts.

Does the UFW log show every blocked packet?

No. The default log level low records at most about three packets per minute, with a short burst buffer. In our measurement roughly one dropped packet in eight was logged. For real volumes, read the packet counters: iptables -L INPUT -v -n -x.

Do I need UFW if my provider has a firewall?

Having both does no harm and often helps. The provider firewall keeps traffic away from the server, but it’s tied to a web console and doesn’t move with you if you migrate the server. UFW lives on the server itself and still protects you if someone accidentally loosens the provider rules. Together they’re more robust than either alone.

What’s the difference between UFW and firewalld?

Both are front ends for the same kernel packet filter. UFW is at home on Ubuntu and Debian and works with simple per-port rules. firewalld is the default on Fedora, RHEL and their derivatives and works with zones that network interfaces are assigned to. Use whatever your distribution ships with, and don’t mix the two.

Conclusion

A UFW firewall setup really is easy, and that’s its biggest strength. Six commands, and three services are reachable from the internet instead of 21. That alone justifies UFW on every server.

The actual work starts afterwards, though. In one week our firewall turned away tens of thousands of packets, and the same ruleset still contained two IP blocks that had never blocked anything. ufw status shows what you configured. The packet counters show what actually happens. If you rely only on the first, you see a tidy list and mistake it for a working firewall.

One look a month at iptables -L ufw-user-input -v -n -x is enough to catch it. Almost everyone skips that look. So did we, until we wrote this article.