Skip to content
elephantoo

Firewalls with ufw

Lesson 29 of 31 12 min read

Default-deny policies, allowing services and ports, rate limiting, and firewalld on Fedora/RHEL.


A firewall decides which network traffic may enter (and leave) a machine. On a server, the safest policy is simple: deny everything incoming, then allow only the services you actually run, typically SSH, HTTP and HTTPS. Under the hood Linux uses the kernel's netfilter framework (managed through nftables or the older iptables), but you'll rarely write those rules by hand. On Ubuntu you use ufw (Uncomplicated Firewall), and on Fedora/RHEL firewalld.

💡 These commands need root and a real (virtual) machine. They won't work inside most containers. Practise on a throwaway VM or cloud server, and keep a second SSH session or the provider's web console open in case you lock yourself out.

Why bother?#

Run sudo ss -tlnp on a fresh server and you'll often see more listening services than you expected: a database, a cache, an admin panel, something a package started. A firewall makes sure that only the ones you choose are reachable from outside, even if a service is misconfigured to listen on 0.0.0.0. It's a core layer of defence in depth.

ufw on Ubuntu and Debian#

ufw ships with Ubuntu but is inactive by default. On Debian, install it with sudo apt install ufw.

A safe first setup

Terminal
sudo ufw status                    # Status: inactive
sudo ufw default deny incoming     # block all incoming by default
sudo ufw default allow outgoing    # allow the server to reach out (updates, APIs)
sudo ufw allow OpenSSH             # FIRST: keep SSH open (or: sudo ufw allow 22/tcp)
sudo ufw allow 80/tcp              # HTTP
sudo ufw allow 443/tcp             # HTTPS
sudo ufw enable
Output
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
Terminal
sudo ufw status verbose
Output
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
22/tcp (OpenSSH (v6))      ALLOW IN    Anywhere (v6)
80/tcp (v6)                ALLOW IN    Anywhere (v6)
443/tcp (v6)               ALLOW IN    Anywhere (v6)

ufw creates IPv6 rules automatically (controlled by IPV6=yes in /etc/default/ufw).

Allowing traffic

Terminal
sudo ufw allow 8080/tcp                          # a port and protocol
sudo ufw allow 6000:6010/tcp                     # a port range
sudo ufw allow from 203.0.113.50                 # everything from one IP (e.g. your office)
sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp   # PostgreSQL from the private network only
sudo ufw allow in on eth1 to any port 3306       # only on a specific interface
sudo ufw deny from 198.51.100.23                 # block an abusive IP
sudo ufw allow 'Nginx Full'                      # an application profile (ports 80 and 443)
sudo ufw app list                                # profiles installed by packages

💡 Restrict databases and admin tools by source. MySQL (3306), PostgreSQL (5432), Redis (6379) and Elasticsearch (9200) should almost never be open to Anywhere. Allow them only from the app servers' IPs or private network, or keep them on 127.0.0.1.

Rate limiting SSH

Terminal
sudo ufw limit OpenSSH

limit allows the connection but denies an IP that attempts 6 or more connections within 30 seconds, which slows down brute-force bots. It shows as LIMIT IN in the status.

Deleting and inserting rules

Terminal
sudo ufw status numbered
Output
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere
[ 2] 80/tcp                     ALLOW IN    Anywhere
[ 3] 443/tcp                    ALLOW IN    Anywhere
[ 4] 8080/tcp                   ALLOW IN    Anywhere
[ 5] OpenSSH (v6)               ALLOW IN    Anywhere (v6)
...
Terminal
sudo ufw delete 4                        # by number (check numbers again after each delete!)
sudo ufw delete allow 8080/tcp           # or by repeating the rule
sudo ufw insert 1 deny from 198.51.100.23   # put a rule at the top (rules match in order)

Rules are evaluated top to bottom and the first match wins. That's why a deny for one IP must come before a broad allow.

Other useful commands

Terminal
sudo ufw disable           # turn the firewall off (rules are kept)
sudo ufw reset             # delete all rules and disable (careful!)
sudo ufw logging on        # log blocked packets to /var/log/ufw.log
sudo ufw show added        # rules you added, as commands

The Docker gotcha

Docker inserts its own iptables rules for published ports (docker run -p 8080:80), and they are processed before ufw's rules. A container port published on 0.0.0.0 is reachable from the internet even if ufw denies it. Fixes:

  • publish on localhost only: -p 127.0.0.1:8080:80, and put a reverse proxy in front;
  • or configure the DOCKER-USER chain, or use a cloud firewall in front of the server.

firewalld on Fedora, RHEL, Rocky and AlmaLinux#

firewalld is enabled by default on Fedora and RHEL-family systems. It organises rules into zones (public, internal, trusted, ...), and each network interface belongs to one. Changes are runtime-only unless you add --permanent:

Terminal
sudo firewall-cmd --state
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all                          # the default zone's settings
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --permanent --remove-port=8080/tcp
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port port="5432" protocol="tcp" accept'
sudo firewall-cmd --reload                            # apply permanent rules
sudo firewall-cmd --get-services                      # known service names
Output
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: cockpit dhcpv6-client http https ssh
  ports:
  protocols:
  forward: yes
  masquerade: no
  ...

(That's the output of --list-all, abridged.) Don't run ufw and firewalld together; pick the one your distribution uses.

Cloud firewalls#

Cloud providers have their own network-level firewalls: AWS security groups, GCP VPC firewall rules, Azure NSGs, and DigitalOcean/Hetzner cloud firewalls. Traffic must pass both the cloud firewall and the host firewall. Use them together: the cloud firewall as the outer wall, ufw/firewalld as the inner one. When a port "won't open", check both.

Testing your firewall#

From another machine (not the server itself, since local traffic isn't filtered the same way):

Terminal
nc -zv server.example.com 22       # should succeed
nc -zv server.example.com 3306     # should time out if blocked
nmap -Pn server.example.com        # scan common ports (only scan machines you own!)

On the server, sudo ss -tlnp shows what's listening, and sudo ufw status shows what's allowed. The difference between the two lists is what your firewall is protecting.

Common mistakes#

  • Enabling the firewall before allowing SSH on a remote server, and getting locked out. Recover through the provider's web console.
  • Opening database ports to Anywhere. Restrict them by source IP or subnet.
  • Forgetting IPv6. If the server has an IPv6 address, rules must cover it (ufw does this by default).
  • Assuming ufw protects Docker containers. Published ports bypass it.
  • Deleting rules by number twice in a row. The numbers shift after each delete.
  • firewalld changes without --permanent, which disappear on the next reload or reboot.

What's next#

A firewall controls who can reach your services. Next, learn to keep an eye on how those services are performing, with performance monitoring: CPU, memory, disk I/O and network bottlenecks.

Check your understanding

Quick quiz

0/3 answered
  1. 1.You're configuring ufw on a remote server over SSH. What must you do BEFORE sudo ufw enable?

  2. 2.What does sudo ufw limit 22/tcp do?

  3. 3.On Fedora/RHEL with firewalld, how do you make a rule survive a reload or reboot?

Finished reading?

Mark this lesson complete to track your progress.