Introduction

Most Linux servers ship with a perfectly capable firewall framework and almost nobody configured correctly — or configured at all. UFW, the Uncomplicated Firewall, exists to close that gap. It wraps iptables (and on newer Ubuntu releases, nftables) in a command-line interface that a human being can actually remember without keeping a cheat sheet open in a second terminal window.

This guide covers the practical mechanics of UFW administration: the difference between allow and limit, how to scope rules to specific network interfaces and traffic directions, and how to build a coherent rule set for a real workload — a web server accepting HTTP and HTTPS, with SSH access rate-limited to a known-good home address. Every rule is covered for both IPv4 and IPv6, because in 2026, running IPv4-only firewall rules on a dual-stack host is a configuration error masquerading as a firewall.

By the end of this guide you’ll have a working firewall rule set that:

  • Defaults to denying all inbound traffic
  • Accepts HTTP and HTTPS on the public interface
  • Rate-limits SSH connections, with an allow from your home IP
  • Works identically for IPv4 and IPv6

Prerequisites:

  • Ubuntu 20.04 LTS or later (22.04, 24.04, or 24.10 all work identically for this guide)
  • sudo access on the machine
  • A basic familiarity with networking concepts (ports, protocols, interfaces)
  • If you’re hardening a remote machine over SSH: add your SSH rule before enabling UFW, or you will lock yourself out

How UFW Works

UFW sits on top of iptables or nftables depending on your Ubuntu version, acting as a translation layer between human-readable rules and the kernel’s packet-filtering tables. When you run sudo ufw allow 80/tcp, UFW generates the underlying iptables rules for you and stores them so they persist across reboots.

It ships with Ubuntu by default but is inactive until you switch it on. You can verify its current state at any time:

sudo ufw status verbose

If it’s inactive, you’ll see Status: inactive. If it’s running, you’ll see a numbered list of rules currently in effect.

To enable and disable UFW:

sudo ufw enable
sudo ufw disable

Enabling UFW starts enforcing rules immediately. Disabling it drops all firewall enforcement until you re-enable it — not a state you want on a production machine.


IPv6: Make Sure It’s On

Before writing a single rule, confirm UFW is configured to handle IPv6 traffic. Open /etc/default/ufw and check the IPV6 setting:

grep IPV6 /etc/default/ufw

You should see:

IPV6=yes

If it reads IPV6=no, change it to yes:

sudo nano /etc/default/ufw

Find the line and update it:

IPV6=yes

Then reload UFW to apply the change:

sudo ufw disable
sudo ufw enable

What this does: With IPV6=yes, UFW automatically creates matching rules in both the filter table (for IPv4) and the filter6 table (for IPv6) whenever you add a rule. You write the rule once; UFW handles both protocol families. If you leave this set to no, every rule you add applies only to IPv4, and your IPv6 interface is effectively unguarded.

You can verify that both protocol stacks are covered by looking at the status output — rules with (v6) appended are the IPv6 counterparts:

sudo ufw status verbose
To                         Action      From
--                         ------      ----
22/tcp                     LIMIT IN    Anywhere
22/tcp (v6)                LIMIT IN    Anywhere (v6)
80/tcp                     ALLOW IN    Anywhere
80/tcp (v6)                ALLOW IN    Anywhere (v6)

Setting Default Policies

UFW’s default policies determine what happens to traffic that doesn’t match any explicit rule. The secure baseline is to deny everything inbound and allow everything outbound:

sudo ufw default deny incoming
sudo ufw default allow outgoing

What this does: deny incoming means any connection attempt that doesn’t match an explicit allow or limit rule is silently dropped. allow outgoing means the machine can initiate connections to the outside world without restriction. This is the right starting point for a server — you enumerate what you accept, everything else is dropped.

If you need to restrict outbound traffic as well (useful for machines that should only talk to specific destinations), you can set deny outgoing and then explicitly allow the destinations you need. That’s a more advanced posture and outside the scope of this guide, but the same rule syntax applies in both directions.


Allow vs. Limit: Understanding the Difference

UFW has two primary actions for permitting traffic: allow and limit. They both let connections through, but they behave very differently under load.

allow

