Setting up a firewall is essential for securing your server, and UFW makes this process straightforward and user-friendly.

Think of it as your server's bouncer – it filters network traffic and only allows authorized access, keeping unwanted traffic out.

In this guide, I’ll walk you through setting up a firewall with UFW, as well as introduce you to advanced firewall rules that allow you to take your server's security to the next level.

I assume you're working on a properly set-up Ubuntu server. If not, check out my guide on preparing Ubuntu servers to get started.

Author's Note

Before we dive into the details, I’d like to highlight a few points.

Most guides online only cover the basics of UFW and how to add simple user-defined rules from the command line. It's hard to find a guide that goes beyond that.

This guide will take you beyond the basics and provide an example of how to implement advanced firewall rules. I'll explain some of the configuration and rule files, and where certain settings come from.

Understanding how UFW works, how to manage rules, and the order in which they take effect is essential, and I’ll do my best to make that clear in this guide.

If there’s anything you'd like me to cover, feel free to contact me, and I’ll either update this guide or publish a new one with the information you need, or help you if you encounter any issues.

I’m also in the process of writing a dedicated guide on Linux firewalls, focusing on the inner workings of UFW and how to develop advanced solutions with it.

Be sure to sign up for the newsletter to get notified once I publish it.

Installing UFW

On Debian-based distributions, like Ubuntu, UFW often comes pre-packaged.

You can check and install it using this command:

sudo apt install ufw

Many server providers configure the UFW firewall upon deploying the server to allow only SSH connections, enabling you to connect to the server.

If you have a server from Vultr, UFW is likely to be enabled by default. In my case with Hetzner, UFW is not enabled.

👉
New to Hetzner? Use my link to get free credits!

You can check the status of UFW and your current ruleset using this command:

sudo ufw status

The command’s output will either indicate that UFW is inactive, or that it is active with your current rule set.

If UFW is currently inactive, that’s fine, as we’ll proceed to configure it properly and enable it.

However, if UFW is already active, disable and reset it using the following commands:

sudo ufw disable
sudo ufw reset

You can re-enable it once you have added all the rules and finished configuring it.

UFW’s Default Policy

By default, UFW takes a secure approach by blocking all incoming traffic while allowing outgoing traffic from our server. This means our server can communicate externally, but it remains inaccessible to others.

Since there is no issue with our server reaching the outside world, there is no need to make any changes to that aspect.

However, to enable incoming traffic, it’s essential to selectively open only the required ports and authorize traffic through them.

You can find the default policy defined in the /etc/default/ufw file:

DEFAULT_INPUT_POLICY="DROP"
DEFAULT_OUTPUT_POLICY="ACCEPT"

As you can see, the default policy for incoming traffic is set to DROP, while the default policy for outgoing traffic is set to ACCEPT

If UFW is enabled, you can also review the default policy using the following command:

sudo ufw status verbose

You can modify this default behavior of UFW either by directly editing the file or by using these two commands:

sudo ufw default <policy> incoming
sudo ufw default <policy> outgoing

Replace <policy> with either denyallow or reject.

deny corresponds to DROP, allow corresponds to ACCEPT, and reject corresponds to REJECT.

Both DROP and REJECT policies prevent traffic from passing through the firewall, but they differ in their response messages.

With DROP, the traffic is silently discarded without any acknowledgment sent to the source. It neither forwards the packet nor responds to it.

On the other hand, REJECT sends an error message back to the source, signaling a connection failure.

UFW's Configuration Files

There are three files I’d like you to review.

While you may not need to modify them when configuring UFW and adding your rules, it's helpful to be familiar with them.

You may have already opened the /etc/default/ufw file and examined the default policy of UFW, but there are other settings you might need to know about.

For example, you can disable IPv6 completely by changing the IPV6 variable from yes to no.

Additionally, you have the option to modify the default forward policy and the default application policy.

You can also enable UFW to manage the built-in chains by setting the MANAGE_BUILTINS variable to yes. Built-in chains are the default chains provided by Iptables, such as INPUTOUTPUT, and FORWARD.

