Software Engineer

managing iptables firewalls with puppet

· by jsnby · Read in about 5 min · (857 Words)
Puppet

One thing I’ve struggled with in puppet in the past was managing iptables firewalls. I used to end up building a few different templates for the various firewalls that I had to manage and just passed in a list of ports to open up, but it was kind of a nightmare to manage as more and more applications with different requirements were added. The number of templates began to sprawl.

I was excited to hear about the puppetlabs-firewall module when I was at PuppetConf. It’s still in development, so there are a few little quirks, but it seems to work fairly well thus far. Let’s try to recreate the default firewall that ships with CentOS that looks something like this:

# Firewall configuration written by system-config-firewall
# Manual customization of this file is not recommended.
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT

Nothing too crazy going on there, just opening up port 22 for ssh access. First off, grab yourself a copy of the puppetlabs-firewall module from github and stick it somewhere in your module path. You will need to turn on pluginsync on your puppet agents if you haven’t done so already by adding this line to the [main] section of puppet.conf:

pluginsync = true

We need to set a couple of global defaults in site.pp for the new Firewall type:

Firewall {
    notify  => Exec["persist-firewall"],
    require => Exec["purge default firewall"],
}

The persist-firewall exec calls iptables-save any time there is an update to a firewall resource so that the change will persist across service and machine restarts. The purge default firewall exec makes sure that the default firewall (the one that is in place at machine install time) gets nuked. This is necessary because the puppetlabs-firewall module doesn’t currently handle having an existing firewall very well.

Now, let’s create a new module called myfirewall. This class will house the basics for our firewall:

class myfirewall {

    $ipv4_file = $operatingsystem ? {
        "debian"          => '/etc/iptables/rules.v4',
        /(RedHat|CentOS)/ => '/etc/sysconfig/iptables',
    }

    exec { "purge default firewall":
        command => "/sbin/iptables -F && /sbin/iptables-save > $ipv4_file && /sbin/service iptables restart",
        onlyif  => "/usr/bin/test `/bin/grep \"Firewall configuration written by\" $ipv4_file | /usr/bin/wc -l` -gt 0",
        user    => 'root',
    }

    /* Make the firewall persistent */
    exec { "persist-firewall":
        command     => "/bin/echo \"# This file is managed by puppet. Do not modify manually.\" > $ipv4_file && /sbin/iptables-save >> $ipv4_file",
        refreshonly => true,
        user        => 'root',
    }

    /* purge anything not managed by puppet */
    resources { 'firewall':
        purge => true,
    }

    firewall { "001 accept all icmp requests":
        proto => 'icmp',
        jump  => 'ACCEPT',
    }

    firewall { '002 INPUT allow loopback':
        iniface => 'lo',
        chain   => 'INPUT',
        jump    => 'ACCEPT',
    }

    firewall { '000 INPUT allow related and established':
        state => ['RELATED', 'ESTABLISHED'],
        jump  => 'ACCEPT',
        proto => 'all',
    }

    firewall { '100 allow ssh':
        state => ['NEW'],
        dport => '22',
        proto => 'tcp',
        jump  => 'ACCEPT',
    }

    firewall { "998 deny all other requests":
        jump   => 'REJECT',
        proto  => 'all',
        reject => 'icmp-host-prohibited',
    }

    firewall { "999 deny all other requests":
        chain  => 'FORWARD',
        jump   => 'REJECT',
        proto  => 'all',
        reject => 'icmp-host-prohibited',
    }
}

The puppetlabs-firewall module is sorting based on the name of each rule, so by specifying the three digit number prefix in the name, we can control the order in which the rules are applied.

I’m grepping for the line Firewall configuration written by in the purge default firewall Exec to determine if we need to throw out the entire iptables config and start fresh. This works for me on CentOS and Fedora, but you might need to adjust it for your distribution.

So, if you run puppet, you should end up with something like this in /etc/sysconfig/iptables:

# This file is managed by puppet. Do not modify manually.
# Generated by iptables-save v1.4.7 on Tue Oct 11 08:56:16 2011
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [1:260]
-A INPUT -m comment --comment "000 INPUT allow related and established" -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -m comment --comment "001 accept all icmp requests" -j ACCEPT
-A INPUT -i lo -p tcp -m comment --comment "002 INPUT allow loopback" -j ACCEPT
-A INPUT -p tcp -m multiport --dports 22 -m comment --comment "100 allow ssh" -m state --state NEW -j ACCEPT
-A INPUT -m comment --comment "998 deny all other requests" -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -m comment --comment "999 deny all other requests" -j REJECT --reject-with icmp-host-prohibited
COMMIT
# Completed on Tue Oct 11 08:56:16 2011

Now, this is all fine and dandy, but how does this help with other applications? I certainly didn’t want to have to put logic in the myfirewall class to handle all of the apps that have firewall needs. Let’s take apache…let’s say that I’ve got a class called httpd that manages apache on my box and it needs to have port 80 opened up. I simply add a firewall resource to the class:

class httpd {

    # ...

    firewall { '100 allow httpd:80':
        state => ['NEW'],
        dport => '80',
        proto => 'tcp',
        jump  => 'ACCEPT',
    }

    # ...
}

Since I don’t really care about the sort order between port 22 and port 80 being opened, I named both rules with a 100; prefix…this should keep both of the rules next to each other in the resulting configuration.

I can now keep all of my firewall resources in the module for each specific application and just include the myfirewall class in one of the base classes that gets applied to every node. On that note, I should probably move the resource that opens port 22 into the module managing my sshd configuration.

I hope this helps get you started with the module. The module has lots of capabilities that I didn’t touch on, so please see the docs on github.