The Puppet Labs Issue Tracker has Moved: https://tickets.puppetlabs.com

IPTables Module

The iptables module illustrated will help administer iptables rules. There are essentially three parts: the module-manifest which declares an iptables class and a defined-type for adding fragments, the rebuild-iptables script that rebuilds the iptables rules when a fragment is added or removed, and an area for fragments. You will also need to create a modules/iptables/files/empty directory to force the purge attribute to work as expected, ie deleting no longer used fragments.

modules/iptables/manifests/init.pp

    # Handles iptables concerns.  See also ipt_fragment definition
    define ipt_fragment($ensure) {
        case $ensure {
            absent: {
                file { "/etc/iptables.d/$name":
                    ensure => absent,
                }
            }
            present: {
                file {
                   "/etc/iptables.d/$name":
                        source => "puppet://puppet/iptables/fragments/$name",
                        notify => Exec[rebuild_iptables],
                }
            }
        }
    }
    
    class iptables {
        package { "iptables":
            ensure => present
        }
    
        exec { "rebuild_iptables":
            command => "/usr/sbin/rebuild-iptables",
            refreshonly => true,
            require => File["/usr/sbin/rebuild-iptables"],
        }
    
        file {
            "/etc/iptables.d":
                ensure => directory,
                purge => true,
                recurse => true,
                force => true,
                source => "puppet:///iptables/empty",
                notify => Exec["rebuild_iptables"];
            "/usr/sbin/rebuild-iptables":
                source => "puppet://puppet/iptables/rebuild-iptables";
        }
    }