UFW creates its own chains to manage its rules, such as ufw-user-input and ufw-user-output. These UFW chains are linked to the built-in chains to apply UFW’s firewall rules.

When MANAGE_BUILTINS is set to yes, on stopping or reloading UFW, it will flush the built-in chains completely, removing all rules (both UFW-managed and non-UFW rules) from INPUTOUTPUT, and FORWARD.

Any non-UFW rules in the built-in chains will be lost. UFW will take full control of the firewall, which may conflict with other tools that manage these chains.

For this reason, I don’t recommend enabling it unless you are fully aware of the implications.

The only change I make in this file is to the IPT_SYSCTL variable. There’s another file I’ll discuss next, which is /etc/ufw/sysctl.conf. UFW uses this file to tweak certain kernel parameters.

However, the original file for changing kernel parameters is /etc/sysctl.conf.

While UFW uses its own version for this purpose, I prefer not to do that. When I harden the Linux kernel, I make my changes to kernel parameters directly in the /etc/sysctl.conf file.

If UFW modifies the same parameters in its own file, my changes won’t take effect because UFW will override any corresponding parameters in the original sysctl.conf file.

That’s why I configure UFW to use the original sysctl.conf file like this:

IPT_SYSCTL=/etc/sysctl.conf

This way, I avoid dealing with two files and can maintain a clearer overview of the changes in a single file.

If you open the /etc/ufw/sysctl.conf file, you’ll find that UFW has tweaked several kernel parameters. Once UFW is enabled, the new values for these parameters will take effect.

By default, UFW disables the acceptance of ICMP redirects, which helps prevent man-in-the-middle attacks. It also ignores bogus (invalid) ICMP errors and disables the logging of Martian packets.

Since I prefer not to manage kernel parameters in two places, I configure UFW to use the /etc/sysctl.conf file instead.

To incorporate the default changes that UFW makes, I can simply add these to the end of the /etc/sysctl.conf file:

net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.icmp_echo_ignore_all = 0
net.ipv4.conf.all.log_martians = 0
net.ipv4.conf.default.log_martians = 0

Now, simply reboot the server for the changes to take effect. There’s no need to enable UFW for the changes to apply, since we are using the original sysctl.conf file.

👉
Check out my kernel hardening guide, where I list all the kernel parameters I tweak to improve the security of my Linux servers.

The last file I want you to review is the /etc/ufw/ufw.conf file, which contains just two variables:

ENABLED=no
LOGLEVEL=low

The first variable controls whether UFW is enabled or disabled. There's no need to change it manually, as enabling or disabling UFW from the command line will automatically update this value.

The second variable controls the log level of UFW. It can be set to offlowmediumhigh, or full, depending on how much logging detail you want.

You can also change the logging level directly from the command line using the following command:

sudo ufw logging <logging_level>

This will automatically update the value of the LOGLEVEL variable.

UFW's Rule Files

In the /etc/ufw/ directory, you'll find files with the .rules extension:

after6.rules  after.rules  before6.rules  before.rules  user6.rules  user.rules

These files control how UFW manages incoming, outgoing, and forwarded traffic. Files with the number 6 handle IPv6 traffic, while files without it handle IPv4 traffic.

It's important to note that you should not modify the user.rules or user6.rules files directly, as any changes could be overwritten by UFW. Rules added by the user from the command line are saved to these files, which is why they are called user.rules files.

You are free to add custom rules only to the before.rules or after.rules files.

The order in which UFW processes firewall rules is as follows: before.rules first, user.rules next, and after.rules last.

For instance, if you place a rule to block HTTP traffic in before.rules, it will be processed before any rule in the user.rules file that might allow HTTP traffic.

The rules in the before.rules files take priority, meaning they are executed first, allowing you to enforce more critical security measures, such as rules to block SYN flood attacks.

There are rare cases where I've had to add custom rules to the after.rules file. While I don't think you'll need to do this, it's good to at least know it exists – just in case!

