Managing iptables with Puppet

7,089

Solution 1

Here's what I'm doing with Red Hat Enterprise (RHEL).

RHEL has an iptables service that loads rules from /etc/sysconfig/iptables and I'm working with modifying that file and restarting the iptables service. Many people like to drop fragments into an iptables.d directory and build an iptables (via make or something like that) ruleset from that. I include stuff for rebuilding the default ruleset, but that usually never does anything. If your needs are simple you could just copy an iptables file to the system.

Despite how ugly this seems, it's quite thoroughly tested on RHEL4, RHEL5 and RHEL6.

I had this going before augeas support was in puppet. If I was writing it again today I'd look at the augeas iptables lens before resorting to exec { "perl ...": }.

Some global defines for editing files in place

Based on stuff originally from http://reductivelabs.com/trac/puppet/wiki/SimpleTextRecipes

# Ensure that the line "line" exists in "file":
# Usage: 
# append_if_no_such_line { dummy_modules:
#        file => "/etc/modules",
#        line => dummy 
# }
# 
define append_if_no_such_line($file, $line, $refreshonly = 'false') {
   exec { "/bin/echo '$line' >> '$file'":
      unless => "/bin/grep -Fxqe '$line' '$file'",
      refreshonly => $refreshonly,
   }
}

# Ensure that the line "line" exists in "file":
# Usage: 
# prepend_if_no_such_line { dummy_modules:
#        file => "/etc/modules",
#        line => dummy 
# }
# 
define prepend_if_no_such_line($file, $line, $refreshonly = 'false') {
   $line_no_slashes = slash_escape($line)
   exec { "/usr/bin/perl -p0i -e 's/^/$line_no_slashes\n/;' '$file'":
      unless => "/bin/grep -Fxqe '$line' '$file'",
      refreshonly => $refreshonly,
   }
}

define insert_line_after_if_no_such_line($file, $line, $after) {
    $line_no_slashes = slash_escape($line)
    $after_no_slashes = slash_escape($after)

    exec { "/usr/bin/perl -p0i -e 's/^($after_no_slashes)\$/\$1\n$line_no_slashes/m' '$file'":
        onlyif => "/usr/bin/perl -ne 'BEGIN { \$ret = 0; } \$ret = 1 if /^$line_no_slashes/; END { exit \$ret; }' '$file'",
    }
}

define insert_line_before_if_no_such_line($file, $line, $beforeline) {
    $line_no_slashes = slash_escape($line)
    $before_no_slashes = slash_escape($beforeline)

    exec { "/usr/bin/perl -p0i -e 's/^($before_no_slashes)\$/$line_no_slashes\n\$1/m' '$file'":
        onlyif => "/usr/bin/perl -ne 'BEGIN { \$ret = 0; } \$ret = 1 if /^$line_no_slashes/; END { exit \$ret; }' '$file'",
    }
}

My iptables class:

class iptables {
   if $lsbmajdistrelease >= '6' {
     $primarychain = 'INPUT'
   } else {
     $primarychain = 'RH-Firewall-1-INPUT'
   }

   package {
      iptables: 
         ensure => installed   # "latest" would be too much
   }

   service { 
     iptables:
        enable    => true,    # default on
        ensure    => running, # start it up if it's stopped
        hasstatus => true,    # since there's no daemon
  }


   file {
     "/etc/sysconfig/iptables":
       ensure => present;
   }

   ##
   # Build up a config if it's missing components we expect; should
   # automatically repair a config if it's broken for really simple reasons
   ##

   # Very first thing: a comment at the top warning about our evil; add even if
   # we're not touching anything else...
   prepend_if_no_such_line { 
      "/etc/sysconfig/iptables comment":
         file => "/etc/sysconfig/iptables",
         line => "# This file partially managed by puppet; attempts to edit will result in magic reappearances"
   }

   # start
   # *filter
   insert_line_after_if_no_such_line {
      "/etc/sysconfig/iptables *filter":
         file    => "/etc/sysconfig/iptables",
         line    => "\\*filter",
         after   => "#.*",
         notify => Service[iptables],
   }

   # first default chain
   # :INPUT ACCEPT [0:0]
   insert_line_after_if_no_such_line {
      "/etc/sysconfig/iptables:INPUT":
         file   => "/etc/sysconfig/iptables",
         line   => ":INPUT ACCEPT \\[0:0\\]",
         after  => "\\*filter",
         notify => Service[iptables],
   }

   # second default chain
   # :FORWARD ACCEPT [0:0]
   insert_line_after_if_no_such_line {
      "/etc/sysconfig/iptables:FORWARD":
         file   => "/etc/sysconfig/iptables",
         line   => ":FORWARD ACCEPT \\[0:0\\]",
         after  => ":INPUT ACCEPT \\[\\d+:\\d+\\]",
         notify => Service[iptables],
   }


