Tutorial

How To Implement a Basic Firewall Template with Iptables on Ubuntu 20.04

How To Implement a Basic Firewall Template with Iptables on Ubuntu 20.04

Introduction

Implementing a firewall is an important step in securing your server. A large part of that is deciding on the individual rules and policies that will enforce traffic restrictions to your network. Firewalls like iptables also allow you to have a say about the structural framework in which your rules are applied.

In this guide, you’ll learn how to construct a firewall that can be the basis for more complex rule sets. This firewall will focus primarily on providing reasonable defaults and establishing a framework that encourages extensibility.

Prerequisites

To complete this tutorial, you will need access to an Ubuntu 20.04 server with a non-root user configured with sudo privileges. You can do this by following all the steps outlined in our Ubuntu 20.04 initial server setup guide, except for Step 4, since we will be setting up the firewall in this tutorial.

Additionally, we recommend reviewing the firewall policies you wish to implement. You can follow this guide to get a better idea of what to consider.

Installing the Persistent Firewall Service

Begin by updating the local package cache:

  1. sudo apt update

Now install the iptables-persistent package. This allows you to save your rule sets and have them automatically applied at boot:

  1. sudo apt install iptables-persistent

During the installation, you’ll be asked whether you want to save your current rules, select <Yes>. Please note that you’ll be running the netfilter-persistent command to execute the iptables persistent firewall service. Next, you’ll edit the generated rules files.

A Note About IPv6 in this Guide

Before we get started, we’ll briefly discuss IPv4 vs IPv6. The iptables command only handles IPv4 traffic. For IPv6 traffic, a separate companion tool called ip6tables is used. The rules are stored in separate tables and chains. For the netfilter-persistent command, the IPv4 rules are written to and read from /etc/iptables/rules.v4, and the IPv6 rules are stored in /etc/iptables/rules.v6.

This guide assumes that you are not actively using IPv6 on your server. If your services do not leverage IPv6, it’s safer to block access entirely, as will be demonstrated in this guide.

Implementing the Basic Firewall Policy (The Quick Way)

For the purpose of getting up and running as quickly as possible, we’ll show you how to edit the rules file directly and copy and paste the finished firewall policy. Afterwards, we will explain the general strategy and how these rules could be implemented using the iptables command instead of modifying the file.

To implement the firewall policy and framework, you’ll edit the /etc/iptables/rules.v4 and /etc/iptables/rules.v6 files. Open the rules.v4 file in your preferred text editor. Here, we’ll use nano:

  1. sudo nano /etc/iptables/rules.v4

Inside, the file will contain the following contents:

/etc/iptables/rules.v4
# Generated by iptables-save v1.8.4 on Tue Mar  1 19:03:10 2022
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
# Completed on Tue Mar  1 19:03:10 2022

Delete these contents and replace them with the following:

/etc/iptables/rules.v4
*filter
# Allow all outgoing, but drop incoming and forwarding packets by default
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Custom per-protocol chains
:UDP - [0:0]
:TCP - [0:0]
:ICMP - [0:0]

# Acceptable UDP traffic

# Acceptable TCP traffic
-A TCP -p tcp --dport 22 -j ACCEPT

# Acceptable ICMP traffic

# Boilerplate acceptance policy
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -i lo -j ACCEPT

# Drop invalid packets
-A INPUT -m conntrack --ctstate INVALID -j DROP

# Pass traffic to protocol-specific chains
## Only allow new connections (established and related should already be handled)
## For TCP, additionally only allow new SYN packets since that is the only valid
## method for establishing a new TCP connection
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --syn -m conntrack --ctstate NEW -j TCP
-A INPUT -p icmp -m conntrack --ctstate NEW -j ICMP

# Reject anything that's fallen through to this point
## Try to be protocol-specific w/ rejection message
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable

# Commit the changes
COMMIT

*raw
:PREROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT

*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT

*security
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT

*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT

Save and close the file. If you’re using nano, you can do this by pressing CTRL + X, then Y and ENTER.

You can test the file for syntax errors by running the following command. Make sure to fix syntax errors if you receive any:

  1. sudo iptables-restore -t /etc/iptables/rules.v4

Next, open the /etc/iptables/rules.v6 file to modify the IPv6 rules:

  1. sudo nano /etc/iptables/rules.v6

This file will have the following contents:

/etc/iptables/rules.v6
# Generated by ip6tables-save v1.8.4 on Tue Mar  1 19:03:10 2022
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
# Completed on Tue Mar  1 19:03:10 2022

You can block all IPv6 traffic by replacing the contents of the file with the following configuration:

/etc/iptables/rules.v6
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
COMMIT

*raw
:PREROUTING DROP [0:0]
:OUTPUT DROP [0:0]
COMMIT

*nat
:PREROUTING DROP [0:0]
:INPUT DROP [0:0]
:OUTPUT DROP [0:0]
:POSTROUTING DROP [0:0]
COMMIT

*security
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
COMMIT

*mangle
:PREROUTING DROP [0:0]
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
:POSTROUTING DROP [0:0]
COMMIT

Save and close the file.

To test this file for syntax errors, use the ip6tables-restore command with the -t option:

  1. sudo ip6tables-restore -t /etc/iptables/rules.v6

When both rules files report no syntax errors, you can apply the rules you’ve set by running:

  1. sudo service netfilter-persistent reload
Output
* Loading netfilter rules... run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables start run-parts: executing /usr/share/netfilter-persistent/plugins.d/25-ip6tables start [ OK ]

This will immediately implement the policy outlined in your files. You can verify this by listing the iptables rules currently in use. First check for IPv4:

  1. sudo iptables -S
Output
-P INPUT DROP -P FORWARD DROP -P OUTPUT ACCEPT -N ICMP -N TCP -N UDP -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A INPUT -i lo -j ACCEPT -A INPUT -m conntrack --ctstate INVALID -j DROP -A INPUT -p udp -m conntrack --ctstate NEW -j UDP -A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP -A INPUT -p icmp -m conntrack --ctstate NEW -j ICMP -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable -A INPUT -p tcp -j REJECT --reject-with tcp-reset -A INPUT -j REJECT --reject-with icmp-proto-unreachable -A TCP -p tcp -m tcp --dport 22 -j ACCEPT

Then check the current IPv6 rules:

  1. sudo ip6tables -S
Output
-P INPUT DROP -P FORWARD DROP -P OUTPUT DROP

These firewall rules will be re-applied at each boot. Test it out to make sure that you can still log in and that all other access is blocked off.

An Explanation of Our General Firewall Strategy

In the basic firewall constructed with the rules from the previous section, we’ve created an extensible framework that can be adjusted to add or remove rules. For IPv4 traffic, we’re mainly concerned with the INPUT chain within the filter table. This chain will process all packets destined for our server. We’ve also allowed all outgoing traffic and denied all packet forwarding, which would only be appropriate if this server were acting as a router for other hosts. We accept packets in all of the other tables since we only want to filter packets in this guide.

In general, our rules set up a firewall that will deny incoming traffic by default. We then create exceptions for the services and traffic types we wish to exclude from this policy.

In the main INPUT chain, we’ve added some generic rules for traffic that we are confident will always be handled the same way. For instance, we always want to deny packets that are deemed “invalid” and we’ll always want to allow traffic on the local loopback interface and data associated with an established connection.

Afterwards, we match traffic based on the protocol it is using and shuffle it to a protocol-specific chain. These protocol-specific chains are meant to hold rules that match and allow traffic for specific services. In this example, the only service we allow is SSH in our TCP chain. If we were offering another service, like an HTTP(S) server, we could add exceptions here as well. These chains will be the focus of most of your customization.

Any traffic that does not match the generic rules or the service rules in the protocol-specific chains are handled by the last few rules in the INPUT chain. We have set the default policy to DROP for our firewall, which will deny packets that fall through our rules. However, the rules at the end of the INPUT chain reject packets and send a message to the client that mimics how the server would respond if there were no service running on that port.

For IPv6 traffic, we drop all traffic. Our server is not using this protocol, so it is safest to not engage with the traffic at all.

Implementing Your Firewalls Using the iptables Command

Now that you understand the general idea behind the policy we built, we will discuss how you could create those rules using iptables commands. We will end up with the same rules that we specified above but we will create our policies by adding rules iteratively. Because iptables applies each of the rules immediately, rule ordering is very important (for example, we leave the rules that deny packets until the end).

Resetting your Firewall

Start by resetting your firewall rules so that you can review how policies can be built from the command line. Flush all of your rules by running the following:

  1. sudo service netfilter-persistent flush

Now verify that your rules are reset:

  1. sudo iptables -S

You should have output that shows the rules in the filter table are gone and that the default policy is set to ACCEPT on all chains:

Output
-P INPUT ACCEPT -P FORWARD ACCEPT -P OUTPUT ACCEPT

Creating Protocol-Specific Chains

Next, you will create all of your protocol-specific chains. These will be used to hold the rules that create exceptions to your deny policy for services you want to expose. You’ll create one for UDP traffic:

  1. sudo iptables -N UDP

Then another one for TCP:

  1. sudo iptables -N TCP

And one more for ICMP:

  1. sudo iptables -N ICMP

Next, add the exception for SSH traffic. SSH uses TCP, so you’ll add a rule to accept TCP traffic destined for port 22 to the TCP chain:

  1. sudo iptables -A TCP -p tcp --dport 22 -j ACCEPT

If you want to add additional TCP services, you can do that now by repeating the command with the port number replaced.

Creating General Purpose Accept and Deny Rules

In the INPUT chain, where all incoming traffic begins filtering, we need to add our general purpose rules. These are some common sense rules that set the baseline for our firewall by accepting traffic that’s low risk (local traffic and traffic that’s associated with connections we’ve already checked) and dropping traffic that is clearly not useful (invalid packets).