And it’s important to mention that all of UFW's rule files primarily use the Iptables syntax.

Understanding the order in which UFW processes these files and their respective roles is essential for effectively managing your firewall.

Basic User-Defined Rules

In the following, I'll cover the basic firewall rules that can be added from the command line.

UFW offers a set of commands for managing firewall rules directly, allowing you to quickly specify which services or ports are allowed or denied.

While these rules are designed for basic network access control and aren't intended for advanced use cases, they are perfect for setting up a firewall swiftly and efficiently.

💡
To review the rules you've added when the firewall is disabled, use the command sudo ufw show added, as sudo ufw status won’t display the rules in that case.

Remember, every rule you add from the command line is saved to the user.rules file, so feel free to check it as you add rules to understand how UFW translates the commands into the file.

Allowing and Denying Traffic

The core functionality of UFW is to allow or deny network traffic.

To allow or deny traffic for specific ports, you use the allow or deny rules, respectively.

To allow incoming traffic on port 22 (SSH):

sudo ufw allow 22

You can also specify a protocol (TCP or UDP):

sudo ufw allow 22/tcp

To deny traffic on port 80 (HTTP):

sudo ufw deny 80

If a range of ports is required, such as 5000-6000, use the following:

sudo ufw deny 5000:6000/tcp

Similarly, to deny traffic for the same range:

sudo ufw deny 5000:6000/tcp

You can also allow or deny traffic based on a service's name instead of specifying a port number. For example, to allow SSH traffic by using the service name, you can use:

sudo ufw allow ssh

UFW will then automatically determine the correct port (port 22 for SSH) and the associated protocol (TCP) to apply the rule. This approach simplifies rule management, especially when dealing with well-known services.

Application Profiles

Applications (software or services installed) can register their profiles with UFW upon installation, enabling UFW to manage them by name.

To view the available profiles, you can use the following command:

sudo ufw app list

If your server is new, you are more likely to see only the OpenSSH profile, which is the service behind SSH.

When using application profiles, there’s no need to memorize specific ports. Instead, you use the profile name.

For instance, to allow traffic on port 443 (HTTPS), you can use the following commands:

sudo ufw allow "NGINX HTTPS"
sudo ufw allow "Apache Secure"

If you’re curious about the origins of these profiles, check the /etc/ufw/applications.d/ directory.

The limit Rule

The limit rule in UFW helps protect against brute-force attacks by restricting the number of connection attempts an IP can make in a short time.

For example, when securing SSH, this rule lets legitimate users connect but temporarily blocks any IP that makes too many failed attempts.

By default, the rule allows only 6 connections from the same IP within 30 seconds. If the limit is exceeded, the IP is blocked temporarily, which reduces the risk of brute-force attacks.

To apply this rule for SSH traffic:

sudo ufw limit 22

This command enables SSH access while protecting the server from excessive connection attempts.

Access Control by IP or Subnet

UFW lets you control access based on IP addresses or subnets, which is useful for limiting access to specific clients or denying access from specific sources.

To allow all traffic from a specific IP to any port:

sudo ufw allow from 192.168.1.100

To allow SSH traffic only from a specific IP:

sudo ufw allow from 192.168.1.100 to any port 22

To allow traffic from a subnet:

sudo ufw allow from 192.168.1.0/24

To deny traffic from a specific IP address:

sudo ufw deny from 203.0.113.50

In some cases, you might want to specify not just the IP or subnet but also the protocol (TCP or UDP).

For example, to allow only TCP traffic from an IP address to port 80, you can specify the protocol as follows:

sudo ufw allow from 192.168.1.100 to any port 80 proto tcp

By specifying protocols in your access control rules, you gain more control over the traffic flow, ensuring that only the desired traffic (TCP or UDP) is allowed from specific sources or networks.

Enabling and Checking Status

Before activating our firewall, it’s crucial to review the rules we’ve added so far to prevent any unexpected behavior.

As I mentioned earlier, if the firewall is disabled, we can't use the sudo ufw status command to view our rules.