allow grants access unconditionally. Any source IP, any number of times, any rate. Use it for services where you genuinely don’t care about the source — a public web server is the clearest example. Legitimate users, bots, crawlers, and the occasional vulnerability scanner all get in equally.

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

limit

limit grants access, but with a circuit breaker: if a single source IP initiates six or more connections within a 30-second window, UFW blocks that source for the remainder of that window. Once the window resets, connections are allowed again — unless the pattern repeats.

sudo ufw limit 22/tcp

What this does: For a legitimate user who mistypes a password once or twice, nothing changes — you never hit six attempts in thirty seconds by accident. For a brute-force bot firing off dozens of authentication attempts per second, the door closes fast. UFW’s limit action translates to iptables’ hashlimit module under the hood, tracking per-source-IP connection rates.

The 6-in-30 threshold is fixed in UFW’s command interface. If you need finer-grained rate limiting (per-minute rates, progressive backoff, IP reputation), you’re looking at fail2ban or custom iptables rules — UFW’s limit is a blunt but effective first line.

limit applies equally to IPv4 and IPv6 sources. A bot hammering your SSH port from a 2001:db8::/32 address gets rate-limited the same way as one coming from 203.0.113.0/24.


Specifying Rules by Interface

By default, a UFW rule applies to all network interfaces on the machine. A rule like sudo ufw allow 80/tcp will accept HTTP traffic on your public Ethernet interface, your loopback interface, and any VPN tunnel you have running — all of it.

When you need more surgical control, you can scope a rule to a specific interface using the in on and out on qualifiers.

Discovering Your Interface Names

First, find out what your interfaces are called. On Ubuntu 20.04 and later, the default naming convention is enp3s0, ens3, eth0, or similar:

ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> ...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> ...

In the examples below, eth0 is the public-facing interface and eth1 is a private internal interface. Substitute your actual interface names.

Traffic Direction: in on vs. out on

The direction qualifiers tell UFW which way the traffic is moving relative to the interface:

  • in on <interface> — traffic arriving on this interface (inbound). This is the direction you care about most for a server accepting connections.
  • out on <interface> — traffic leaving through this interface (outbound). Useful for restricting what your machine can reach from a given interface.

If you omit the direction, UFW defaults to in.

Interface-Scoped Allow Rules

Allow HTTP only on the public interface:

sudo ufw allow in on eth0 to any port 80 proto tcp
sudo ufw allow in on eth0 to any port 443 proto tcp

Allow PostgreSQL only on the private interface (never expose a database port on the public interface):

sudo ufw allow in on eth1 to any port 5432 proto tcp

Allow DNS responses coming back in on the private interface:

sudo ufw allow in on eth1 to any port 53

Restrict outbound traffic to leave only through the private interface:

sudo ufw allow out on eth1 to 10.0.0.0/8

What this does: Each of these rules narrows the scope of the permission. Without the in on eth0 qualifier, ufw allow 80/tcp would accept HTTP on every interface — including eth1, which you might not want exposed to internal traffic. Interface scoping is particularly important on machines with multiple NICs, VPN tunnels, or container networking bridges.

IPv6 and Interface Rules

UFW applies interface-scoped rules to both IPv4 and IPv6 traffic on that interface automatically. A rule like:

sudo ufw allow in on eth0 to any port 443 proto tcp

Generates both an IPv4 rule matching traffic arriving on eth0 bound for port 443 AND an IPv6 rule doing the same. You can verify by inspecting the numbered rule list:

sudo ufw status numbered
     To                         Action      From
     --                         ------      ----
[ 1] 443/tcp on eth0            ALLOW IN    Anywhere
[ 2] 443/tcp (v6) on eth0       ALLOW IN    Anywhere (v6)

Allowing Traffic from a Specific Source

Rules can be further scoped to a source address — useful when you want to allow a service only from a known IP rather than the entire internet.

IPv4 Source

Allow SSH only from a specific IPv4 address (your home IP):

sudo ufw allow from 203.0.113.42 to any port 22 proto tcp

Allow SSH from an entire IPv4 subnet (your home network, if you use a dynamic home IP on a stable /24):

sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp

IPv6 Source

The same syntax works directly with IPv6 addresses and prefixes:

sudo ufw allow from 2001:db8::1 to any port 22 proto tcp