First, create an exception to accept all traffic that is part of an established connection or is related to an established connection:

  1. sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

This rule uses the conntrack extension, which provides internal tracking so that iptables has the context it needs to evaluate packets as part of larger connections instead of as a stream of discrete, unrelated packets. TCP is a connection-based protocol, so an established connection is fairly well-defined. For UDP and other connectionless protocols, established connections refer to traffic that has seen a response (the source of the original packet will be the destination of the response packet, and vice versa). A related connection refers to a new connection that has been initiated in association with an existing connection. The classic example here is an FTP data transfer connection, which would be related to the FTP control connection that has already been established.

You’ll also want to allow all traffic originating on the local loopback interface. This is traffic generated by the server and destined for the server. It is used by services on the host to communicate with one another:

  1. sudo iptables -A INPUT -i lo -j ACCEPT

Finally, deny all invalid packets. Packets can be invalid for a number of reasons. They may refer to connections that do not exist, they may be destined for interfaces, addresses, or ports that do not exist, or they may be malformed. In any case, you’ll drop all invalid packets since there is no proper way to handle them and because they could represent malicious activity:

  1. sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

Creating the Jump Rules to the Protocol-Specific Chains

So far, we’ve created some general rules in the INPUT chain and some rules for specific acceptable services within our protocol-specific chains. However, right now, traffic comes into the INPUT chain and has no way of reaching our protocol-specific chains.

Now you need to direct traffic in the INPUT chain into the appropriate protocol-specific chains. You can match on protocol type to send it to the right chain. Also, ensure that the packet represents a new connection (any established or related connections should already be handled earlier). Start with UDP traffic:

  1. sudo iptables -A INPUT -p udp -m conntrack --ctstate NEW -j UDP

Next, run the following command for TCP traffic. Please note that with TCP packets, you’ll add the additional requirement that the packet is a SYN packet, which is the only valid type to start a TCP connection:

  1. sudo iptables -A INPUT -p tcp --syn -m conntrack --ctstate NEW -j TCP

Then run the following for ICMP traffic:

  1. sudo iptables -A INPUT -p icmp -m conntrack --ctstate NEW -j ICMP

Rejecting All Remaining Traffic

If a packet that was passed to a protocol-specific chain did not match any of the rules within, control will be passed back to the INPUT chain. Anything that reaches this point should not be allowed by your firewall.

You’ll deny the traffic using the REJECT target, which sends a response message to the client. This allows you to specify the outbound messaging so that you can mimic the response that would be given if the client tried to send packets to a regular closed port. The response is dependent on the protocol used by the client.

Attempting to reach a closed UDP port will result in an ICMP message stating “port unreachable”. You can imitate this by running the following:

  1. sudo iptables -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable

Attempting to establish a TCP connection on a closed port results in a TCP RST response:

  1. sudo iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset

For all other packets, you can send a ICMP “protocol unreachable” message to indicate that the server doesn’t respond to packets of that type:

  1. sudo iptables -A INPUT -j REJECT --reject-with icmp-proto-unreachable

Adjusting Default Policies

The last three rules you added should handle all remaining traffic in the INPUT chain. However, you should set the default policy to DROP as a precaution, as in the following:

  1. sudo iptables -P INPUT DROP

You should also set this policy in the FORWARD chain if this server isn’t configured as a router to other machines:

  1. sudo iptables -P FORWARD DROP

Warning: With your policy set to DROP, if you clear your iptables with sudo iptables -F, your current SSH connection will be dropped! Flushing with sudo netfilter-persistent flush is a better way to clear rules since it will reset the default policy as well.

To match your IPv6 policy of dropping all traffic, you can use the following ip6tables commands, starting with INPUT:

  1. sudo ip6tables -P INPUT DROP

Then run the following for FORWARD:

  1. sudo ip6tables -P FORWARD DROP

Finish up by setting the policy for OUTPUT:

  1. sudo ip6tables -P OUTPUT DROP

This should replicate your rules set fairly closely.

Saving iptables Rules

At this point, you should test your firewall rules and make sure they block the traffic you want to keep out while not hindering your normal access. Once you’re satisfied that your rules are behaving correctly, you can save them so that they will be automatically applied to your system at boot.

Save your current rules (both IPv4 and IPv6) by running the following:

  1. sudo service netfilter-persistent save

This will overwrite your /etc/iptables/rules.v4 and /etc/iptables/rules.v6 files with the policies you crafted on the command line.

Conclusion

By following this guide, either by pasting your firewall rules directly into the configuration files, or by manually applying and saving them on the command line, you have created a good starting firewall configuration. You will have to add the individual rules to allow access to the services you want to make available.

The framework established in this guide should allow you to make adjustments and can help clarify your existing policies. Check out some of our other guides on how to build out your firewall policy with some popular services:

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar

Technical Writer

Educator and writer committed to empowering our community by providing access to the knowledge and tools for making creative ideas into a reality



Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more