Firewall with UFW

Chapter 3 — Firewall with UFW

A firewall is the first thing that meets any incoming network connection. It decides, before any application sees the traffic, whether that connection should be allowed at all. Even though Cloudflare Tunnel handles your web traffic without opening inbound ports, a server-side firewall protects against attacks from within the local network, mis-configured services, and future changes to your setup. UFW — the Uncomplicated Firewall — provides a clean interface to iptables that's simple enough to set up correctly in minutes.

What this chapter covers: What a firewall does and why it matters even behind a Cloudflare Tunnel. UFW's default-deny-inbound policy. Checking what's listening before enabling. The safe setup order (SSH allow before enable — or you lock yourself out). Application profiles. Port-specific and source-specific rules. Rate limiting with ufw limit. Managing rules with numbered list, delete, and insert. UFW logging. IPv6 behaviour. The minimal ruleset for a Cloudflare Tunnel server. What to do if you lock yourself out.

Why a Firewall Matters Even with Cloudflare Tunnel

Without a firewall — all these attack surfaces exist: Internet (blocked by ISP) ──✗──▶ Your server Cloudflare Tunnel (outbound) ──✓──▶ localhost:80 (controlled) Local network (home devices) ──?──▶ Your server:22, :80, :3306, :anything Compromised home device ──?──▶ Your server (all ports open) Accidental service bind ──?──▶ 0.0.0.0:8080 (any network) With UFW (default deny inbound): Internet ──✗──▶ blocked at firewall Cloudflare Tunnel (outbound) ──✓──▶ outbound rules allow this Local network ──?──▶ only port 22 (SSH) allowed Compromised home device ──✗──▶ port 3306 (MySQL) denied — can't touch it Accidental service on :8080 ──✗──▶ denied — firewall catches it

The key insight: the Cloudflare Tunnel uses an outbound connection. UFW's default policy allows all outbound traffic, so the tunnel works perfectly with a strict inbound firewall. Web requests arrive from Cloudflare to localhost:80 — the loopback interface, which is treated separately from network interfaces. Meanwhile, the firewall blocks anything unexpected trying to reach the server from other machines on your home network.

  • MySQL (port 3306) is bound to 127.0.0.1 by default — good. But if it were ever misconfigured to 0.0.0.0, UFW would block access from outside the server.
  • New services you install often bind to all interfaces. UFW denies them until you explicitly allow them.
  • Defence in depth — the ISP blocks inbound, the router has its own firewall, and UFW adds a third layer on the server itself. Any one layer failing alone doesn't expose the server.

UFW Default Policies

Incoming — DENY (default)
All inbound connections are dropped unless you explicitly add an allow rule. This is the "default-deny" principle — start from zero and only open what you need.

Effect: any port not listed in your allow rules is silently blocked. Attackers scanning for open ports get no response.
Outgoing — ALLOW (default)
All outbound connections from the server are allowed by default. This means your server can: reach package repositories, contact Cloudflare for the tunnel, make DNS queries, send emails, etc.

Effect: you don't need to add rules for outbound traffic. The tunnel just works.
UFW also has a FORWARD policy (traffic being routed through the server to another destination). Default is DENY. Leave it that way unless you're setting up a router or VPN server.

Check What's Listening Before You Start

Before enabling UFW, know what services are running and which ports they use. This prevents accidentally blocking something you need, and helps you identify services that shouldn't be exposed.

# List all listening ports with the process that owns them $ sudo ss -tlnp State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",...)) LISTEN 0 511 *:80 *:* users:(("apache2",...)) LISTEN 0 70 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",...)) LISTEN 0 128 [::]:22 [::]:* users:(("sshd",...)) LISTEN 0 511 [::]:80 [::]:* users:(("apache2",...)) # Key observations from this output: # - sshd on 0.0.0.0:22 and [::]:22 — IPv4 and IPv6, all interfaces # - apache2 on *:80 — all interfaces (needs to accept tunnel connections from localhost) # - mysqld on 127.0.0.1:3306 — loopback ONLY, already safe # UFW rules to write: allow 22, optionally allow 80, deny everything else
Any service listening on 0.0.0.0 or * (not 127.0.0.1) is reachable from the network. Without UFW, that means reachable by anyone on your home network (or internet, if port forwarding exists). With UFW, they're blocked unless you add an allow rule. This is exactly the protection you want.

Safe Setup Order — SSH First, Then Enable

The cardinal rule: allow SSH before enabling UFW
If you enable UFW with its default-deny policy without first allowing SSH, your SSH session will be immediately cut off and you won't be able to reconnect. Recovery requires:
  1. Physical access to the server (plug in a keyboard and monitor)
  2. Or a remote console via your hosting provider (not applicable for a home server)
  3. Log in locally, run sudo ufw allow ssh && sudo ufw reload
Always allow SSH first, then enable. Never reverse this order.
Setup Walkthrough · Safe UFW Activation
Install UFW, add the SSH rule, then enable — in the correct order to avoid locking yourself out.
1
Install UFW (already included on Ubuntu/Debian, but verify).
$ sudo apt install ufw -y $ sudo ufw status Status: inactive ← UFW installed but not yet running — this is correct at this stage
2
Set default policies explicitly.
$ sudo ufw default deny incoming Default incoming policy changed to 'deny' $ sudo ufw default allow outgoing Default outgoing policy changed to 'allow'
3
Allow SSH — do this BEFORE enabling.
$ sudo ufw allow ssh # "ssh" is a UFW application profile — equivalent to "allow 22/tcp" Rules updated Rules updated (v6) # If your SSH runs on a non-standard port (e.g. 2222), use the port number: $ sudo ufw allow 2222/tcp
Not sure which port SSH is on? Check: sudo ss -tlnp | grep ssh
4
Enable UFW.
$ sudo ufw enable Command may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is now active and enabled on system startup
Your existing SSH session continues because the SSH allow rule was already in place.
5
Verify status — confirm SSH is allowed and everything else is denied.
$ sudo ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp ALLOW IN Anywhere 22/tcp (v6) ALLOW IN Anywhere (v6)
6
Open a new SSH session from a different terminal to verify you're not locked out.
$ ssh philip@webserver # from another terminal/window philip@webserver:~$ ← successful login — firewall is not blocking SSH
Only close the original session once this confirms SSH still works. If the new session fails, you still have the original session to diagnose and fix.
UFW rules persist across reboots automatically. Once enabled, the firewall will be active every time the server starts — no need to re-enable manually.

Which Ports to Open for Your Setup

The right ruleset depends on how traffic reaches your server. With Cloudflare Tunnel, web traffic arrives via localhost (the loopback interface) — not from the network — so ports 80 and 443 don't need to be open in UFW for the tunnel to work.

22 / tcp
SSH
Always open. Without this you can't administer the server remotely. Use ufw allow ssh or ufw allow 22/tcp.
80 / tcp
HTTP (web)
Not needed for tunnel. Cloudflare Tunnel delivers traffic to localhost — UFW doesn't see it. Only open if you need direct HTTP access from the local network.
443 / tcp
HTTPS (web)
Not needed for tunnel. Same as port 80 — tunnel bypasses UFW's network interface rules. Only open for direct HTTPS connections.
3306 / tcp
MySQL
Keep closed. MySQL should only be accessible from 127.0.0.1 (already configured by default). Never open this to the network.
everything else
All other ports
Denied by default. Any new service that accidentally binds to a public interface is blocked automatically until you explicitly allow it.
The minimal tunnel ruleset — just one rule: sudo ufw allow ssh. That's genuinely all you need if all public-facing traffic goes through Cloudflare Tunnel. Everything else is denied. The tunnel (outbound) is unaffected. This is one of the cleanest possible firewall configurations.

UFW Rule Syntax

sudo ufw allow ssh ← allow SSH using the named application profile
sudo ufw allow 80/tcp ← allow HTTP on TCP only (not UDP)
sudo ufw allow in from 192.168.1.0/24 to any port 80 ← allow HTTP from local network only
sudo ufw deny from 45.33.32.156 ← block a specific IP address entirely
sudo ufw limit ssh ← allow SSH but rate-limit: ban after 6 attempts in 30s
sudo ufw delete allow 80/tcp ← remove a rule
# Source-restricted rules — allow port 80 only from home network $ sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp # Replace 192.168.1.0/24 with your actual subnet (check: ip route | grep 'src') # Find your local subnet $ ip route | grep proto 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 # Subnet is 192.168.1.0/24 # Allow HTTP and HTTPS from local network only (useful for internal testing) $ sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp $ sudo ufw allow from 192.168.1.0/24 to any port 443 proto tcp

Application Profiles

UFW ships with named application profiles for common services — shortcuts that allow the right ports without needing to remember numbers.

# List all available application profiles $ sudo ufw app list Available applications: Apache Apache Full Apache Secure OpenSSH # What does each Apache profile open? $ sudo ufw app info 'Apache Full' Profile: Apache Full Title: Web Server (HTTP,HTTPS) Description: Apache V2 is the next generation of the omnipresent Apache web server. Ports: 80,443/tcp $ sudo ufw app info 'OpenSSH' Ports: 22/tcp # The three Apache profiles: # Apache → port 80 only (HTTP) # Apache Secure → port 443 only (HTTPS) # Apache Full → ports 80 and 443 (both)
For a Cloudflare Tunnel setup, avoid ufw allow 'Apache Full'. Opening ports 80 and 443 to all sources is unnecessary — the tunnel delivers traffic via localhost. At most, allow those ports restricted to your local subnet. The only externally-visible benefit is that attackers can see your server has something running on 80/443, which is information you'd rather not give away.

Rate Limiting with ufw limit

ufw limit allows connections to a port but automatically blocks an IP that makes 6 or more connection attempts within 30 seconds. It's a quick defence against SSH brute-force attacks without the full weight of fail2ban (covered in Chapter 4).

# Replace "allow ssh" with "limit ssh" for rate-limited SSH $ sudo ufw delete allow ssh # remove the plain allow rule first $ sudo ufw limit ssh # add the rate-limited version # Verify — should now show LIMIT IN instead of ALLOW IN $ sudo ufw status To Action From -- ------ ---- 22/tcp LIMIT IN Anywhere 22/tcp (v6) LIMIT IN Anywhere (v6)
ufw limit vs fail2ban: ufw limit is simple — fixed threshold (6 attempts / 30 seconds), IPv4 and IPv6, built into UFW. fail2ban (Chapter 4) is more powerful — configurable thresholds, persistent bans, monitors log files, and covers Apache and other services too. Use both: limit for quick UFW-level protection now, fail2ban for deeper coverage later.

Managing Rules

View rules with numbers

$ sudo ufw status numbered Status: active To Action From -- ------ ---- [ 1] 22/tcp LIMIT IN Anywhere [ 2] 80/tcp ALLOW IN 192.168.1.0/24 [ 3] 22/tcp (v6) LIMIT IN Anywhere (v6) [ 4] 80/tcp (v6) ALLOW IN Anywhere (v6)

Delete a rule by number

# Delete rule number 4 (the IPv6 port 80 allow in the example above) $ sudo ufw delete 4 Deleting: allow in on any port 80 proto tcp from Anywhere (v6) Proceed with operation (y|n)? y Rule deleted (v6) # Or delete by specifying the rule itself $ sudo ufw delete allow 80/tcp

Block a specific IP address

# Block all traffic from a specific IP (e.g. a persistent scanner you spotted in logs) $ sudo ufw deny from 45.33.32.156 Rule added # Block a whole subnet $ sudo ufw deny from 45.33.32.0/24 # Deny rules should be inserted BEFORE allow rules to take effect # Insert at position 1 (top of list) to guarantee it's checked first $ sudo ufw insert 1 deny from 45.33.32.156

Reset UFW (nuclear option — removes all rules)

$ sudo ufw reset Resetting all rules to installed defaults. This may disrupt existing ssh connections. Proceed with operation (y|n)? y # This disables UFW and removes all custom rules. # Use when you want to start completely fresh. # Remember to re-add SSH allow rule before re-enabling!

UFW Logging

# Enable logging (off by default in some installations) $ sudo ufw logging on # Logging levels: off / low / medium / high / full # "low" logs blocked packets; "medium" also logs allowed. Start with low. $ sudo ufw logging medium # UFW logs go to /var/log/ufw.log (and also syslog) $ sudo tail -f /var/log/ufw.log Jun 15 03:22:14 webserver kernel: [UFW BLOCK] IN=eth0 OUT= MAC=... SRC=45.33.32.156 DST=192.168.1.100 PROTO=TCP DPT=3306 # UFW BLOCK = connection was denied. # SRC = attacking IP, DPT = destination port (3306 = MySQL scan — blocked) Jun 15 03:25:01 webserver kernel: [UFW ALLOW] IN=eth0 OUT= MAC=... SRC=192.168.1.5 DST=192.168.1.100 PROTO=TCP DPT=22 # UFW ALLOW = connection permitted (your SSH from another device on the LAN)
What to look for in UFW logs: Repeated BLOCK entries on port 22 from diverse IPs = SSH brute force scan (this is normal and expected on any internet-connected server — fail2ban in Chapter 4 handles this automatically). Repeated BLOCK entries on port 3306 = MySQL scan (blocked correctly). Any UFW BLOCK on a port you didn't know was being probed is worth noting.

IPv6 Support

UFW handles IPv4 and IPv6 with the same rules by default — when you allow ssh, UFW automatically creates both an IPv4 and IPv6 rule. Verify this is enabled:

$ sudo cat /etc/default/ufw | grep IPV6 IPV6=yes ← this should be yes # If IPV6=no, edit the file and restart UFW $ sudo nano /etc/default/ufw # set IPV6=yes $ sudo ufw disable && sudo ufw enable

With IPV6=yes, every rule you add automatically gets an IPv6 counterpart. When you run sudo ufw status, you'll see rules listed twice — once for IPv4 and once as (v6) for IPv6. This is correct behaviour.

The Complete Ruleset for This Setup