modules/iptables/files/rebuild-iptables

    #!/usr/bin/perl -w
    our $ID = q$Id: rebuild-iptables 344 2006-10-04 02:48:30Z digant $;
    #
    # rebuild-iptables -- Construct an iptables rules file from fragments.
    #
    # Written by Russ Allbery 
    # Adapted by Digant C Kasundra 
    # Copyright 2005, 2006 Board of Trustees, Leland Stanford Jr. University
    #
    # Constructs an iptables rules file from the prefix, standard, and suffix
    # files in the iptables configuration area, adding any additional modules
    # specified in the command line, and prints the resulting iptables rules to
    # standard output (suitable for saving into /var/lib/iptables or some other
    # appropriate location on the system).
    
    ##############################################################################
    # Modules and declarations
    ##############################################################################
    
    require 5.006;
    use strict;
    
    use Getopt::Long qw(GetOptions);
    
    # Path to the iptables template area.
    our $TEMPLATE   = '/afs/ir/service/jumpstart/data/iptables';
    
    ##############################################################################
    # Installation
    ##############################################################################
    
    # Return the prefix
    sub prefix {
        my $data;
        ( $data = <<'END_OF_PREFIX' ) =~ s/^\s+//gm;
            *filter
            :INPUT ACCEPT
        :FORWARD ACCEPT
        :OUTPUT ACCEPT
        :SUL -
        -A INPUT -j SUL
        -A SUL -i lo -j ACCEPT
    END_OF_PREFIX
    
        return $data;
    }
    
    # Return the suffix
    sub suffix {
        my $data;
        ( $data = <<'END_OF_SUFFIX' ) =~ s/^\s+//gm;
            # Rejects all remaining connections with port-unreachable errors.
    
            -A SUL -p tcp -m tcp --tcp-flags SYN,RST,ACK SYN -j REJECT --reject-with icmp-port-unreachable
            -A SUL -p udp -j REJECT --reject-with icmp-port-unreachable
            COMMIT
    END_OF_SUFFIX
    
        return $data;
    }
    # Read in a file, processing includes as required.  Returns the contents of
    # the file as an array.
    sub read_iptables {
        my ($file) = @_;
        my @data;
        $file = $TEMPLATE . '/' . $file unless $file =~ m%^\.?/%;
        open my $MODULE, '<', $file or die "$0: cannot open $file: $!\n";
        local $_;
        while (<$MODULE>) {
            if (/^\s*include\s+(\S+)$/) {
                my $included = $1;
                $included = $TEMPLATE . '/' . $included
                    unless $included =~ m%^\.?/%;
                if ($file eq $included) {
                    die "$0: include loop in $file, line $.\n";
                }
                push (@data, "\n");
                push (@data, read_iptables ($included));
                push (@data, "\n");
            } elsif (/^\s*include\s/) {
                die "$0: malformed include line in $file, line $.\n";
            } else {
                push (@data, $_);
            }
        }
        close $MODULE;
        return @data;
    }
    
    # Write a file carefully.
    # Consider using File::Temp
    sub write_iptables {
        my ($file, @data) = @_;
        open my $NEW, '>', "$file.new" or die "$0: cannot create $file.new: $!\n";
        print $NEW @data           or die "$0: cannot write to $file.new: $!\n";
        close $NEW                 or die "$0: cannot flush $file.new: $!\n";
        rename ("$file.new", $file)
            or die "$0: cannot install new $file: $!\n";
    }
    
    # Install iptables on a Red Hat system.  Takes the array containing the new
    # iptables data.
    sub install_redhat {
        my (@data) = @_;
        write_iptables ('/etc/sysconfig/iptables', @data);
        system('/sbin/service', 'iptables', 'restart');
    }
    
    # Install iptables on a Debian system.  Take the array containing the new
    # iptables data.
    sub install_debian {
        my (@data) = @_;
        unless (-d '/etc/iptables') {
            mkdir ('/etc/iptables', 0755)
                or die "$0: cannot mkdir /etc/iptables: $!\n";
        }
        write_iptables ('/etc/iptables/general', @data);
        system('/sbin/iptables-restore < /etc/iptables/general');
    }
    
    ##############################################################################
    # Main routine
    ##############################################################################
    
    # Fix things up for error reporting.
    $| = 1;
    my $fullpath = $0;
    $0 =~ s%.*/%%;
    
    # Parse command-line options.
    my ($help, $version);
    Getopt::Long::config ('bundling', 'no_ignore_case');
    GetOptions ('h|help'             => \$help,
                'v|version'          => \$version) or exit 1;
    if ($help) {
        print "Feeding myself to perldoc, please wait....\n";
        exec ('perldoc', '-t', $fullpath);
    } elsif ($version) {
        my $version = join (' ', (split (' ', $ID))[1..3]);
        $version =~ s/,v\b//;
        $version =~ s/(\S+)$/($1)/;
        $version =~ tr%/%-%;
        print $version, "\n";
        exit;
    }
    my @modules;
    
    if ( -d '/etc/iptables.d' ) {
        @modules = ;
    }
    
    # Concatenate everything together.
    my @data;
    push (@data, prefix());
    push (@data, "\n");
    for my $module (@modules) {
        push (@data, read_iptables($module));
        push (@data, "\n");
    }
    push (@data, suffix());
    
    if (-f '/etc/debian_version') {
        install_debian (@data);
    } elsif (-f '/etc/redhat-release') {
        install_redhat (@data);
    } else {
        die "$0: cannot figure out whether this is Red Hat or Debian\n";
    }
    
    exit 0;
    __END__
    
    ##############################################################################
    # Documentation
    ##############################################################################
    
    =head1 NAME
    
    rebuild-iptables - Construct an iptables rules file from fragments
    
    =head1 SYNOPSIS
    
    rebuild-iptables [B<-hv>]
    
    =head1 DESCRIPTION
    
    B constructs an iptables configuration file by concatenating
    various modules found in F.  The resulting iptables
    configuration file is written to the appropriate file for either Red Hat or
    Debian (determined automatically) and iptables is restarted.
    
    Each module is just a text file located in the directory mentioned above that
    contains one or more iptables configuration lines (basically the arguments to
    an B invocation), possibly including comments.
    
    Along with the modules in the directory specified, a standard prefix and suffix
    is added.
    
    Normally, the contents of each module are read in verbatim, but a module may
    also contain the directive:
    
        include 
    
    on a separate line, where  is the path to another module to include,
    specified the same way as modules given on the command line (hence, either a
    file name relative to F or an
    absolute path).  Such a line will be replaced with the contents of the named
    file.  Be careful when using this directive to not create loops; files
    including themselves will be detected, but more complex loops will not and
    will result in infinite output.
    
    =head1 OPTIONS
    
    =over 4
    
    =item B<-h>, B<--help>
    
    Print out this documentation (which is done simply by feeding the script to
    C).
    
    =item B<-v>, B<--version>
    
    Print out the version of B and exit.
    
    =back
    
    =head1 FILES
    
    =over 4
    
    =item F
    
    The default module location.
    
    =item F
    
    If this file exists, the system is assumed to be a Debian system for
    determining the installation location when B<-i> is used.
    
    =item F
    
    The install location of the generated configuration file on Debian.
    
    =item F
    
    If this file exists, the system is assumed to be a Red Hat system for
    determining the installation location when B<-i> is used.
    
    =item F
    
    The install location of the generated configuration file on Red Hat.
    
    =back
    
    =head1 AUTHOR
    
    Russ Allbery 
    Digant C Kasundra 
    
    =head1 SEE ALSO
    
    iptables(8)
    
    =cut

modules/iptables/files/fragments/ftp (sample)

    # FTP setup, requiring passive mode.
    -A SUL -s 171.64.0.0/14 -p tcp -m tcp --dport 20 --syn -j ACCEPT
    -A SUL -s 172.24.0.0/14 -p tcp -m tcp --dport 20 --syn -j ACCEPT
    -A SUL -s 171.64.0.0/14 -p tcp -m tcp --dport 21 --syn -j ACCEPT
    -A SUL -s 172.24.0.0/14 -p tcp -m tcp --dport 21 --syn -j ACCEPT

Discussion

Thanks for the recipe, I have taken what you have done here and modified it to be more versatile. The rebuild-iptables has been modified. this is the diff output from your original to my modified, I have also included the modified version as an attachment.

