using assemble to build iptables rules

So I've been thinking through a clean way to build iptables rules sets
with some re-use between debian/ubuntu/redhat/centos world, and this
is the strategy I've come up with in pseudo code:

in a common bootstrap role:

create /etc/syconfig/iptables.d

in a debian/ubuntu bootstrap role:
install iptables-persist, disable ufw

In any of your application roles, have a template or file with contents like:

-A INPUT -p tcp -m state --state NEW -m tcp --dport 4369 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 6000:7999 -j ACCEPT

that gets dropped into /etc/sysconfig/iptables.d

In a "chinstrap" role (runs after all other roles), use the assemble
module to sum everything in /etc/sysconfig/iptables.d/ together and
place it in /etc/sysconfig/iptables (RedHat) or /etc/iptables/v4.rules
(Ubuntu), and notify a handler to restart iptables if the assembled
file has changed.

The tricky part was the ordering, but that seems ok -- the chinstrap
role creates a segment named 0000-begin that contains all the
necessary beginning entries for the iptables config, and zzzz-end that
contains the ending bits. Everything else gets merged in between
begin & end.

Does this sound kosher? It works in the technical sense. If only
firewalld existed in RHEL < 7.

- James

Yes it does, this is almost the same approach I've taken. There's a
few gotcha's to be aware of, however.

- iptables has multiple tables (filter, nat, mangle, raw and security,
I believe), "filter" is just the default table. If you're
concatenating your fragments together like this, you're locked into
always using filter for your rules. I solved this by:
  * Having per-table begin and end snippets, with the table name
prefixed to the snippet name
  * Having per-table snippets with actual rules, again with the table
name prefixed to the snippet name

- iptables and ip6tables have their own iptables-restore and
ip6tables-restore respectively, so you need two sets of snippets, one
for ipv4 and one for ipv6. Although 90% of your rules will probably
look identical, there are subtle nuances so I would stress not to try
and use the same rules-file for iptables and ip6tables.

- fail2ban [1] and other similar tools that add their own chains, have
these chains wiped out when iptables-restore is run. My "firewall
activate" role fires a handler which restarts fail2ban for this
reason.

I wrote a small module [2] to help with making snippets in other roles
though, as using template or copy for it felt like exposing too much
implementation detail. I still supply the rule in a "raw" form, but
allow the module to abstract away a few things. This way I can just
set present/absent on it, specify the iptables version (4 or 6) as
well as the table (with a default of "filter"). In playbooks, it looks
like this:

- name: Open up firewall ports 80 and 443
  iptables:
    state: present
    ipversion: "{{ item.version }}"
    name: "{{ item.name }}"
    rules: "{{ item.rules }}"
  with_items:
   - name: 50_nginx
     version: 4
     rules: |
       -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
       -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
   - name: 50_nginx
     version: 6
     rules: |
       -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
       -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
  when: nginx_firewall_integration|default(False)

Since in this case (usually it will be this way, but not always) the
rules for 4 and 6 are identical, it can also be condensed to:

- name: Open up firewall ports 80 and 443
  iptables:
    state: present
    ipversion: "{{ item }}"
    name: 50_nginx
    rules: |
       -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
       -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
  with_items: [4,6]
  when: nginx_firewall_integration|default(False)

[1]: http://www.fail2ban.org
[2]: https://gist.github.com/zoni/7655561

This is great stuff, thanks!

Hi James,

If you do not mind using a tool, ferm [1] configuration is very, very close to close to the iptables syntax and includes an @include statement that makes the assemble step unnecessary. Oh, and it has syntax for managing the ipv4/ipv6 duplication.

[1] http://ferm.foo-projects.org/

Regards,
Joost