How to Set Up a Firewall with UFW on Ubuntu and Debian

A stylized checkpoint barrier filtering arrows. Text: UFW FIREWALL SETUP

We may earn a referral fee at no additional cost to you when you shop through the links on this page.

A fresh VPS answers on every port that has something listening, and the SSH port starts collecting password guesses as soon as it is reachable. ufw closes everything except what you choose to open, and this guide sets it up so that the one rule that locks people out gets added before the firewall is switched on.

Quick Setup: Six Commands 

Run these in order on the server, over SSH, as a user with sudo. The first two lines only matter on Debian, where the package is not installed; on Ubuntu the install step reports that ufw is already the newest version and moves on.

Terminal
sudo apt update
sudo apt install ufw
sudo ufw limit OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

Then confirm it is on, which is the command you will use for the rest of this guide:

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)           LIMIT IN    Anywhere                  
22/tcp (OpenSSH (v6))      LIMIT IN    Anywhere (v6)

Everything below explains those lines, opens the ports your services need, and covers the two situations where ufw looks switched on and is not protecting you.

Before You Start 

  • A server running Ubuntu 22.04 or later, or Debian 13, that you can reach over SSH as a user with sudo.
  • A second way in. Every provider with a web console gives you one; open it in another tab and log in through it once before you enable the firewall, because the only ufw mistake that costs real time is the one that blocks your own SSH session, and that is not the moment to discover the console wants a password you never set.
  • A machine you administer. Shared hosting does not give you root or the kernel’s packet filter, so none of this applies there.

Every command in this guide was run on freshly provisioned DigitalOcean servers (DigitalOcean calls them droplets), and the one feature that turns a ufw mistake from a minute into an afternoon is a console you can reach when SSH is blocked. Whether your provider gives you one is worth checking before you enable anything.

This guide is for the Debian family only. Rocky Linux, AlmaLinux and Fedora ship firewalld as their native tool, and the ufw that EPEL offers the RHEL rebuilds is an older 0.35 release; use firewalld there rather than fighting the platform.

Check Whether ufw Is Installed 

Ubuntu server images ship ufw, installed but switched off. Debian images do not ship it at all, even though the package is in the standard repositories. A quick way to tell which case you are in is to ask dpkg (the general method for any package is in how to check whether a package is installed):

Terminal
dpkg-query -W -f '${Package} ${Version} ${Status}' ufw

On an Ubuntu 22.04 server that prints the installed version:

Output
ufw 0.36.1-4ubuntu0.1 install ok installed

On a Debian 13 server it prints the not-installed marker instead:

Output
ufw  unknown ok not-installed

If you see that, or ufw itself answers with command not found, install it. Installing does not turn it on and adds no rules, so this step is safe over SSH:

Terminal
sudo apt update
sudo apt install ufw

These are the versions the four images tested for this guide carry, and the three Ubuntu ones are already on the box before you log in:

Platformufw versionPreinstalled?
Ubuntu 22.040.36.1-4ubuntu0.1Yes, inactive
Ubuntu 24.040.36.2-6Yes, inactive
Ubuntu 26.040.36.2-9build1Yes, inactive
Debian 130.36.2-9No, apt install ufw

The version matters less than it looks. What differs between the platforms is what else is on the image, which the next sections call out where it bites.

Allow SSH Before You Enable Anything 

ufw’s default policy denies everything incoming. Enable it with no rules and the next login is refused; the session you are typing in may survive on connection tracking, but you cannot count on it, and a dropped one cannot come back. That is the single most common way people lock themselves out of a new server, so the first rule you add is the one that keeps you in:

Terminal
sudo ufw limit OpenSSH

Two things are going on in that command. OpenSSH is an application profile, a small file the openssh-server package drops into /etc/ufw/applications.d that maps the name to port 22/tcp. It is present on all four images tested here, including Debian before ufw was installed, so the name is safe to use. And limit rather than allow adds rate limiting. The ufw manual describes it precisely: When a limit rule is used, ufw will normally allow the connection but will deny connections if an IP address attempts to initiate 6 or more connections within 30 seconds. That is enough to slow a password-guessing script to a crawl without troubling one person typing a password. The exception is many people or a script connecting from one address, an office behind NAT or a deployment tool opening sessions in a burst; if that is your setup, use allow for SSH and leave the rate limiting to fail2ban.

