Automating iptables with Ansible: Part 2

[But wait! The saga continues. See Part 3 for an even better, better way.]

I have found a better way to manage iptables using Ansible and ferm. See my original post on the subject for background. Here I will focus on the new technique.

A New and Improved Technique

I developed a simpler technique that uses a shared ferm.conf file and a few host variables. Separate ferm.conf files per role are no longer needed. It is very simple and makes use of jinja template features. It requires three steps.

First, include the following play snippet in your base role. Mine is called ‘centos’ and contains plays common to all my centos servers.

  # Firewall 
  - name: iptables is running and enabled
    service: name=iptables state=started enabled=yes

  - name: Add ferm conf directory
    file: path=/etc/ferm owner=root group=root mode=0700 state=directory

  - name: Set iptables configuration
    template: src=ferm.conf.j2 dest=/etc/ferm/ferm.conf owner=root group=root

  - name: Load iptables configuration
    command: /usr/sbin/ferm /etc/ferm/ferm.conf

  - name: Persist this iptables config across reboots
    command: service iptables save

Second, add a ferm configuration template to your base/templates directory. I recommend you name it ferm.conf.j2. You can use this as-is or modify it fit your needs.

domain ip {
    table filter {
        chain LOGGING;
        chain INPUT {
            policy DROP;

            # connection tracking
            mod state {
                state INVALID DROP;
                state (RELATED ESTABLISHED) ACCEPT;

            # allow local connections
            interface lo ACCEPT;

            # respond to ping
            protocol icmp icmpv6-type 8 ACCEPT;

            # remote administration only allowed from the admin network
            saddr {{ admin_network }}/{{ CIDR }} proto tcp dport ssh ACCEPT;

            # Open incoming TCP ports
            {% if open_tcp_ports is defined %}
            protocol tcp dport ({{ open_tcp_ports|join(' ') }}) ACCEPT;
            {% endif %}

            # Open incoming UDP ports
            {% if open_udp_ports is defined %}
            protocol udp dport ({{ open_udp_ports|join(' ') }}) ACCEPT;
            {% endif %}

            jump LOGGING;
        chain LOGGING {
            mod limit limit 2/min LOG log-prefix 'IPTables-Dropped: ';
        chain FORWARD policy DROP;
        chain OUTPUT policy ACCEPT;

If you don’t want to restrict SSH access to a specific subnet then simply delete the line that begins with saddr. If you do then make sure to add TCP port 22 to the host ports list as described in the third step.

The variables admin_network (e.g., and CIDR (e.g., 24) are global and defined in the group_vars/all file.

The third and final step is to add the ports you want open to each host variable file. If, for example, the host is named then its variables file would be host_vars/ The file is named after the FQDN of the host.

In the host variables file list the TCP and UDP ports you want open for that host. The format is a YAML list like this:

    - 53
    - 3000:4000
    - 53

You can add as many ports as you want. For a port range use the colon notation as shown in this example. If you do not want a TCP port open (besides the default SSH port 22) then omit the open_tcp_ports variable. Likewise if you don’t want any UDP ports open then simply do not include the open_udp_ports variable in your host variables file.


Jinja2 allows you to customize Ansible in some interesting and unusual ways. In fact, with Ansible you actually have two languages: the Ansible YAML DSL and the jinja2 templating language. Mixing them can greatly simplify your playbooks as I am starting to learn.

Categories: DevOps

Tags: , , ,

7 replies

  1. Hi!

    Thank you for your article. I started testing ferm because I was looking for “ansible firewall” and now I’m using ferm.

    But I found your way a bit to complicated, because I have to configure lot of variables in the hosts/groups space.

    When I set up a specific tool with a role (lets say we want to install a jabber-server) I have my role folder and I want to set my firewall-rule inside this role folder. During execution of the ferm-role the variables from other roles are not visible (i think).

    So I set up a “basic” ferm.conf in my server-system basic role and this ferm.conf has the line “@include ‘ferm.d/’;”. Then each role can upload a firewall-rule-file into this directory and handle reloading ferm if any file changes.

    I can provide the complete example if you want to have.


    • That’s a very interesting idea. Can you post the complete example (ferm fragments and Ansible code)? I’d like to see it.

      I experimented a bit with your method and noticed a couple of things. With your method one could create very complex iptables configurations. An admin could create directories that correspond to different tables or chains, for example /etc/ferm/input or /etc/ferm/nat/prerouting then just add numbered fragment in them so they load in the right order 01Default, 02Next, etc. and in the right place. This a powerful approach.

      The problem that I see is the handler. You need a handler to make ferm load the rules and to save them. Ansible doesn’t process handlers across site.yml groups so I think you would need to add a notify with the same handler code for each role. That means ferm may be called multiple times. Perhaps you have a solution for the handler issue? Even with the handler issue this is a good method.

      I look forward to seeing your code. Thanks.


      • Hi, I have written down everything in my personal wiki.

        If there are any comments let me know.


      • Very well done.I think your approach makes a lot of sense. I agree with your intent to make the firewall a role based rather than host based setting. The problem is that Ansible doesn’t support this type of global handler feature, so it’s messy either way.

        In the future when Ansible Galaxy has many useful roles to download (like Chef) this approach will be a problem. Role have to maintain encapsulation just as you point out on your wiki.

        Ansible really needs to add a firewall module. Galaxy isn’t going to work without one. I posted a feature request on their github page.


      • The Ansible devs immediately closed this request without any consideration. This is puzzling and frankly irritating. All the other CM tools support iptables. It seems like an obvious requirement. As much as I like the design of Ansible, cases like this make me question its roadmap.


  2. I explored ferm too because of this article. Still I wanted a little more flexibility…

    So I created a role to manage straight iptables rules if you guys are interested:

    Thanks for the writeup!


Share Your Ideas

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: