We hope you find this tutorial helpful. In addition to guides like this one, we provide simple cloud infrastructure for developers. Learn more →

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

PostedAugust 20, 2015 90.3k views Firewall Ubuntu

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, we will 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 easy extensibility. We will be demonstrating this on an Ubuntu 14.04 server.

Prerequisites

Before you begin, you should have a basic idea of the firewall policies you wish to implement. You can follow this guide to get a better idea of some of the things you should be thinking about.

In order to follow along, you will need to have access to an Ubuntu 14.04 server. We will be using a non-root user configured with sudo privileges throughout this guide. You can learn how to configure this type of user in our Ubuntu 14.04 initial server setup guide.

When you are finished, continue below.

Installing the Persistent Firewall Service

To get started, you will need to install the iptables-persistent package if you have not done so already. This will allow us to save our rule sets and have them automatically applied at boot:

  • sudo apt-get update
  • sudo apt-get install iptables-persistent

During the installation, you'll be asked whether you want to save your current rules. Say "yes" here. We will be editing the generated rules files momentarily.

A Note About IPv6 in this Guide

Before we get started, we should talk briefly about 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 iptables-persistent, the IPv4 rules are written to and read from /etc/iptables/rules.v4 and the IPv6 rules are kept 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 is safer to block access entirely, as we will be doing in this article.

Implementing the Basic Firewall Policy (The Quick Way)

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

To implement our firewall policy and framework, we will be editing the /etc/iptables/rules.v4 and /etc/iptables/rules.v6 files. Open the rules.v4 file in your text editor with sudo privileges:

  • sudo nano /etc/iptables/rules.v4

Inside, you will see a file that looks something like this:

/etc/iptables/rules.v4
# Generated by iptables-save v1.4.21 on Tue Jul 28 13:29:56 2015
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
# Completed on Tue Jul 28 13:29:56 2015

Replace the contents with:

/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.

You can test the file for syntax errors by typing this command. Fix any syntax errors that this reveals before continuing:

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

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

  • sudo nano /etc/iptables/rules.v6

We can block all IPv6 traffic by replacing the contents of the file with the below 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, we can use the ip6tables-restore command with the -t option:

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

When both rules files report no syntax errors, you can apply the rules within by typing:

  • sudo service iptables-persistent reload

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

  • sudo iptables -S
  • sudo ip6tables -S

These firewall rules will be re-applied at each boot. Test 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 we've constructed with the above rules, we've created an extensible framework that can be easily 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 are only looking to filter packets in this guide.

In general, our rules set up a firewall that will deny incoming traffic by default. We then go about creating 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 will 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 that 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 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 simply drop all traffic. Our server is not using this protocol, so it is safest to not engage with the traffic at all.

(Optional) Update Nameservers

Blocking all IPv6 traffic can interfere with how your server resolves things on the Internet. For example, this can affect how you use APT.

If you get errors like this when you try to run apt-get update:

Error
Err http://security.ubuntu.com trusty-security InRelease

Err http://security.ubuntu.com trusty-security Release.gpg
  Could not resolve 'security.ubuntu.com'

. . .

You should follow this section to get APT working again.

First, set your nameservers to outside nameservers. This example uses Google's nameservers. Open /etc/network/interfaces for editing:

  • sudo nano /etc/network/interfaces

Update the dns-nameservers line as shown:

/etc/network/interfaces
. . .
iface eth0 inet6 static
        address 2604:A880:0800:0010:0000:0000:00B2:0001
        netmask 64
        gateway 2604:A880:0800:0010:0000:0000:0000:0001
        autoconf 0
        dns-nameservers 8.8.8.8 8.8.4.4

Refresh your network settings:

  • sudo ifdown eth0 && sudo ifup eth0

The expected output is:

Output
RTNETLINK answers: No such process
Waiting for DAD... Done

Next, create a new firewall rule to force IPv4 when it's available. Create this new file:

  • sudo nano /etc/apt/apt.conf.d/99force-ipv4

Add this single line to the file:

/etc/apt/apt.conf.d/99force-ipv4
Acquire::ForceIPv4 "true";

Save and close the file. Now you should be able to use APT.

Implementing our Firewalls Using the IPTables Command

Now that you understand the general idea behind the policy we built, we will walk through how you could go about creating 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 (we leave the rules that deny packets until the end).

Reset your Firewall

We will start by resetting our firewall rules so that we can see how policies can be built from the command line. You can flush all of your rules by typing:

  • sudo service iptables-persistent flush

You can verify that your rules are reset by typing:

  • sudo iptables -S

You should see that 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

Create Protocol-Specific Chains

We will start by creating all of our protocol-specific chains. These will be used to hold the rules that create exceptions to our deny policy for services we want to expose. We will create one for UDP traffic, one for TCP, and one for ICMP:

  • sudo iptables -N UDP
  • sudo iptables -N TCP
  • sudo iptables -N ICMP