If sshd listens on a non-standard port, the profile still says 22, so name the port instead:

Terminal
sudo ufw limit 2222/tcp

ufw knows this is the dangerous moment. The manual notes that By default, ufw will prompt when enabling the firewall while running under ssh. This can be disabled by using ‘ufw --force enable’. That prompt is your last chance to check, so do not reach for the force flag on a server you are logged into remotely.

Add the SSH rule first, every time, on every server. If you are ever unsure whether it is there, run sudo ufw status and look for an OpenSSH or 22/tcp line before you run ufw enable.

Enable the Firewall and Read Its Status 

Set the default policies explicitly, even though they are already the defaults on a fresh install. Writing them down means the article, your shell history and the server agree:

Terminal
sudo ufw default deny incoming
sudo ufw default allow outgoing

Now switch it on. In the manual’s words, enable reloads firewall and enables firewall on boot, so one command covers both now and the next reboot:

Terminal
sudo ufw enable
Output
Firewall is active and enabled on system startup

Here is the part that trips people up. On the Ubuntu images, ufw.service is already enabled and active before you run anything, and it stays that way whether the firewall is on or off. So this pair of commands, run on a fresh Ubuntu 22.04, 24.04 or 26.04 server before ufw enable, disagrees with each other:

Terminal
systemctl is-active ufw
sudo ufw status
Output
active
Status: inactive

The service being active means the unit that would load the rules at boot is running; it says nothing about whether rules are loaded. What actually decides that is a single line in /etc/ufw/ufw.conf, which ufw enable flips from no to yes:

Terminal
grep -v '^#' /etc/ufw/ufw.conf
Output
ENABLED=no

LOGLEVEL=low

So the rule is simple: ufw status is the only truthful check. Never infer the firewall’s state from systemctl status ufw or from a list of running services. Once enabled, ufw status verbose shows the policies, the logging level and every rule, and that is the output you want to see before you close the console tab.

Open Ports for Your Services 

With SSH allowed and the default policy denying everything else, each service you run needs its own rule. The simple syntax takes a port, optionally with a protocol:

Terminal
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Or both in one rule, which is what the rest of this guide uses:

Terminal
sudo ufw allow 80,443/tcp

Leave the protocol off and the rule covers both TCP and UDP, which is rarely what you want for a web port. For a range the protocol is not optional; ufw refuses a range without one:

Terminal
sudo ufw allow 6000:6007/tcp

To restrict a port to one address or network, use the longer form. This is the right shape for a MySQL server that only your office should reach, or an admin panel:

Terminal
sudo ufw allow from 203.0.113.0/24 to any port 3306 proto tcp

203.0.113.0/24 is a documentation range that matches nobody; replace it with your office or VPN network before you run it, or the rule allows nothing.

Do not expect the same trick to lock SSH down to your own address while the broad OpenSSH rule from earlier is still in place: rules are matched in order, and the global one keeps matching every other source. To restrict SSH, add the narrow rule first, then delete the broad one, so there is never a moment with no SSH rule at all. Replace 203.0.113.42 with the address you are connecting from (the first field of echo $SSH_CLIENT is that address; the other two are port numbers), and have the console open before you run the second line:

Terminal
sudo ufw limit from 203.0.113.42 to any port 22 proto tcp
sudo ufw delete limit OpenSSH

Application profiles are the alternative to remembering port numbers. List what your server knows about:

Terminal
sudo ufw app list

On the Ubuntu images that list has exactly one entry, OpenSSH, until you install something that ships a profile. Install Nginx and Nginx Full, Nginx HTTP and Nginx HTTPS appear; the Apache package adds Apache, Apache Full and Apache Secure. Debian’s ufw package ships its own set of generic profiles, so on Debian 13 ufw app list shows dozens of names including WWW Full for ports 80 and 443. Name the profile in quotes if it has a space:

Terminal
sudo ufw allow 'Nginx Full'

Ask for a profile that is not there and ufw tells you plainly, which is what you get on any of the three images before Nginx is installed:

Output
ERROR: Could not find a profile matching 'Nginx Full'

The fix is either to install the service first or to use the port form. To see which ports a profile opens before trusting it, run sudo ufw app info 'Nginx Full'.

Deny, Delete and Reset Rules 

To remove a rule, list them with numbers first:

Terminal
sudo ufw status numbered
Output
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    LIMIT IN    Anywhere                  
[ 2] 80,443/tcp                 ALLOW IN    Anywhere                  
[ 3] 6000:6007/tcp              ALLOW IN    Anywhere                  
[ 4] 3306/tcp                   ALLOW IN    203.0.113.0/24            
[ 5] OpenSSH (v6)               LIMIT IN    Anywhere (v6)             
[ 6] 80,443/tcp (v6)            ALLOW IN    Anywhere (v6)             
[ 7] 6000:6007/tcp (v6)         ALLOW IN    Anywhere (v6)

That is the state after the rules from the previous section (if you narrowed SSH to your own address, rule 1 reads differently). Every rule you add for a port without a source address shows up twice, once for IPv4 and once for IPv6, and they are separate entries. Deleting by number removes only the one you name, and the numbers shift up as soon as one is gone, so delete one at a time and re-list between deletions:

Terminal
sudo ufw delete 2

To remove both halves of a rule in one go, repeat the original rule with delete in front of it:

Terminal
sudo ufw delete allow 80,443/tcp

Blocking a specific address is a deny rule, and because ufw processes rules in order, insert it ahead of any allow that would otherwise match; it then shows up as rule 1 in the numbered list:

Terminal
sudo ufw insert 1 deny from 198.51.100.7

Replace 198.51.100.7 with the address you actually want to block; as written it is a documentation address and blocks nobody.

ufw reset is the nuclear option and it is worth knowing exactly what it does. In the manual’s words: Disables and resets firewall to installation defaults. It backs up each rules file in /etc/ufw with a timestamped suffix before wiping it, and it leaves the firewall inactive, so after a reset you are back to needing the SSH rule and ufw enable before you are protected again.

Read the Firewall Log 

Logging is on at the low level by default, which records blocked packets that do not match a rule. The levels are off, low, medium, high and full. The manual defines medium as log level low, plus all allowed packets not matching the defined policy, all INVALID packets, and all new connections, and warns that Loglevels above medium generate a lot of logging output, and may quickly fill up your disk. Raise it while you debug, then put it back:

Terminal
sudo ufw logging medium
sudo ufw logging low

Where the lines end up depends on what else is installed. The manual is exact about it: Logged packets use the LOG_KERN syslog facility. Systems configured for rsyslog support may also log to /var/log/ufw.log. The kernel log is always there, so this works on every system:

Terminal
sudo journalctl -k | grep UFW

The dedicated file is not. All three Ubuntu images ship rsyslog, so /var/log/ufw.log appears there as soon as the first packet is logged, which on a public address is within minutes of enabling. The Debian 13 image ships without rsyslog, so the file never appears and looking for it gets you this:

Terminal
ls -l /var/log/ufw.log
Output
ls: cannot access '/var/log/ufw.log': No such file or directory

That is not a fault; the journal has every line the file would. If you want the file on Debian, sudo apt install rsyslog creates it from the next logged packet. A blocked packet logs as a [UFW BLOCK] line carrying the interface, source and destination addresses and ports. If you are watching for a specific attacker, journalctl -k -f | grep UFW follows the log live.

What ufw Actually Does Underneath 

ufw does not filter packets itself. The Linux kernel’s netfilter subsystem does that, and ufw is a front end that writes iptables rules for it. On all four platforms here, iptables is in turn the iptables-nft wrapper, so the rules land in the newer nf_tables kernel interface even though you never type nft:

Terminal
iptables --version
update-alternatives --display iptables
Output
iptables v1.8.11 (nf_tables)
iptables - auto mode
  link best version is /usr/sbin/iptables-nft
  link currently points to /usr/sbin/iptables-nft

Debian has done this since Debian 10, and the Debian wiki notes that you can switch back and forth between iptables-nft and iptables-legacy with update-alternatives. You should not need to. What is useful to know is the shape ufw gives the INPUT chain, because it explains both rule ordering and the Docker problem in the next section:

Terminal
sudo iptables -S INPUT
Output
-P INPUT DROP
-A INPUT -j ufw-before-logging-input
-A INPUT -j ufw-before-input
-A INPUT -j ufw-after-input
-A INPUT -j ufw-after-logging-input
-A INPUT -j ufw-reject-input
-A INPUT -j ufw-track-input