Allow SSH from an entire IPv6 prefix:

sudo ufw allow from 2001:db8:abcd::/48 to any port 22 proto tcp

What this does: These rules create a source-specific allow entry. Any connection to port 22 that does not originate from the specified address or prefix will be handled by whatever other rules are in place — if SSH is also covered by a limit rule, connections from unknown sources are rate-limited. If SSH has no other rule, connections from unknown sources are denied by the default policy.

You can combine source filtering with interface scoping:

sudo ufw allow in on eth0 from 203.0.113.42 to any port 22 proto tcp

This allows SSH only when it arrives on eth0 AND from 203.0.113.42. Both conditions must be true.


Building a Web Server Rule Set

A typical web server needs to accept HTTP and HTTPS from anyone. Here’s a minimal, interface-scoped configuration:

# Allow HTTP on the public interface
sudo ufw allow in on eth0 to any port 80 proto tcp
 
# Allow HTTPS on the public interface
sudo ufw allow in on eth0 to any port 443 proto tcp

UFW also understands named services from /etc/services, so these are equivalent:

sudo ufw allow in on eth0 http
sudo ufw allow in on eth0 https

If you want to allow both HTTP and HTTPS in a single rule, you can specify a port range or use the application profile shorthand. UFW ships with several built-in application profiles — check what’s available:

sudo ufw app list

Common ones include Apache, Apache Full, Apache Secure, Nginx, Nginx Full, and Nginx HTTPS. The Full profiles typically cover both HTTP (80) and HTTPS (443):

sudo ufw allow in on eth0 "Nginx Full"

What this does: The application profiles are defined in /etc/ufw/applications.d/ and group related ports under a friendly name. Using them makes your rule set easier to read and audit at a glance — sudo ufw status will show Nginx Full rather than a list of port numbers. You can inspect any profile’s contents:

sudo ufw app info "Nginx Full"
Profile: Nginx Full
Title: Web Server (Nginx, HTTP + HTTPS)
Description: This profile opens both 80 and 443.

Ports:
  80,443/tcp

For the IPv6 side: because IPV6=yes is set, every one of these rules automatically has a (v6) counterpart created. Your web server accepts https:// requests over both tcp/443 on 0.0.0.0 and tcp/443 on :: without any additional commands.


Hardening SSH Access

SSH is the most-scanned port on the internet. Leaving it open to the world with only a password (or even a key) standing between you and an attacker is leaving a lot to chance. The right approach is layered:

  1. Rate-limit SSH globally to slow brute-force attempts
  2. Explicitly allow your home IP without rate-limiting
  3. Optionally, block everything else from reaching SSH entirely

Step 1: Rate-Limit SSH from Anywhere

sudo ufw limit in on eth0 to any port 22 proto tcp

This applies the 6-in-30 rate limit to all SSH connections arriving on your public interface, regardless of source. A bot firing 60 attempts per second gets shut down after the sixth in the first 30-second window.

Both IPv4 (tcp/22) and IPv6 (tcp6/22) are covered automatically.

Step 2: Allow Your Home IP Explicitly

If your home has a static IPv4 address:

sudo ufw allow in on eth0 from 203.0.113.42 to any port 22 proto tcp

If you also have a static IPv6 prefix at home:

sudo ufw allow in on eth0 from 2001:db8:1234::1 to any port 22 proto tcp

A note on rule ordering: UFW processes rules in the order they appear in sudo ufw status numbered. A more-specific allow rule for your home IP should generally appear before the broader limit rule, but in practice UFW’s limit action is smart enough that a legitimate single-session connection from your home IP will never trigger the rate limiter — six connections in thirty seconds is not normal SSH usage. The explicit allow from rule provides a belt-and-suspenders guarantee that your home IP is always permitted regardless of what the rate limiter decides.

To verify the order of your rules:

sudo ufw status numbered
     To                         Action      From
     --                         ------      ----
[ 1] 80/tcp on eth0             ALLOW IN    Anywhere
[ 2] 80/tcp (v6) on eth0        ALLOW IN    Anywhere (v6)
[ 3] 443/tcp on eth0            ALLOW IN    Anywhere
[ 4] 443/tcp (v6) on eth0       ALLOW IN    Anywhere (v6)
[ 5] 22/tcp on eth0             ALLOW IN    203.0.113.42
[ 6] 22/tcp on eth0             ALLOW IN    2001:db8:1234::1
[ 7] 22/tcp on eth0             LIMIT IN    Anywhere
[ 8] 22/tcp (v6) on eth0        LIMIT IN    Anywhere (v6)

