Firewall and UFW
Least Privilege framed a default-deny firewall as the network-layer application of the same principle applied everywhere else in this module. This article makes that concrete: UFW (Uncomplicated Firewall), Ubuntu's standard front end for packet filtering, which lets you express "deny everything except what I explicitly need" in a handful of readable commands instead of a raw rule table.
What You Will Learn
By the end of this article you will be able to:
- explain what a default-deny policy means in practice and set one with UFW;
- allow and deny traffic by port, protocol, application profile, and source address;
- read
ufw status verboseand understand what each column means; - use rate limiting to blunt a brute-force attempt at the firewall itself;
- diagnose why a rule that looks correct isn't taking effect.
What UFW Actually Is
UFW isn't a separate packet-filtering engine — it's a management layer on top of the kernel's own netfilter framework (the same engine iptables and nftables talk to directly, covered in iptables and nftables). UFW exists because the raw syntax for either of those is verbose and easy to get subtly wrong; UFW trades some of their flexibility for commands that read like a policy statement.
A freshly installed UFW is inactive by default — enabling it without first confirming the rules you actually need is the single most common way to lock yourself out of a remote server, covered explicitly below.
Default Policy: Deny Incoming, Allow Outgoing
This is the default-deny principle from Least Privilege applied directly: nothing reaches a listening service on this host unless a rule explicitly permits it, while the host itself can still initiate outbound connections (to fetch packages, resolve DNS, and so on) without needing a rule for every possible destination. Restricting outbound traffic too is possible (default deny outgoing) but adds meaningful ongoing maintenance — most servers don't need it and it's easy to break routine operations like apt update by mistake.
Allowing and Denying Traffic
22/tcp— a single port and protocol;80,443/tcp— a comma-separated list of ports, one rule;from 203.0.113.0/24 to any port 5432— restrict a port to a specific source network, the correct approach for a database port that should never be reachable from the general internet, only from a known application subnet.
Confirm SSH access before enabling UFW on a remote server
Enabling UFW with a default-deny incoming policy and no rule permitting SSH cuts off the very session used to manage the server, with no way back in except console access through the hosting provider. Before running sudo ufw enable:
- check the current, inactive state first:
sudo ufw status; - explicitly allow the port the current SSH session is using:
sudo ufw allow 22/tcp(or the actual configured port, if it was changed — see SSH Hardening); - only then run
sudo ufw enable; - immediately test a new SSH connection in a second terminal, without closing the first;
- if the new connection fails, the still-open first session can run
sudo ufw disableto recover immediately.
Named application profiles are an alternative to remembering raw port numbers for common services:
ufw app list reads these definitions from /etc/ufw/applications.d/, populated automatically by packages like nginx and openssh-server when installed — a more self-documenting alternative to sudo ufw allow 80,443/tcp when the profile already exists.
Denying is the mirror of allowing, and rule order matters — the first matching rule wins:
With these two rules in this order, the specific SSH exception never actually takes effect, because the earlier, broader deny rule matches first for any traffic from that address, ports included. The exception needs to be listed before the broader deny to have any effect — verified with ufw status numbered, shown next.
Reading Status
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 80,443/tcp ALLOW IN Anywhere
[ 3] 5432 ALLOW IN 203.0.113.0/24
The numbers matter for removal — deleting by number, rather than reconstructing the exact original allow/deny syntax, is the reliable way to remove a specific rule:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
Logging: on (low) means denied packets are logged at a low level of detail to the kernel log — enough to see that something was blocked and from where, without the volume of full logging every allowed packet as well.
Rate Limiting
limit, used in place of allow, tracks connection attempts from a given address and automatically denies further attempts if more than roughly 6 connections arrive within 30 seconds — a coarse, firewall-level defense against a brute-force SSH scan, distinct from fail2ban's log-pattern-based approach covered next in this module. The two are complementary rather than redundant: limit reacts purely to connection rate, with no awareness of whether the attempts actually failed authentication, while fail2ban reads the authentication logs themselves and can therefore ban after a smaller number of genuinely failed logins, at the cost of needing the log pattern to match correctly.
IPv6
UFW manages IPv6 rules transparently as long as it's enabled for IPv6 at all, which is the default on a modern Ubuntu install:
With IPV6=yes, a plain sudo ufw allow 22/tcp creates matching rules for both address families — no separate IPv6-specific command is normally needed, though ufw status verbose on a dual-stack host is worth checking explicitly if a service is reachable over one address family but not the other.
Practical Scenario: Locking Down a Web Server
Problem. A fresh Ubuntu Server LTS instance runs nginx and is reachable over SSH. It needs to accept only HTTP/HTTPS and SSH, deny everything else inbound, and rate-limit SSH against brute-force scanning.
Check the starting state:
Set the default policy and the required rules, SSH first:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit 22/tcp
sudo ufw allow 'Nginx Full'
Enable, and verify from a second session before closing the first:
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
-- ------ ----
22/tcp LIMIT IN Anywhere
Nginx Full ALLOW IN Anywhere
Result. A new SSH session from a second terminal confirms access still works; a port scan against any other port from outside now receives no response at all, rather than a refused, since default-deny drops the packet rather than actively rejecting it.
Conclusion. The server now exposes exactly two services to the network — SSH (rate-limited) and web traffic — with everything else silently unreachable, matching the attack-surface reduction goal from Linux Security Fundamentals.
Common Mistakes
- Enabling UFW before confirming an SSH allow rule. Covered in detail above — the single most common way to lock yourself out of a remote server.
- Ordering a specific allow rule after a broader deny rule. UFW evaluates rules in order and stops at the first match; a narrow exception placed after a broad deny for the same traffic never takes effect.
- Assuming UFW alone stops an already-authenticated attacker. UFW controls what can reach a service; it does nothing once traffic is legitimately permitted to a port that's running a vulnerable application. It's one layer of defense in depth, not a complete one.
- Forgetting
ufw reloadorenableisn't needed after every change. Unlike some firewall front ends, UFW rule changes (allow/deny/delete) take effect immediately — there's no separate "apply" step, which occasionally surprises someone expecting to need one.
Exercises
- On a lab VM, set a default-deny incoming policy, allow only SSH, and verify with
ufw status numberedthat no other port responds from a second machine. - Add a rule allowing a specific port only from a single source IP, and explain what changes in
ufw status verbosecompared to allowing it from anywhere. - Create two rules — a broad deny from an address and a narrower allow for one port from that same address — in an order where the allow has no effect, then fix the ordering and confirm with
ufw status numbered. - Explain, in your own words, the difference between what
ufw limit 22/tcpprotects against and what fail2ban protects against.
Summary
UFW is a readable front end over the kernel's netfilter packet filter, built around expressing a default-deny incoming policy and then allowing exactly the ports and sources a server needs. Rule order determines which one wins on a match, limit adds coarse rate-based protection against scanning, and the most important operational habit is confirming an SSH allow rule before ever enabling the firewall on a remote box. The next article, iptables and nftables, looks at the lower-level tools UFW itself is built on, for situations that need more control than UFW's simplified syntax provides.