Network Hardening
Firewalls
Host firewalls with nftables and ufw: default deny, allow what you need.
Trimming unneeded services (see Reducing Exposed Services) shrinks your attack surface, but a firewall gives you a second, independent gate: even if a service starts listening unexpectedly, the firewall can still refuse connections to it. The goal is a default-deny policy for inbound traffic. Instead of enumerating everything to block, you drop everything and then allow the short list of ports you actually serve. That design fails safe: a mistake tends to block too much (which you notice immediately) rather than expose too much (which you might never notice). On Ubuntu the default frontend is ufw, and underneath it the kernel framework is nftables.
nftables concepts
nftables is the modern kernel packet-filtering framework, the successor to iptables. Three concepts matter:
| Concept | What it is |
|---|---|
| Table | A container for chains, scoped to a protocol family (inet covers both IPv4 and IPv6) |
| Chain | An ordered list of rules attached to a hook such as input, forward, or output, with a default policy |
| Rule | A match plus a verdict (accept, drop, reject) |
An inbound packet traverses the input chain top to bottom; the first matching rule’s verdict wins, and if nothing matches, the chain’s default policy applies. Default-deny means setting that policy to drop.
A minimal default-deny ruleset
Written directly in nftables, a host firewall that allows only SSH and established replies looks like this. Save it as /etc/nftables.conf.
sudo tee /etc/nftables.conf >/dev/null <<'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
tcp dport 22 ct state new accept
ip protocol icmp accept
}
chain forward { type filter hook forward priority 0; policy drop; }
chain output { type filter hook output priority 0; policy accept; }
}
EOF
sudo systemctl enable --now nftables
sudo nft list ruleset | head -8
Expected output:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
tcp dport 22 ct state new accept
The order is deliberate: accept loopback, accept replies to connections you initiated, drop garbage, then allow new SSH connections. Everything else falls through to policy drop.
ufw as a friendly frontend
Most of the time you do not hand-write nftables. ufw (Uncomplicated Firewall) is Ubuntu’s default frontend: you express intent in plain commands and it generates the underlying rules. This is the recommended path on Ubuntu.
# Default deny inbound, allow outbound, then permit SSH with rate limiting
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit 22/tcp
sudo ufw enable
sudo ufw status verbose
Expected output:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
-- ------ ----
22/tcp LIMIT Anywhere
22/tcp (v6) LIMIT Anywhere (v6)
ufw limit is a useful touch: it allows SSH but throttles a source address that opens too many connections in a short window, which blunts brute-force scans. Add other services as needed, for example sudo ufw allow 443/tcp for HTTPS.
Choosing between the two
| ufw | nftables directly | |
|---|---|---|
| Ease of use | High, plain intent commands | Lower, full syntax |
| Best for | Single hosts, common cases, Ubuntu defaults | Complex policy, NAT, fine-grained matching |
| Reversible | ufw disable / ufw reset | Edit and reload /etc/nftables.conf |
| Relationship | Generates rules in the kernel framework | Is the kernel framework |
On Ubuntu, firewalld is available but not the default; ufw is the expected tool. Drive ufw day to day and drop to nftables only when you need something ufw cannot express. Do not run two frontends managing the same rules at once.
Practical Guidance
- Set the inbound policy to deny by default and add explicit allow rules only for ports you actually serve; a firewall that starts from deny fails safe.
- On Ubuntu, prefer
ufwfor everyday work and reserve raw nftables for policy it cannot express, but never run two managers over the same ruleset. - Always allow loopback and established/related traffic before the deny takes effect, or you will break local sockets and reply packets.
- Use
ufw limit 22/tcp(or an nftables rate limit) to throttle SSH brute-force attempts at the firewall. - Enable the firewall while keeping a console or second session open, and test on a snapshot VM, since a wrong rule can lock you out of SSH.
- Treat the firewall as a second gate behind service reduction, not a substitute for it: fewer listeners means simpler, safer rules.