   # third default chain
   # :OUTPUT ACCEPT [0:0]
   insert_line_after_if_no_such_line {
      "/etc/sysconfig/iptables:OUTPUT":
         file   => "/etc/sysconfig/iptables",
         line   => ":OUTPUT ACCEPT \\[0:0\\]",
         after  => ":FORWARD ACCEPT \\[\\d+:\\d+\\]",
         notify => Service[iptables],
   }

   if $lsbmajdistrelease <= 5 {

      # Finally, the RH special chain
      # :RH-Firewall-1-INPUT - [0:0]
      insert_line_after_if_no_such_line {
         "/etc/sysconfig/iptables:RH-Firewall-1-INPUT":
            file   => "/etc/sysconfig/iptables",
            line   => ":RH-Firewall-1-INPUT - \\[0:0\\]",
            after  => ":OUTPUT ACCEPT \\[\\d+:\\d+\\]",
            notify => Service[iptables],
      }

      # redirect INPUT to RH chain
      # -A INPUT -j RH-Firewall-1-INPUT
      insert_line_after_if_no_such_line {
         "/etc/sysconfig/iptables:INPUT:RH-Firewall-1-INPUT":
            file   => "/etc/sysconfig/iptables",
            line   => "-A INPUT -j RH-Firewall-1-INPUT",
            after  => ":RH-Firewall-1-INPUT - \\[\\d+:\\d+\\]",
            notify => Service[iptables],
      }

      # redirect FORWARD to RH chain
      # -A FORWARD -j RH-Firewall-1-INPUT
      insert_line_after_if_no_such_line { 
         "/etc/sysconfig/iptables:FORWARD:RH-Firewall-1-INPUT":
            file   => "/etc/sysconfig/iptables",
            line   => "-A FORWARD -j RH-Firewall-1-INPUT",
            after  => "-A INPUT -j RH-Firewall-1-INPUT",
            notify => Service[iptables],
      }

   }

   # Let anything on localhost work...
   # -A $primarychain -i lo -j ACCEPT
   insert_line_after_if_no_such_line {
      "/etc/sysconfig/iptables:$primarychain lo":
         file    => "/etc/sysconfig/iptables",
         line   => "-A $primarychain -i lo -j ACCEPT",
         after  => "-A FORWARD -j $primarychain",
         notify => Service[iptables],
   }

   # And let through all the ICMP stuff:
   # -A $primarychain -p icmp --icmp-type any -j ACCEPT
   if $lsbmajdistrelease >= '6' {
     insert_line_after_if_no_such_line {
        "/etc/sysconfig/iptables:$primarychain icmp":
           file   => "/etc/sysconfig/iptables",
           line   => "-A $primarychain -p icmp -j ACCEPT",
           after  => "-A $primarychain -i lo -j ACCEPT",
           notify => Service[iptables],
     }
   } else {
     insert_line_after_if_no_such_line {
        "/etc/sysconfig/iptables:$primarychain icmp":
           file   => "/etc/sysconfig/iptables",
           line   => "-A $primarychain -p icmp --icmp-type any -j ACCEPT",
           after  => "-A $primarychain -i lo -j ACCEPT",
           notify => Service[iptables],
     }
   }

   # Finally, let anything that's part of an exisiting connection through:
   # -A $primarychain -m state --state ESTABLISHED,RELATED -j ACCEPT
   insert_line_after_if_no_such_line {
      "/etc/sysconfig/iptables:ESTABLISHED":
         file   => "/etc/sysconfig/iptables",
         line   => "-A $primarychain -m state --state ESTABLISHED,RELATED -j ACCEPT",
         after  => "-A $primarychain -p icmp --icmp-type any -j ACCEPT",
         notify => Service[iptables],
   }

   # Very last thing:
   # COMMIT
   append_if_no_such_line {
      "/etc/sysconfig/iptables:COMMIT":
         file   => "/etc/sysconfig/iptables",
         line   => "COMMIT",
         notify => Service[iptables],
   }

   # Next to last thing: reject!
   # -A $primarychain -j REJECT --reject-with icmp-host-prohibited
   insert_line_before_if_no_such_line {
      "/etc/sysconfig/iptables:final reject":
         file       => "/etc/sysconfig/iptables",
         line       => "-A $primarychain -j REJECT --reject-with icmp-host-prohibited",
         beforeline => "COMMIT",
         notify     => Service[iptables],
   }
}

# example:
# iptable_rule { "iptable:ssh":
#   rule => "-m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT"
# }
# change your mind about a rule, do this:
# iptable_rule { "iptable:ssh":
#   rule   => "-m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT",
#   ensure => "absent",
# }
define iptable_rule($rule, $ensure = 'present') {
   if $lsbmajdistrelease >= '6' {
     $primarychain = 'INPUT'
   } else {
     $primarychain = 'RH-Firewall-1-INPUT'
   }
   $iptablesline = "-A $primarychain $rule"
   case $ensure {
      default: { err ( "unknown ensure value $ensure" ) }
      present: {
         insert_line_before_if_no_such_line {
            "/etc/sysconfig/iptables:add $rule":
               file       => "/etc/sysconfig/iptables",
               line       => $iptablesline,
               beforeline => "-A $primarychain -j REJECT --reject-with icmp-host-prohibited",
               notify     => Service[iptables],
         }
      }
      absent: {
         delete_lines {
            "/etc/sysconfig/iptables:remove $rule":
               file    => "/etc/sysconfig/iptables",
               pattern => $iptablesline,
               notify  => Service[iptables],
         }
      }
   }
}

# Example:
# iptable_tcp_port { "iptable:ssh":
#    port => "22",
# }
# Example:
# iptable_tcp_port { "iptable:oracle:130.157.5.0/24":
#    port    => "1521",
#    source => "130.157.5.0/24",
# }
# (add ensure => "absent" to remove)
define iptable_tcp_port($port, $ensure = 'present', $source = 'ANY') {
   case $source {
      "ANY": {
         iptable_rule {
            "iptable_tcp_port:$port":
               rule   => "-m state --state NEW -m tcp -p tcp --dport $port -j ACCEPT",
               ensure => $ensure,
         }
      }
      default: {
         iptable_rule {
            "iptable_tcp_port:$port:$source":
               rule   => "-m state --state NEW -m tcp -p tcp --source $source --dport $port -j ACCEPT",
               ensure => $ensure,
         }
      }
   }
}

# Example:
# iptable_udp_port { "iptable:ntp":
#    port => "123",
# }
# (again, ensure => "absent" if needed)
define iptable_udp_port($port, $ensure = 'present', $source = 'ANY') {
   case $source {
      "ANY": {
         iptable_rule {
            "iptable_udp_port:$port":
               rule   => "-p udp -m udp --dport $port -j ACCEPT",
               ensure => $ensure,
         }
      }
      default: {
         iptable_rule {
            "iptable_udp_port:$port":
               rule   => "-p udp -m udp --source $source --dport $port -j ACCEPT",
               ensure => $ensure,
         }
      }
   }
}

Some examples of usage in other classes:

class ssh {
  include iptables
  iptable_tcp_port {
    "iptables:ssh":
      port   => "22",
      ensure => "present"
   }
}
class ssh_restricted inherits ssh {
  Iptable_tcp_port["iptables:ssh"]{ensure => "absent"}
  iptable_tcp_port {
    "ssh:RESTRICTED":
      port   => "22",
      source => "X.Y.0.0/16", 
      ensure => "present";
   }
}

class apache {
  iptable_tcp_port {
    "iptables:http":
      require => Service["httpd"],
      port => "80";
  }
}

class apache::secure {
  iptable_tcp_port {
    "iptables:https":
      require => Service["httpd"],
      port => "443";
  }
}

class snmp {
  iptable_udp_port { "iptables:snmp": port => "161" }
}

Solution 2

Puppet Labs has an example right in their wiki: Module Iptables Patterns

In short: You create fragments for each service and then install them by invoking the ipt_fragment defined-type:

ipt_fragment { "filter-ftp": ensure => present }

When a fragment is installed (they're in /etc/iptables.d/), it triggers the execution of a script that concatenates all the fragments and restarts iptables.

Solution 3

The question is, what do you intend to accomplish?

Putting iptables on Puppet is easy: put the script on the puppet server, and serve it to wherever you need it. If it needs some customization, make it a template.

Now, perhaps you want something like "If host X has WebServer, then ports 80 and 443 to it must be open". In that case, what I suggest you to do is to compose the script from multiple file parts, using Common module's concatenated_file or concatfilepart (I prefer the latter, which enables ordering, but it is not available on all forks -- I'm point you to camptocamp's, which has it).

These file parts can be easily be written using templates. The trick is that you export the concatfilepart on the Apache class (possibly by calling a define that prepares a concatfilepart based on parameters such as IP address and port), and on the iptables class you'd realize all the exported concatfilepart tagged with iptables or something like that.

If you do that, I'd love to see that module on github. I never got to write an iptables module. :-)

Share:
7,089
NukaRakuForgotEmail
Author by

NukaRakuForgotEmail

Learning and helping.

Updated on September 18, 2022

Comments

  • NukaRakuForgotEmail
    NukaRakuForgotEmail over 1 year

    The idea of managing our iptables rules with Puppet has been brought up. I see that augeas has an iptables lense but it is currently experimental.

    Does anyone have any suggestions as to how to handle this? Ideally, I'd like to construct the chains based on a server's class.

    • Admin
      Admin over 12 years
      Which flavor of Linux? Some have some default iptables stuff that's a good idea to work with.
    • Admin
      Admin over 12 years
      RHEL in my situation.