If rules are out of order, you can delete and re-add them, or use ufw insert to place a rule at a specific position:

sudo ufw insert 1 allow in on eth0 from 203.0.113.42 to any port 22 proto tcp

This inserts the rule at position 1, pushing everything else down.


Managing Rules: Delete, Reset, and Review

Viewing Rules

sudo ufw status verbose

Adds protocol, interface, and direction detail to the output. Use numbered when you need to delete by position:

sudo ufw status numbered

Deleting Rules

Delete by number (safest for complex rule sets):

sudo ufw delete 7

Delete by rule specification (UFW will find and remove the matching rule):

sudo ufw delete allow in on eth0 to any port 80 proto tcp

Dry-Run Before Adding

UFW will tell you what it would do before actually doing it using --dry-run:

sudo ufw --dry-run allow in on eth0 to any port 8080 proto tcp

This is useful for verifying that a rule matches what you expect, particularly when working with interface scoping or source filtering.

Resetting to a Clean Slate

If you’ve gotten into a tangle and want to start over:

sudo ufw reset

This disables UFW and removes all rules. It does not restore your defaults — you’ll need to re-run ufw default deny incoming and ufw default allow outgoing before re-enabling.


Putting It All Together

Here’s the complete rule sequence for a dual-stack Ubuntu web server with SSH access limited to a home IP:

# 1. Set defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
 
# 2. Confirm IPv6 is enabled in /etc/default/ufw (IPV6=yes)
 
# 3. Allow HTTP and HTTPS on the public interface
sudo ufw allow in on eth0 to any port 80 proto tcp
sudo ufw allow in on eth0 to any port 443 proto tcp
 
# 4. Allow SSH from home (IPv4)
sudo ufw allow in on eth0 from 203.0.113.42 to any port 22 proto tcp
 
# 5. Allow SSH from home (IPv6)
sudo ufw allow in on eth0 from 2001:db8:1234::1 to any port 22 proto tcp
 
# 6. Rate-limit SSH from everywhere else
sudo ufw limit in on eth0 to any port 22 proto tcp
 
# 7. Enable the firewall
sudo ufw enable
 
# 8. Verify
sudo ufw status verbose

Replace eth0 with your actual public interface name, 203.0.113.42 with your home IPv4 address, and 2001:db8:1234::1 with your home IPv6 address (or prefix).

The resulting status output should look like this:

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

To                         Action      From
--                         ------      ----
80/tcp on eth0             ALLOW IN    Anywhere
80/tcp (v6) on eth0        ALLOW IN    Anywhere (v6)
443/tcp on eth0            ALLOW IN    Anywhere
443/tcp (v6) on eth0       ALLOW IN    Anywhere (v6)
22/tcp on eth0             ALLOW IN    203.0.113.42
22/tcp on eth0             ALLOW IN    2001:db8:1234::1
22/tcp on eth0             LIMIT IN    Anywhere
22/tcp (v6) on eth0        LIMIT IN    Anywhere (v6)

Every rule written once, enforced for both protocol stacks. The web server is open to the world; SSH is rate-limited for the world and explicitly open for home — and “home” means both your IPv4 and IPv6 addresses.


Closing Thoughts

UFW earns its name. The syntax is human-scale in a way that raw iptables rules are not, and for most server workloads it covers everything you actually need. The two things that trip people up most: forgetting that IPV6=yes is required for dual-stack coverage, and not scoping rules to specific interfaces when the machine has more than one.

The rule set above is a floor, not a ceiling. If you’re running additional services — a VPN endpoint, a database, a monitoring agent — each gets its own allow or limit rule, scoped to the interface and source that legitimately needs it. Everything else stays denied by default.

For SSH, pair this with SSH key authentication and disable password authentication in sshd_config. UFW slows the bots down; keys shut the door. Belt and suspenders.

The next person who has to set this up shouldn’t have to start from zero. Now they don’t.