We can go right ahead and add the exception for SSH traffic. SSH uses TCP, so we will add a rule to accept TCP traffic destined for port 22 to the TCP chain:

  • sudo iptables -A TCP -p tcp --dport 22 -j ACCEPT

If we wanted to add additional TCP services, we could do that now by repeating the command with the port number replaced.

Create 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, we will create an exception to accept all traffic that is part of an established connection or is related to an established connection:

  • 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 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.

We want to also 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:

  • sudo iptables -A INPUT -i lo -j ACCEPT

Finally, we want to 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 simply be malformed. In any case, we will drop all invalid packets since there is no proper way to handle them and because they could represent malicious activity:

  • sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

Creating the Jump Rules to the Protocol-Specific Chains

So far, we have 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.

We need to direct traffic in the INPUT chain into the appropriate protocol-specific chains. We can match on protocol type to send it to the right chain. We will also ensure that the packet represents a new connection (any established or related connections should already be handled earlier). For TCP packets, we will add the additional requirement that the packet is a SYN packet, which is the only valid type to start a TCP connection:

  • sudo iptables -A INPUT -p udp -m conntrack --ctstate NEW -j UDP
  • sudo iptables -A INPUT -p tcp --syn -m conntrack --ctstate NEW -j TCP
  • sudo iptables -A INPUT -p icmp -m conntrack --ctstate NEW -j ICMP

Reject 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 our firewall.

We will deny the traffic using the REJECT target, which sends a response message to the client. This allows us to specify the outbound messaging so that we 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 "port unreachable" message. We can imitate this by typing:

  • 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:

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

For all other packets, we can send an ICMP "protocol unreachable" message to indicate that the server doesn't respond to packets of that type:

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

Adjusting Default Policies

The last three rules we added should handle all remaining traffic in the INPUT chain. However, we should set the default policy to DROP as a precaution. We should also set this policy in the FORWARD chain if this server isn't configured as a router to other machines:

  • sudo iptables -P INPUT DROP
  • 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 iptables-persistent flush is a better way to clear rules since it will reset the default policy as well.

To match our IPv6 policy of dropping all traffic, we can use the following ip6tables commands:

  • sudo ip6tables -P INPUT DROP
  • sudo ip6tables -P FORWARD DROP
  • sudo ip6tables -P OUTPUT DROP

This should replicate our rules set fairly closely.

Saving IPTables Rules

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

Save your current rules (both IPv4 and IPv6) by typing:

  • sudo service iptables-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 easily make adjustments and can help clarify your existing policies. Check out some of our other guides to see how to build out your firewall policy with some popular services:

16 Comments

  • Great article, thanks a lot!

  • In /etc/iptables/rules.v4, under # Acceptable TCP traffic, I think

    -A TCP -p tcp --dport 22 -j ACCEPT
    

    Should be

    -A INPUT -p tcp --dport 22 -j ACCEPT
    

    Which means that the command below does not catch all the errors in the file:

    sudo iptables-restore -t /etc/iptables/rules.v4
    
    • @finid: Actually, the first command should be correct for this guide.

      We've created a new chain called TCP which the INPUT chain feeds into. By adding all of our exceptions to this chain, we can easily append rules to the chain without worrying about order so much. This is just a nice usability enhancement that allows you to quickly ascertain the services you let through your firewall by looking at the protocol-specific chains we created and it allows you to add and delete them without much regard to the rule ordering.

  • netfilter-persistent which is what iptables-persistent is/going to be called in Ubuntu 15.04. Doesn't work. yay

  • Doesn't work. Errors in rules.v4 file

  • Should your /etc/iptables/rules.v4 config also allow port 80 tcp connections?

  • I followed these instructions and when I applied the IPv6 rules I got an issue.

    When I execute:

    sudo apt-get update
    

    I get the following error:

    Err http://security.ubuntu.com trusty-security InRelease
    
    Err http://ppa.launchpad.net trusty InRelease
    
    Err http://ppa.launchpad.net trusty Release.gpg
      Could not resolve 'ppa.launchpad.net'
    Err http://security.ubuntu.com trusty-security Release.gpg
      Could not resolve 'security.ubuntu.com'
    Err http://mirrors.digitalocean.com trusty InRelease
    
    Err http://mirrors.digitalocean.com trusty-updates InRelease
    
    Err http://mirrors.digitalocean.com trusty Release.gpg
      Could not resolve 'mirrors.digitalocean.com'
    Err http://mirrors.digitalocean.com trusty-updates Release.gpg
      Could not resolve 'mirrors.digitalocean.com'
    Reading package lists... Done
    W: Failed to fetch http://mirrors.digitalocean.com/ubuntu/dists/trusty/InRelease
    
    W: Failed to fetch http://mirrors.digitalocean.com/ubuntu/dists/trusty-updates/InRelease
    
    W: Failed to fetch http://security.ubuntu.com/ubuntu/dists/trusty-security/InRelease
    
    W: Failed to fetch http://ppa.launchpad.net/ondrej/php5-5.6/ubuntu/dists/trusty/InRelease
    
    W: Failed to fetch http://security.ubuntu.com/ubuntu/dists/trusty-security/Release.gpg  Could not resolve 'security.ubuntu.com'
    
    W: Failed to fetch http://ppa.launchpad.net/ondrej/php5-5.6/ubuntu/dists/trusty/Release.gpg  Could not resolve 'ppa.launchpad.net'
    
    W: Failed to fetch http://mirrors.digitalocean.com/ubuntu/dists/trusty/Release.gpg  Could not resolve 'mirrors.digitalocean.com'
    
    W: Failed to fetch http://mirrors.digitalocean.com/ubuntu/dists/trusty-updates/Release.gpg  Could not resolve 'mirrors.digitalocean.com'
    
    W: Some index files failed to download. They have been ignored, or old ones used instead.
    
    

    Please help!

    • @paveltashev Hello. This can actually be the result of a few different factors working in conjunction.

      First, the failure to fetch the new package updates seem to be related to DNS. Depending on a few different things sometimes your network configuration will be configured to rely on IPv6 name servers. This can become a problem if we are blocking IPv6. We can easily change the settings to use IPv4 servers however.

      First, open up the network configuration file in your text editor:

      • sudo nano /etc/network/interfaces

      Inside, you should see a few different configuration blocks, one for each of your configured network interfaces. Only one of these will define a parameter called dns-nameservers. This is the option we need to modify. We can change the value to use the 8.8.8.8 and 8.8.4.4 name servers, both of which are reliably operated by Google:

      /etc/network/interfaces
      . . .
      
      iface eth0 inet6 static
              address ...
              netmask 64
              gateway 2604:A880:0800:00A1:0000:0000:0000:0001
              autoconf 0
              dns-nameservers 8.8.8.8 8.8.4.4
      iface eth0 inet static
              address . . .
              . . .
      

      Save and close the file when you've change that value.

      Next, we can modify the behavior of the getaddrinfo configuration file. This will help us control whether IPv4 or IPv6 destinations are preferred if we are given a choice. We can change this behavior by modifying the /etc/gai.conf file:

      • sudo nano /etc/gai.conf

      Inside, find and uncomment this line:

      /etc/gai.conf
      . . .
      precedence ::ffff:0:0/96  100
      . . .
      

      This will tell your system to always choose IPv4 when given the choice. Save and close the file when you have uncommented the line above.

      Now, we just need to restart our network interface to use our new DNS settings. Assuming that the network interface in question is eth0, you can stop and quickly restart the interface, reading the new configuration changes, by typing:

      • sudo ifdown eth0 && sudo ifup eth0

      You will have a momentary pause in your connection to the server as the network is adjusted.

      After that, all of your services, including apt, will prefer IPv4. That should fix the issue you are seeing.

  • Justin, I have to say that I'm a bit disappointed in the direction this tutorial takes.

    Some people aren't using IPv6 because they do not know how. Writing tutorials that assume you want to block IPv6 in 2015 is simply wrong, and is part of the reason so much of today's IPv6-enabled hosts are not actually IPv6-enabled when it comes to services.

    You write incredible documentation Justin, and it's very sad to see such a shortcut being used instead of an opportunity to teach people how to properly implement an IPv6 firewall. It is literally the exact same configuration except you would allow ICMPv6, so there was probably more time spent explaining people how not to use IPv6 than the time that would've been spent implementing the entire firewall correctly.

    For instance, the whole section on apt-get would be unnecessary with a properly configured server. A customer recently was unable to update their server after following this tutorial and I strongly suspect that they DROP'd all IPv6 traffic without following the apt-get section.

    I'm sure the idea was to confuse customers less, but I think the result is the opposite. And it's simpler to enable IPv6 than to block it, so might as well do the right thing.

    I am willing to send in pull requests or to perform the work required to turn this into a dual stack tutorial if this is the direction DigitalOcean wants to take instead.

    That being said, I want to emphasize that the way you write tutorials is terrific and it speaks a lot that my main complaint is not about the accuracy of the technical content but rather what content was included. My nitpicks about the technical content are not worth mentioning. Keep up the good work!

  • Just a note, to save iptables properly on Debian 8/Ubuntu:

    service netfilter-persistent reload
    service netfilter-persistent save

    You have to reload the configuration into memory, otherwise it will just overwrite your changes!

  • After editing the rules.v4

    iptables-restore: line 41 failed
    
  • For Ubuntu 16.04

    service netfilter-persistent reload
    service netfilter-persistent save
    

    worked for me instead of

    sudo service iptables-persistent reload
    
  • Please update your document from iptables-persistent to netfilter-persistent in service reload command sudo service iptables-persistent reload

Creative Commons License