Main changes include:

  • removed the suffix dropping all traffic to a drop table
  • added type to prefix as we now handle table type (ie filter or mangle)
  • added support to have comments/whitespace to fragments
  • changed module globbing to use filename prefix of filter- or mangle- this is so we can have rules for both tables in multiple modules
  • added some documentation to reflect the filename requirements
    --- rebuild-iptables        2008-07-02 10:06:43.000000000 +1000
    +++ rebuild-iptables-new    2008-07-02 10:04:43.000000000 +1000
    @@ -31,17 +31,24 @@
    
     # Return the prefix
     sub prefix {
    +    my $type = shift;
         my $data;
    -    ( $data = <<'END_OF_PREFIX' ) =~ s/^\s+//gm;
    -        *filter
    -        :INPUT ACCEPT
    -    :FORWARD ACCEPT
    -    :OUTPUT ACCEPT
    -    :SUL -
    -    -A INPUT -j SUL
    -    -A SUL -i lo -j ACCEPT
    +   if ( $type eq 'filter' ) {
    +           ( $data = <<'END_OF_PREFIX' ) =~ s/^\s+//gm;
    +*filter
    +:INPUT ACCEPT
    +:FORWARD ACCEPT
    +:OUTPUT ACCEPT
     END_OF_PREFIX
    -
    +   }
    +   elsif ( $type eq 'mangle' ) {
    +           ( $data = <<'END_OF_PREFIX' ) =~ s/^\s+//gm;
    +*nat
    +:PREROUTING ACCEPT
    +:POSTROUTING ACCEPT
    +:OUTPUT ACCEPT
    +END_OF_PREFIX
    +   }
         return $data;
     }
    
    @@ -49,10 +56,6 @@
     sub suffix {
         my $data;
         ( $data = <<'END_OF_SUFFIX' ) =~ s/^\s+//gm;
    -        # Rejects all remaining connections with port-unreachable errors.
    -
    -        -A SUL -p tcp -m tcp --tcp-flags SYN,RST,ACK SYN -j REJECT --reject-with icmp-port-unreachable
    -        -A SUL -p udp -j REJECT --reject-with icmp-port-unreachable
             COMMIT
     END_OF_SUFFIX
    
    @@ -80,7 +83,12 @@
             } elsif (/^\s*include\s/) {
                 die "$0: malformed include line in $file, line $.\n";
             } else {
    -            push (@data, $_);
    +            # strip comments/whitespace/blank lines out of module
    +            $_ =~ s/\s*#.*$//;
    +            $_ =~ s/^\s*//;
    +            if ( $_ !~ /^\s*$/ ) {
    +                push (@data, $_);
    +            }
             }
         }
         close $MODULE;
    @@ -143,17 +151,30 @@
         print $version, "\n";
         exit;
     }
    -my @modules;
    +my ( @filter_modules, @mangle_modules );
    
     if ( -d '/etc/iptables.d' ) {
    -    @modules = ;
    +    @filter_modules = ;
    +    @mangle_modules = ;
     }
    
     # Concatenate everything together.
     my @data;
    -push (@data, prefix());
    +push (@data, prefix('filter'));
     push (@data, "\n");
    -for my $module (@modules) {
    +for my $module (@filter_modules) {
    +    push (@data, read_iptables($module));
    +    push (@data, "\n");
    +}
    +push (@data, suffix());
    +
    +push (@data, prefix('mangle'));
    +push (@data, "\n");
    +for my $module (@mangle_modules) {
         push (@data, read_iptables($module));
         push (@data, "\n");
     }
    @@ -193,6 +214,13 @@
     contains one or more iptables configuration lines (basically the arguments to
     an B invocation), possibly including comments.
    
    +NOTE: the module name needs to be prefixed with either filter- or mangle- . For
    +example: /etc/iptables.d/filter-foo. This is required so the rules can be put
    +in the appropriate table.
    +
    +WARNING: if the module name is not prefixed with filter- or mangle- it WILL be
    +ignored.
    +
     Along with the modules in the directory specified, a standard prefix and suffix
     is added.

Alternate method

Here is a simpler alternative so far tested on Debian. It simply copies a rules file (“hostname.rules”) into place and refreshes iptables.

class iptables {
  package {
    "iptables-persistent": ensure => present;
  }

  service { "iptables-persistent":
    require => Package["iptables-persistent"],

    # Because there is no running process for this service, the normal status
    # checks fail.  Because puppet then thinks the service has been manually
    # stopped, it won't restart it.  This fake status command will trick puppet
    # into thinking the service is *always* running (which in a way it is, as
    # iptables is part of the kernel.)
    hasstatus => true,
    status => "true",

    # Under Debian, the "restart" parameter does not reload the rules, so tell
    # Puppet to fall back to stop/start, which does work.
    hasrestart => false,

  }

  file {
    "/etc/iptables/rules":
      owner   => "root",
      group   => "root",
      mode    => 640,
      source  => "puppet:///modules/iptables/$hostname.rules",
      require => Package["iptables-persistent"],

      # When this file is updated, make sure the rules get reloaded.
      notify  => Service["iptables-persistent"],
    ;
  }

}