Tjelvar Olsson     About     Posts     Feed     Newsletter


I'm currently working on a computing book aimed at biologists. Check out the Biologist's Guide to Computing!

How to manage firewalls using ferm and Ansible

Firewall.

In the previous post we created an Ansible playbook for installing the GBrowse genome browser. As the name implies GBrowse is a browser based application and it serves web pages over http using Apache. If one is installing this software as a service to be made more widely accessible one needs to start thinking about security. In this post we will therefore configure a firewall for our machine.

iptables

The standard tool for setting up firewalls on Linux is iptables. It is a way to set up policy chains to allow or block traffic to, from and through the machine of interest. If you have not come across or managed iptables before I recommend that you have a look at howtogeek’s Beginner’s Guide to iptables, the Linux Firewall and Major Hayden’s Best practices: iptables.

ferm

However, managing firewalls using iptables can be a pain. Several tools have therefore evolved to make things easier. In this post we will be using a program called ferm (for Easy Rule Making).

When configuring a firewall it is easy to lock oneself out of the machine one is configuring. The most common scenario for this is setting the default policy to drop incoming connections and then accidentally flushing the connection rules, including the rule to accept ssh connections, leaving the server inaccessible. To avoid this scenario we will configure the default policy to accept incoming connections and to secure the server we will include a rule to drop any incoming connections that do not match any other rules.

Below is a list stating the behaviour that we want from the INPUT chain of our firewall.

  • We want the default policy to accept incoming connections
  • We want to enable connection tracking
  • We want to be able to ping the machine
  • We want to be able to ssh into the machine
  • We want to be able to add custom rules using Ansible
  • Finally, we want to drop any incoming connections that do not match any rules

The behaviours that we want from the OUTPUT and FORWARD chains are simpler. We do not want to limit any outgoing connections so we will set the output policy to accept all connections and because we are not configuring a router we will set the policy of the forward chain to drop all connections.

We can configure the behaviour above using the ferm.conf file below.

# Ferm script for configuring iptables.

table filter {

    chain INPUT {
	# Set the default policy to ACCEPT to avoid getting
	# accidentally locked out.
        policy ACCEPT;

        # Connection tracking.
        mod state state INVALID DROP;
        mod state state (ESTABLISHED RELATED) ACCEPT;

        # Allow local connections.
        interface lo ACCEPT;

        # Respond to ping.
        proto icmp icmp-type echo-request ACCEPT;

        # Allow ssh connections.
        proto tcp dport ssh ACCEPT;

        # Ansible specified rules.
        
        # Because the default policy is to ACCEPT we DROP
        # everything that comes through to this stage.
        DROP;
    }

    # Outgoing connections are not limited.
    chain OUTPUT policy ACCEPT;

    # This is not a router.
    chain FORWARD policy DROP;
}

If you have ferm installed you can apply the firewall above using the command below.

$ sudo ferm ferm.conf

Note that in the ferm.conf file above we have an empty section marked by the comment # Ansible specified rules.. We will use this to dynamically alter the firewall rules during the running of our Ansible playbook.

Integrating ferm with Ansible

Let us create an Ansible role for installing and configuring ferm. Copy and paste the code below into a file named roles/ferm/task/main.yml.

---
# Install and configure ferm.
#

# The ferm program is in the epel repository so we need
# to enable it. This could be a separate role, but this
# is left as an exercise for the reader.
- name: enable the epel repo
  yum: name=epel-release
       state=present

# We need to install libselinux-python on the target
# machine to be able to use Ansible to copy the ferm.conf
# file to the /etc/ferm/ directory. It would be reasonable
# to move this task into a separate role for installing common
# software, again this is left as an exercise for the reader.
- name: install libselinux-python
  yum: name=libselinux-python
       state=present

- name: install ferm
  yum: name=ferm
       state=present

- name: add /etc/ferm directory
  file: path=/etc/ferm
        mode=0700
        state=directory

- name: add the ferm.conf file to /etc/ferm
  copy: src=ferm.conf
        dest=/etc/ferm/ferm.conf
  notify: run ferm

Note that the last task copies the ferm.conf file we created above to the target machine. However, for this to work Ansible expects the ferm.conf file to be located in the directory named roles/ferm/files/. Let us therefore create this directory and move the file there.

$ mkdir roles/ferm/files
$ mv ferm.conf roles/ferm/files/

In the previous post I introduced the concept of handlers that could be notified by other tasks. Let us create a handler for applying the ferm rules. Copy and paste the code below into a file named roles/ferm/handlers/main.yml.

---

- name: run ferm
  command: ferm /etc/ferm/ferm.conf
  notify: save iptables

- name: save iptables
  command: service iptables save

Now we have a handler named run ferm, which when notified will run the command ferm /etc/ferm/ferm.conf and in turn notify the save iptables handler, which makes sure that the firewall rules persist if the machine is rebooted.

Let us add this role to our playbook. Update the gbrowse.yml file so that it looks like the below (we have only added the ferm role).

---
- hosts: all
  sudo: True

  roles:
    - ferm
    - gbrowse

However, if you run the gbrowse.yml playbook at this point the GBrowse application will stop working as port 80 will be closed. Let us therefore add a task to open up ports 80 (http) and 443 (https) to the apache role. Edit the file roles/apache/tasks/main.yml to look like the below.

---
# Install and configure Apache.

- name: install apache
  yum: name=httpd
       state=present

- name: start apache and enable at boot
  service: name=httpd
           enabled=yes
           state=started

- name: open up the http and https ports
  lineinfile: dest=/etc/ferm/ferm.conf
              line='proto tcp dport (http https) ACCEPT;'
              insertafter='# Ansible specified rules.'
  notify: run ferm

In the above we make use of Ansible’s lineinfile module to insert a new rule to the ferm.conf file.

Results

Let us run the playbook and find out what the resulting iptables firewall looks like. Here I am using the same Vagrant/Ansible setup as described in how to create automated and reproducible work flows for installing scientific software.

$ ansible-playbook -i hosts gbrowse.yml
...
$ vagrant ssh
Last login: Thu Apr 16 02:03:00 2015 from 192.168.33.1
[vagrant@localhost ~]$ sudo iptables -nL
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
DROP       all  --  0.0.0.0/0            0.0.0.0/0           state INVALID 
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           state RELATED,ESTABLISHED 
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     icmp --  0.0.0.0/0            0.0.0.0/0           icmp type 8 
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:22 
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:80 
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:443 
DROP       all  --  0.0.0.0/0            0.0.0.0/0           

Chain FORWARD (policy DROP)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Discussion

If you have public facing machines you need to think about security. However managing firewalls using iptables directly can be a pain.

In this post I have outlined how you can integrate ferm and Ansible to manage your firewall. The cool thing about this approach is that the role of interest, in this case apache, is responsible for opening up the relevant ports.

Furthermore as the /etc/ferm/ferm.conf file will be re-written every time you run the playook your rules will be updated both if you add or remove roles from the playbook. In other words if you removed the apache role and ran the playbook ports 80 and 433 would be closed at the end when the handlers were executed (handlers notified during a playbook are executed at the end of it).

Finally, note that security is a complex topic and that the reading of this post should not be taken as a substitute for a proper understanding of how to manage firewalls. That is a roundabout way of stating that I do not take responsibility for any security breaches that you encounter.