# ── Complete UFW setup for a Cloudflare Tunnel home server ──────── # 1. Set default policies $ sudo ufw default deny incoming $ sudo ufw default allow outgoing # 2. Allow SSH with rate limiting $ sudo ufw limit ssh # 3. Allow HTTP/HTTPS from local network only (for internal access/testing) # Skip this if you only ever access the site via the Cloudflare Tunnel $ sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp $ sudo ufw allow from 192.168.1.0/24 to any port 443 proto tcp # 4. Enable the firewall $ sudo ufw enable # 5. Verify $ sudo ufw status verbose Status: active Default: deny (incoming), allow (outgoing), disabled (routed) To Action From -- ------ ---- 22/tcp LIMIT IN Anywhere 80/tcp ALLOW IN 192.168.1.0/24 443/tcp ALLOW IN 192.168.1.0/24 22/tcp (v6) LIMIT IN Anywhere (v6)
Test the Cloudflare Tunnel still works after enabling UFW. Visit https://osztromok.com from outside — if it loads, the tunnel is unaffected (as expected, since it uses outbound connections). Then try SSHing from another machine on the network — should succeed on port 22.

Troubleshooting

SSH dropped immediately after ufw enable — locked out
You enabled UFW without first allowing SSH. Recovery options: (1) Physical access — connect a keyboard and monitor to the server, log in locally, run sudo ufw allow ssh && sudo ufw reload. (2) If you have another active SSH session open in a different terminal, use it immediately — run the allow command before it times out. (3) If the server is running in a VM with a hypervisor console, use the console to log in and fix the rule. Going forward: always allow SSH first, enable second.
Cloudflare Tunnel stops working after enabling UFW
Unexpected — the tunnel uses outbound connections which UFW's default allow outgoing policy covers. Check: (1) Did you accidentally change the outgoing default to deny? Run sudo ufw status verbose and look for Default: ... allow (outgoing). (2) Is cloudflared running? sudo systemctl status cloudflared. (3) The tunnel connects to Cloudflare's servers on port 443 (outbound) — this should always be allowed by the default outgoing policy.
Local network access to the website is blocked after enabling UFW
You haven't added the local network allow rule. Run: ip route | grep proto kernel to get your subnet (e.g. 192.168.1.0/24), then: sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp and sudo ufw allow from 192.168.1.0/24 to any port 443 proto tcp. Verify with sudo ufw status that the rules were added, then reload: sudo ufw reload.
ufw status shows "inactive" even though I ran ufw enable
UFW requires a reboot to activate on some systems, or the enable command failed. Check: sudo systemctl status ufw — if it shows failed, there may be a conflict with iptables. Run sudo ufw enable again and look for error output. Also check: sudo iptables -L — if iptables rules already exist from another firewall tool (firewalld, nftables), they may conflict.
My IP got rate-limited by ufw limit and I can't SSH in
ufw limit bans your IP for 30 seconds after 6 connection attempts. Wait a minute and try again — the ban is temporary. If you keep triggering it, you may have an SSH client that retries aggressively. Switch to ufw allow ssh while debugging, then switch back to ufw limit ssh once the client behaviour is corrected. fail2ban (Chapter 4) has a whitelist mechanism for trusted IPs that UFW's built-in rate limiting lacks.

Quick Reference — Chapter 3

CommandPurpose
sudo ufw status verboseShow active rules, default policies, and logging level
sudo ufw status numberedShow rules with numbers — needed for targeted deletion
sudo ufw default deny incomingBlock all inbound by default — set before enabling
sudo ufw default allow outgoingAllow all outbound — required for tunnel, DNS, apt updates
sudo ufw allow sshAllow SSH — must run BEFORE ufw enable
sudo ufw limit sshAllow SSH with rate limiting (6 attempts / 30 seconds)
sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcpAllow port 80 from local network only
sudo ufw deny from IPBlock all traffic from a specific IP address
sudo ufw insert 1 deny from IPInsert deny rule at top of list (evaluated first)
sudo ufw delete NUMBERDelete rule by number (from ufw status numbered)
sudo ufw logging mediumEnable medium logging — blocked and allowed packets
sudo ufw reloadReload rules without disabling — safe to run while active
sudo ufw resetWipe all rules and disable — use with caution
sudo ufw app listList available named application profiles
Port / ServiceRule for Cloudflare Tunnel setup
22/tcp (SSH)ufw limit ssh — always open, rate-limited
80/tcp (HTTP)Optional: allow from local subnet only — not needed for tunnel
443/tcp (HTTPS)Optional: allow from local subnet only — not needed for tunnel
3306/tcp (MySQL)Never open — keep bound to 127.0.0.1 only
Everything elseDenied by default — don't open unless you have a specific reason