How-to

ufw Setup in Linux: Server Firewall and nftables Basics

Updated: · Skillaria.Top

Setting up ufw comes down to three steps: set the default policies (sudo ufw default deny incoming and allow outgoing), allow SSH and the required ports (sudo ufw allow 22/tcp, sudo ufw allow 443/tcp), then enable the firewall with sudo ufw enable. Rules are checked with sudo ufw status numbered, and under the hood ufw manages netfilter through iptables or nftables.

A Linux firewall decides which network packets to let into the server and which to drop. The kernel does this through the netfilter subsystem, and it is controlled by utilities: the low-level iptables and nftables, and the ufw front end (Uncomplicated Firewall). On most Ubuntu and Debian servers, ufw setup takes five minutes: block all incoming traffic, open SSH and the service ports, and turn the firewall on. Below are a step-by-step scenario, typical mistakes and the minimum of nftables you need to understand what happens under the hood.

How the firewall works in Linux

Packet filtering lives in the kernel, in netfilter. It provides interception points (hooks): incoming traffic to the host itself, outgoing traffic and transit traffic (forward). User-space utilities only write rules into the kernel for these points.

ToolLevelWhen to use
ufwfront end, simple syntaxa single server on Ubuntu/Debian, typical rules
firewalldfront end with zonesRHEL, Rocky, Fedora
nftables (nft)low-level, moderncomplex rules, NAT, address sets
iptableslow-level, being phased outold systems and scripts

Important: use one management tool. If you edit rules through ufw and at the same time by hand through iptables, they will overwrite each other and the picture becomes unpredictable.

Installing and checking ufw

On Ubuntu ufw is usually already installed but disabled. On Debian you install it separately:

$ sudo apt update
$ sudo apt install ufw
$ sudo ufw status
Status: inactive

The inactive status means ufw rules are not applied. It is too early to enable the firewall: first allow SSH, otherwise on a remote server you will cut off your own access.

Step-by-step ufw setup on a server

Step 1. Default policies

The default policy is what to do with a packet if no rule matches. The safe scheme for a server: incoming is denied, outgoing is allowed.

$ sudo ufw default deny incoming
Default incoming policy changed to 'deny'
$ sudo ufw default allow outgoing
Default outgoing policy changed to 'allow'

Replies to your own outgoing connections pass automatically: ufw tracks connection state (connection tracking), so there is no need to open ports for responses.

Step 2. Allow SSH

$ sudo ufw allow OpenSSH
Rules updated
Rules updated (v6)

OpenSSH is an application profile. If SSH listens on a non-standard port, allow that port explicitly: sudo ufw allow 2222/tcp. To check which port sshd actually listens on, run sudo ss -tlnp | grep sshd.

Step 3. Open the service ports

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

Always specify the protocol: without /tcp ufw opens the port for both TCP and UDP, and an extra open door is not needed.

Step 4. Enable the firewall

$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

ufw starts right away and will come up on every boot. Do not close your current SSH session: open a second one and make sure the connection works.

Step 5. Check the rules

$ sudo ufw status verbose
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 the IPv6 rules itself if /etc/default/ufw contains IPV6=yes (the default value).

How to open a port in ufw: all the syntax options

The ufw allow command understands a short and a full form. The short form suits typical cases; the full form is for when you need to specify a source, an interface or a destination address.

TaskCommand
TCP portsudo ufw allow 443/tcp
UDP portsudo ufw allow 51820/udp
Port rangesudo ufw allow 6000:6007/tcp
By service namesudo ufw allow https
From a single IPsudo ufw allow from 203.0.113.10
From a subnet to a portsudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp
On an interfacesudo ufw allow in on eth1 to any port 3306 proto tcp
Denysudo ufw deny 23/tcp
Reject with a responsesudo ufw reject 25/tcp

The difference between deny and reject: deny silently drops the packet, and the client waits until a timeout; reject sends a response saying the port is closed. For the outside world deny is usually the choice; for internal networks reject is more convenient, because clients see the error immediately.

Database access from a single server only

A common task: PostgreSQL or MySQL must not be visible from the internet, but an application on a neighbouring server must be able to connect.

$ sudo ufw allow from 10.0.0.5 to any port 5432 proto tcp
Rule added

Port 5432 stays closed to everyone else thanks to the deny incoming policy. It is also worth configuring the database itself to listen only on the internal address: the firewall and the service settings protect each other.

Application profiles

Packages can place descriptions of their ports in /etc/ufw/applications.d/. The list and the contents of the profiles:

$ sudo ufw app list
Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH
$ sudo ufw app info "Nginx Full"
Profile: Nginx Full
Title: Web Server (Nginx, HTTP + HTTPS)
Description: Small, but very powerful and efficient web server

Ports:
  80,443/tcp

To allow a profile: sudo ufw allow "Nginx Full". Put names with spaces in quotes. You can describe your own profile in INI format in the same directory and apply it with sudo ufw app update name.

How to delete a ufw rule

The most reliable way is by number:

$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere
[ 2] 80/tcp                     ALLOW IN    Anywhere
[ 3] 443/tcp                    ALLOW IN    Anywhere
[ 4] OpenSSH (v6)               ALLOW IN    Anywhere (v6)
[ 5] 80/tcp (v6)                ALLOW IN    Anywhere (v6)
[ 6] 443/tcp (v6)               ALLOW IN    Anywhere (v6)

$ sudo ufw delete 2
Deleting:
 allow 80/tcp
Proceed with operation (y|n)? y
Rule deleted

After a deletion the numbers shift, so run status numbered again before the next deletion. The second way is to repeat the rule with the word delete: sudo ufw delete allow 80/tcp; it removes both the IPv4 and the IPv6 variant at once.

Rule order and inserting at the top

ufw checks rules from top to bottom, and the first matching one applies. So a deny for a specific address must sit above a general allow:

$ sudo ufw insert 1 deny from 198.51.100.23
Rule inserted

If you add a deny with the usual command, it lands at the end of the list after allow 443/tcp and will not take effect for that port.

Protecting SSH from brute force: ufw limit

The limit rule allows the connection but blocks an address that has opened 6 or more connections within 30 seconds:

$ sudo ufw limit OpenSSH
Rule updated

This is simple protection against mass password guessing. For serious protection, also allow only key-based login and, if you like, add fail2ban, which can ban addresses through ufw.

ufw logs

Logging is enabled with sudo ufw logging on; levels are low, medium, high and full. Entries go to /var/log/ufw.log (through rsyslog) and to the kernel log:

$ sudo journalctl -k | grep 'UFW BLOCK' | tail -2
Oct 14 10:12:03 web kernel: [UFW BLOCK] IN=eth0 OUT= SRC=45.155.205.1 DST=192.0.2.10 PROTO=TCP SPT=51544 DPT=3389
Oct 14 10:12:09 web kernel: [UFW BLOCK] IN=eth0 OUT= SRC=89.248.165.7 DST=192.0.2.10 PROTO=TCP SPT=40312 DPT=23

The SRC and DPT fields show who knocked and on which port. If a service does not respond and the log has UFW BLOCK for its port, the rule was not added.

ufw and Docker: why ports are open despite the rules

This is the most notorious trap. When Docker publishes a port (-p 8080:80), it adds its own NAT rules and the DOCKER chain itself, and traffic to the container goes through forward, bypassing the incoming rules of ufw. As a result ufw status does not show port 8080, yet it is reachable from outside.

Ways to solve it:

  • Publish ports only on localhost: -p 127.0.0.1:8080:80, and expose them to the outside through a reverse proxy (nginx).
  • Add your own rules to the DOCKER-USER chain — Docker does not overwrite it.
  • Do not publish the ports of internal containers at all, and connect them through the Docker network.

Reset and disable

$ sudo ufw disable     # turn off, rules are kept
$ sudo ufw reset       # delete all rules and turn off
$ sudo ufw reload      # re-read the configuration

Before reset, ufw saves copies of the rule files in /etc/ufw/ with a date suffix. The rules themselves are stored in /etc/ufw/user.rules and user6.rules — do not edit them by hand, use the commands only.

nftables basics

nftables is the modern replacement for iptables in the Linux kernel. On Debian 10+ and Ubuntu 20.10+ the iptables command works by default through the iptables-nft compatibility layer, so ufw rules also end up in nftables. To see everything loaded into the kernel:

$ sudo nft list ruleset

The nftables structure: a table (family ip, ip6, or inet for both) contains chains attached to netfilter hook points, and chains contain rules. A minimal server firewall in the file /etc/nftables.conf:

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        ct state invalid drop
        iif "lo" accept
        ip protocol icmp accept
        meta l4proto ipv6-icmp accept
        tcp dport { 22, 80, 443 } accept
    }
    chain forward {
        type filter hook forward priority 0; policy drop;
    }
    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Apply it and enable it at boot:

$ sudo nft -c -f /etc/nftables.conf   # only check the syntax
$ sudo systemctl enable --now nftables

Rules can also be added on the fly, but they disappear after a reboot unless you write them to the file:

$ sudo nft add rule inet filter input tcp dport 5432 ip saddr 10.0.0.5 accept
$ sudo nft -a list chain inet filter input   # -a shows the handle of each rule
$ sudo nft delete rule inet filter input handle 9

The main advantage of nftables is sets ({ 22, 80, 443 }) and a single inet table for IPv4 and IPv6. Do not run your own nftables.service at the same time as ufw: flush ruleset will wipe the ufw rules.

Common mistakes

  • ufw was enabled without an SSH rule — access is lost. The provider's console (VNC/serial) can save you: sudo ufw allow OpenSSH or sudo ufw disable.
  • The port is open, but the service is unreachable — the service listens on 127.0.0.1, not on the external address. Check ss -tlnp.
  • There is also a cloud firewall — many providers have security groups; the port must be opened there too.
  • deny does not work — the rule sits below an allow rule; use insert 1.

Summary

Basic ufw setup means default deny incoming, allowing SSH and the service ports, ufw enable, and a check with status numbered. For targeted access use rules with from and port, for SSH use limit, and when working with Docker remember that it bypasses ufw. When you need NAT or complex rule sets, move to nftables. Protecting a server with a firewall is covered in practice in our Linux course.

FAQ

How is ufw different from iptables?

ufw is a front end with simple syntax that generates iptables or nftables rules itself. iptables and nftables are low-level tools that give full control but require more knowledge.

Do ufw rules survive a reboot?

Yes. After sudo ufw enable the firewall starts at boot, and the rules are stored in /etc/ufw/user.rules and user6.rules.

Do I need to open ports for outgoing connections?

With the allow outgoing policy, no. ufw passes replies to outgoing connections automatically thanks to connection tracking.

How do I allow ping when ufw is enabled?

By default ufw already passes incoming ICMP echo requests: this rule is defined in /etc/ufw/before.rules. To block ping, change ACCEPT to DROP for that rule in this file and run sudo ufw reload.

Is ufw available on CentOS and Rocky Linux?

Their standard tool is firewalld with the firewall-cmd command. ufw can be installed from EPEL, but it is better not to mix it with firewalld.

Learn this topic on video

This topic is covered in depth with practice in the course “Linux: с нуля до уверенного администрирования”.

Go to course