Instead, we use the sudo ufw show added command. This command will list all the rules we have added.

Always add the rules, review them, and then proceed to enable the firewall.

To enable UFW and apply the rules you’ve configured, use the following command:

sudo ufw enable

Now, you can check the status of UFW and your current ruleset using the sudo ufw status command.

For a more detailed view:

sudo ufw status verbose

If you experience any issues, disable UFW using sudo ufw disable and review your rules again.

If you need to reset UFW to its default state (removing all rules), you can use the sudo ufw reset command.

Deleting Rules

If, for some reason, you want to delete a rule you have added, you can use the sudo ufw delete command followed by the rule itself like this:

sudo ufw delete deny from 111.111.111.111 to any port 80 proto tcp
sudo ufw delete allow 80

There is another easier way to delete rules, but it requires the firewall to be enabled. This method involves using the rule number.

Once the firewall is enabled, you can use the sudo ufw status numbered command to obtain a list of your rules and their corresponding numbers, like this:

To                         Action      From
--                         ------      ----
[ 1] 22/tcp                ALLOW IN    Anywhere                  
[ 2] 22/tcp (v6)           ALLOW IN    Anywhere (v6)  

Now, to delete a rule, you can simply use the rule number:

sudo ufw delete 1 

This is a much simpler method.

Best Practices

I want to share some best practices with you from my experience

The first step before enabling a firewall is to allow SSH traffic to ensure access to the server. If you enable the firewall before adding this rule, you risk losing access to your server.

I showed you how to use the allow or limit rules. Using these rules will permit any IP address to access port 22, which means our SSH port is open to everyone. This is something I avoid on a production server.

If I have a static IP from which I can access the server, I restrict the SSH port to that IP. This provides an extra layer of security and reduces the risk of unauthorized access.

Even if you’ve generated an SSH key pair, implemented key authentication, and created a non-root user, hackers could still attempt to breach your server.

If you’ve followed my guide on preparing Ubuntu servers, you should have already completed these security steps.

Only restrict the SSH port to your IP if it’s static – such as when using a VPN service or if your ISP has assigned you a static IP.

If you have a static IP, use the following command:

sudo ufw allow from <YOUR_IP> proto tcp to any port 22

Now, the IP specified in the command is the only one that can access the server.

💡
When you restrict SSH access to a single IP, Fail2ban becomes irrelevant since there are no IPs to block. However, I still recommend keeping Fail2ban installed and enabled.

Another useful feature to implement is a cloud firewall. Most server providers offer a cloud firewall, which allows you to set up a basic firewall at the cloud level and apply it to your servers.

With this, you can restrict the SSH port to your IP address using the cloud firewall while leaving it open to everyone in UFW. If your IP address changes, you can easily access your provider's dashboard to update it.

Now, let's talk about HTTP and HTTPS traffic.

If you're hosting a website, you need to allow traffic on ports 80 and 443 for visitors to access it. There should be no restrictions here, as we want everyone to reach the site.

However, there are a couple of considerations. If you're using an SSL certificate (which you should) and redirecting traffic from HTTP to HTTPS, there's no need to allow traffic on port 80. While allowing traffic on both ports is generally fine, I wanted to mention this.

Additionally, if you're using a proxy service like Cloudflare, traffic will only come from their IPs. To enhance security, restrict HTTP and HTTPS traffic to only Cloudflare's IPs.

For example, use these commands:

sudo ufw allow from <CF_IP> to any port 80 proto tcp
sudo ufw allow from <CF_IP> to any port 443 proto tcp

Repeat these commands for all Cloudflare IPs, or automate it with a bash script.

Advanced Firewall Rules

Up until now, we’ve focused on basic user-defined rules that you can easily manage from the command line.

Now, I’ll introduce the idea of advanced rules, which allow you to control traffic at a deeper level by configuring UFW's /etc/ufw/before.rules files.

These advanced rules let you filter traffic before it reaches your server’s services and before the firewall applies its standard rules.