The policy is DROP, and every packet walks the ufw-before-* chains, then the ufw-after-* chains, in the order listed. The rules you add with ufw allow are not in this listing: they live in a chain named ufw-user-input, which ufw-before-input hands off to as its last step (sudo iptables -S ufw-before-input | tail -1 shows the jump). One platform note: the Debian 13 cloud image does not include the nft binary, so nft list ruleset fails there with command not found even though the kernel side is nf_tables; iptables -S works everywhere.

Docker Publishes Ports Straight Past ufw 

If the server runs Docker, the firewall you just built does not apply to any port a container publishes with -p. This is not a bug in either tool and it is not obscure; Docker’s own documentation states it outright: Docker and ufw use firewall rules in ways that make them incompatible with each other. The mechanism is the chain order from the previous section. As the same page puts it, Docker routes container traffic in the nat table, which means that packets are diverted before it reaches the INPUT and OUTPUT chains that ufw uses.

A ufw status that lists only SSH and 443 is not evidence that a container’s port 8080 is closed. Check with docker ps for 0.0.0.0:8080->80/tcp mappings, and publish on the loopback address (-p 127.0.0.1:8080:80) for anything that should only be reached through a reverse proxy.

The supported place to restrict published ports is the DOCKER-USER chain, which Docker creates in the filter table and processes before its own forwarding rules. You can see it once Docker is installed:

Terminal
sudo iptables -S DOCKER-USER

Rules you add there with iptables survive Docker restarts and are honoured for container traffic; rules you add with ufw are not consulted for it. They do not survive a reboot on their own, though: save them with the netfilter-persistent package (or a script that re-adds them at boot) or they are gone the next time the server comes up. Do not reach for the "iptables": false daemon setting as a fix: Docker’s documentation warns it is likely to break container networking for the Docker Engine.

Troubleshooting 

  • sudo: ufw: command not found. You are on Debian, where the package is not part of the image. sudo apt install ufw, then continue from the SSH rule.
  • Status: inactive although systemctl status ufw says active. The service unit is not the firewall. Run sudo ufw enable; ENABLED=yes in /etc/ufw/ufw.conf is the state that matters.
  • Your SSH session hung the moment you ran ufw enable. The SSH rule was missing. Log in through your provider’s web console, which does not pass through the firewall (the DigitalOcean console is one example), and run sudo ufw allow OpenSSH, or sudo ufw allow 2222/tcp if sshd listens on a port of your own.
  • ERROR: Could not find a profile matching 'Nginx Full'. The profile is installed by the Nginx package, not by ufw. Install Nginx first or use allow 80,443/tcp.
  • A deny rule is ignored. Rules match in order and you appended the deny after an allow that already matched. Use ufw insert 1 deny ... so it is evaluated first.
  • Deleting a rule left its twin behind. IPv4 and IPv6 entries are separate; delete by rule text (ufw delete allow 80/tcp) to remove both.
  • Everything vanished after ufw reset. That is what reset does. The old rule files are next to the new ones in /etc/ufw with a timestamp suffix, and the firewall is now inactive until you enable it again.
  • A container port is reachable despite a deny rule. Published Docker ports bypass ufw; see the Docker section above and use DOCKER-USER or bind the port to 127.0.0.1.

Disable or Remove ufw 

Three different commands are all called “turning off the firewall”, and they leave three different states behind. Pick one; they are not a sequence. Stop filtering but keep the rules:

Terminal
sudo ufw disable

Wipe the rules and start again:

Terminal
sudo ufw --force reset

Remove the tool entirely:

Terminal
sudo apt remove ufw

disable unloads firewall and disables firewall on boot, as the manual puts it, but leaves the rule files in /etc/ufw alone, so a later ufw enable brings the same configuration back. reset keeps nothing except the timestamped backups. Removing the package takes the tool away but not the kernel’s ability to filter, so if you replace ufw with something else, make sure the replacement is in place before you disable this one.

Next Steps 

Your server now answers on SSH and the ports you chose, refuses everything else, and you can read what it refused. The rate limit on SSH slows attackers down; it does not stop them, and the tool that actually bans them is the natural next step.

  • Install and configure Fail2ban to ban addresses that keep failing to log in. On Ubuntu 24.04 and later it bans through the same nftables backend ufw is writing to.
  • Harden the SSH server itself: keys instead of passwords, no root login, and the handful of sshd settings that matter on the one port you left open.
Support DevAnswers
Written, tested and maintained by one developer. Your support keeps these guides accurate and ad-light.
Buy me a coffee

Comments

Leave a reply