Advanced rules are incredibly powerful and can be tailored to specific use cases, offering finer control over your network's security and performance.

And as I mentioned earlier, all of UFW's rule files primarily use the iptables syntax.

Structure of before.rules Files

The before.rules file begins with a declaration of the *filter table and defines several custom chains:

*filter
:ufw-before-input - [0:0]
:ufw-before-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-not-local - [0:0]
  • :ufw-before-input: Processes incoming packets.
  • :ufw-before-output: Processes outgoing packets.
  • :ufw-before-forward: Handles packets forwarded through the server.
  • :ufw-not-local: Deals with packets that are not addressed to or from the local system.

It includes several default rules to handle fundamental network operations and improve security. These rules cover a variety of scenarios, such as allowing all traffic on loopback interfaces or dropping invalid packets. They are applied by default once you enable the firewall.

The file ends with the COMMIT line, signaling the completion of the rules.

The structure in before6.rules for IPv6 is nearly identical to that of before.rules for IPv4.

The main difference is that the chains in before6.rules all have the number 6 added to their names, like this:

*filter
:ufw6-before-input - [0:0]
:ufw6-before-output - [0:0]
:ufw6-before-forward - [0:0]

However, unlike before.rulesbefore6.rules does not include a ufw6-not-local chain.

It includes some of the default rules that before.rules has, but it also contains additional rules that are applied specifically to IPv6 traffic.

The file also ends with the COMMIT line, signaling the completion of the rules.

Use Cases

As I mentioned earlier, these files take priority, meaning they are executed first.

This allows us to define rules that will take effect before traffic reaches anything running on the server and before it travels further through the firewall.

You can, for example, implement a solution to block SYN flood attacks by rate-limiting the number of SYN packets allowed through ports 80 and 443, and block IPs exceeding these limits. This is something that cannot be done using basic user-defined rules from the command line.

While you can use the limit rule for ports 80 and 443, it’s not ideal for a web server since the limits may not be well-suited to handle the typical traffic patterns of a web server.

Using more advanced rules in before.rules allows you to fine-tune the firewall for specific use cases like this.

👉
Check out my guide on preventing SYN flood attacks on a Linux server, where I provide a solution that combines advanced UFW rules with Fail2ban.

Example of Advanced Rules

If you examine the contents of the before.rules file, you will notice these two rules:

-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
-A ufw-before-input -m conntrack --ctstate INVALID -j DROP

These two rules are designed to log and block any invalid packets, and they are added by default by UFW to the ufw-before-input chain, which filters incoming traffic before it reaches the server, ensuring that only legitimate connections are allowed.

UFW uses the conntrack module (short for connection tracking) to monitor connections and identify those with INVALID connection states. While these rules are effective, we can make them even better.

To further enhance the security of our server, we could add two additional rules to log and block any new connections that don’t have only the SYN flag set.

Add the following two rules below the ones added by default by UFW:

-A ufw-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j ufw-logging-deny
-A ufw-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP

Don't forget to add them to the before6.rules file as well:

-A ufw6-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j ufw6-logging-deny
-A ufw6-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP
  • The first rule drops any packet that’s considered INVALID by the conntrack module.
  • The second rule blocks TCP packets that are flagged as NEW (indicating new connection attempts) but don’t have the SYN flag set alone.

Now, reload UFW if it is already enabled:

sudo ufw reload

These additional rules further enhance the firewall’s ability to filter out potentially malicious packets and protect your server from unwanted connection attempts.

Be sure to sign up for my newsletter to get notified about advanced firewall solutions, new rules, and other security tips as I develop them.

Conclusion and Final Thoughts

Great job reaching the end!

I hope this guide has made setting up a firewall with UFW clear and straightforward.

👉
For more comprehensive Linux server security resources, be sure to check out the full collection of detailed guides here.

If you found value in this guide or have any questions or feedback, please don't hesitate to share your thoughts in the discussion section.

Your input is greatly appreciated, and you can also contact me directly if you prefer.