]> gitweb.fluxo.info Git - puppet-ferm.git/commitdiff
add ability to define rules in tables != filter
authorThore Bödecker <me@foxxx0.de>
Tue, 3 Sep 2019 09:56:58 +0000 (11:56 +0200)
committerThore Bödecker <me@foxxx0.de>
Wed, 11 Sep 2019 11:20:35 +0000 (13:20 +0200)
Previously it was neither possible to properly define custom chains nor
to define rules in tables other than the default filter table.
For various legitimate reasons it can be required to define rules in the
raw, nat or mangle tables, e.g. to use NOTRACK or to configure
DNAT/SNAT/MASQUERADE.
Additionally it might come in handy to define custom chains to group
certain rules and allow a more efficient evaluation for incoming packets
by not cramming all rules into the filter/INPUT chain so that
(worst-case) all packets need to traverse and evaluate all rules.

I have tried to maintain backwards compatibility and to not change
default filenames/paths so that it won't result in leftover obsolete unmaged
files from previous versions of this module.

In order to improve the naming schema the rule $policy has been renamed
to $action, however both parameters are available and optional now, with
some sanity checks that require at most one of them and issueing a
warning() for users of the now deprecated $policy parameter.

All previous tests have been adapted to the changes, a long with an
additional set of tests for the new feature.

Fixes #61

17 files changed:
REFERENCE.md
manifests/chain.pp
manifests/config.pp
manifests/init.pp
manifests/rule.pp
spec/acceptance/ferm_spec.rb
spec/classes/ferm_spec.rb
spec/defines/chain_spec.rb
spec/defines/rule_spec.rb
spec/spec_helper.rb
templates/ferm-table-chain-config-include.epp [new file with mode: 0644]
templates/ferm.conf.epp
templates/ferm_chain_header.conf.epp
templates/ferm_header.conf.epp
types/actions.pp [new file with mode: 0644]
types/policies.pp
types/tables.pp [new file with mode: 0644]

index 39ba310c68e80acb6fb47d62901258adf4dfafaf..19ffae060ebb9ccd50ddebf2ea121741f4dcdd11 100644 (file)
@@ -17,13 +17,15 @@ _Private Classes_
 
 **Defined types**
 
-* [`ferm::chain`](#fermchain): defined resource which creates all rules for one chain
-* [`ferm::rule`](#fermrule): defined resource which creates a single rule in a specific chain
+* [`ferm::chain`](#fermchain): This defined resource manages ferm/iptables chains
+* [`ferm::rule`](#fermrule): This defined resource manages a single rule in a specific chain
 
 **Data types**
 
-* [`Ferm::Policies`](#fermpolicies): a list of allowed default policies for a chain
+* [`Ferm::Actions`](#fermactions): a list of allowed actions for a rule
+* [`Ferm::Policies`](#fermpolicies): a list of allowed policies for a chain
 * [`Ferm::Protocols`](#fermprotocols): a list of allowed protocolls to match
+* [`Ferm::Tables`](#fermtables): a list of available tables
 
 ## Classes
 
@@ -123,7 +125,7 @@ Data type: `Ferm::Policies`
 
 Default policy for the FORWARD chain
 Default value: DROP
-Allowed values: (ACCEPT|DROP|REJECT)
+Allowed values: (ACCEPT|DROP)
 
 ##### `output_policy`
 
@@ -131,7 +133,7 @@ Data type: `Ferm::Policies`
 
 Default policy for the OUTPUT chain
 Default value: ACCEPT
-Allowed values: (ACCEPT|DROP|REJECT)
+Allowed values: (ACCEPT|DROP)
 
 ##### `input_policy`
 
@@ -139,7 +141,7 @@ Data type: `Ferm::Policies`
 
 Default policy for the INPUT chain
 Default value: DROP
-Allowed values: (ACCEPT|DROP|REJECT)
+Allowed values: (ACCEPT|DROP)
 
 ##### `rules`
 
@@ -193,17 +195,23 @@ Example: {'nat' => ['PREROUTING', 'POSTROUTING']}
 
 ### ferm::chain
 
-defined resource which creates all rules for one chain
+This defined resource manages ferm/iptables chains
 
-#### Parameters
+#### Examples
 
-The following parameters are available in the `ferm::chain` defined type.
+##### create a custom chain, e.g. for all incoming SSH connections
 
-##### `policy`
+```puppet
+ferm::chain{'check-ssh':
+  chain               => 'SSH',
+  disable_conntrack   => true,
+  log_dropped_packets => true,
+}
+```
 
-Data type: `Ferm::Policies`
+#### Parameters
 
-Set the default policy for a CHAIN
+The following parameters are available in the `ferm::chain` defined type.
 
 ##### `disable_conntrack`
 
@@ -211,23 +219,70 @@ Data type: `Boolean`
 
 Disable/Enable usage of conntrack
 
+##### `log_dropped_packets`
+
+Data type: `Boolean`
+
+Enable/Disable logging of packets to the kernel log, if no explicit chain matched
+
+##### `policy`
+
+Data type: `Optional[Ferm::Policies]`
+
+Set the default policy for CHAIN (works only for builtin chains)
+Default value: undef
+Allowed values: (ACCEPT|DROP) (see Ferm::Policies type)
+
+Default value: `undef`
+
 ##### `chain`
 
 Data type: `String[1]`
 
 Name of the chain that should be managed
+Default value: $name (resource name)
+Allowed values: String[1]
 
 Default value: $name
 
-##### `log_dropped_packets`
+##### `table`
 
-Data type: `Boolean`
+Data type: `Ferm::Tables`
 
-Enable/Disable logging of packets to the kernel log, if no explicit chain matched
+Select the target table (filter/raw/mangle/nat)
+Default value: 'filter'
+Allowed values: (filter|raw|mangle|nat) (see Ferm::Tables type)
+
+Default value: 'filter'
 
 ### ferm::rule
 
-defined resource which creates a single rule in a specific chain
+This defined resource manages a single rule in a specific chain
+
+#### Examples
+
+##### Jump to the 'SSH' chain for all incoming SSH traffic (see chain.pp examples on how to create the chain)
+
+```puppet
+ferm::rule{'incoming-ssh':
+  chain  => 'INPUT',
+  action => 'SSH',
+  proto  => 'tcp',
+  dport  => '22',
+}
+```
+
+##### Create a rule in the 'SSH' chain to allow connections from localhost
+
+```puppet
+ferm::rule{'allow-ssh-localhost':
+  chain  => 'SSH',
+  action => 'ACCEPT',
+  proto  => 'tcp',
+  dport  => '22',
+  saddr  => '127.0.0.1',
+}
+```
 
 #### Parameters
 
@@ -239,12 +294,6 @@ Data type: `String[1]`
 
 Configure the chain where we want to add the rule
 
-##### `policy`
-
-Data type: `Ferm::Policies`
-
-Configure what we want to do with the packet (drop, accept, log...)
-
 ##### `proto`
 
 Data type: `Ferm::Protocols`
@@ -259,6 +308,26 @@ A comment that will be added to the ferm config and to ip{,6}tables
 
 Default value: $name
 
+##### `action`
+
+Data type: `Optional[Ferm::Actions]`
+
+Configure what we want to do with the packet (drop/accept/reject, can also be a target chain name)
+Default value: undef
+Allowed values: (RETURN|ACCEPT|DROP|REJECT|NOTRACK|LOG|MARK|DNAT|SNAT|MASQUERADE|REDIRECT|String[1])
+
+Default value: `undef`
+
+##### `policy`
+
+Data type: `Optional[Ferm::Policies]`
+
+Configure what we want to do with the packet (drop/accept/reject, can also be a target chain name) [DEPRECATED]
+Default value: undef
+Allowed values: (RETURN|ACCEPT|DROP|REJECT|NOTRACK|LOG|MARK|DNAT|SNAT|MASQUERADE|REDIRECT|String[1])
+
+Default value: `undef`
+
 ##### `dport`
 
 Data type: `Optional[Variant[Stdlib::Port,String[1]]]`
@@ -315,13 +384,29 @@ Set the rule to present or absent
 
 Default value: 'present'
 
+##### `table`
+
+Data type: `Ferm::Tables`
+
+Select the target table (filter/raw/mangle/nat)
+Default value: filter
+Allowed values: (filter|raw|mangle|nat) (see Ferm::Tables type)
+
+Default value: 'filter'
+
 ## Data types
 
+### Ferm::Actions
+
+As you can also *jump* to other chains, each chain-name is also a valid action/target
+
+Alias of `Variant[Enum['RETURN', 'ACCEPT', 'DROP', 'REJECT', 'NOTRACK', 'LOG', 'MARK', 'DNAT', 'SNAT', 'MASQUERADE', 'REDIRECT'], String[1]]`
+
 ### Ferm::Policies
 
-a list of allowed default policies for a chain
+a list of allowed policies for a chain
 
-Alias of `Enum['ACCEPT', 'DROP', 'REJECT']`
+Alias of `Enum['ACCEPT', 'DROP']`
 
 ### Ferm::Protocols
 
@@ -329,3 +414,9 @@ a list of allowed protocolls to match
 
 Alias of `Enum['icmp', 'tcp', 'udp', 'udplite', 'icmpv6', 'esp', 'ah', 'sctp', 'mh', 'all']`
 
+### Ferm::Tables
+
+a list of available tables
+
+Alias of `Enum['raw', 'mangle', 'nat', 'filter']`
+
index 1198f62d1f06e96e66aa1b42f6cb80702f95b217..a01b9b440b4eeed297869023ca682824cf14c2e3 100644 (file)
@@ -1,23 +1,57 @@
-# defined resource which creates all rules for one chain
-# @param policy Set the default policy for a CHAIN
+# @summary This defined resource manages ferm/iptables chains
+#
+# @example create a custom chain, e.g. for all incoming SSH connections
+#   ferm::chain{'check-ssh':
+#     chain               => 'SSH',
+#     disable_conntrack   => true,
+#     log_dropped_packets => true,
+#   }
+#
 # @param disable_conntrack Disable/Enable usage of conntrack
-# @param chain Name of the chain that should be managed
 # @param log_dropped_packets Enable/Disable logging of packets to the kernel log, if no explicit chain matched
+# @param policy Set the default policy for CHAIN (works only for builtin chains)
+#   Default value: undef
+#   Allowed values: (ACCEPT|DROP) (see Ferm::Policies type)
+# @param chain Name of the chain that should be managed
+#   Default value: $name (resource name)
+#   Allowed values: String[1]
+# @param table Select the target table (filter/raw/mangle/nat)
+#   Default value: 'filter'
+#   Allowed values: (filter|raw|mangle|nat) (see Ferm::Tables type)
 define ferm::chain (
-  Ferm::Policies $policy,
   Boolean $disable_conntrack,
   Boolean $log_dropped_packets,
-  String[1] $chain = $name,
+  String[1] $chain                 = $name,
+  Optional[Ferm::Policies] $policy = undef,
+  Ferm::Tables $table              = 'filter',
 ) {
+  # prevent unmanaged files due to new naming schema
+  # keep the default "filter" chains in the original location
+  # only prefix chains in other tables with the table name
+  if $table == 'filter' and $chain in ['INPUT', 'FORWARD', 'OUTPUT'] {
+    $filename = "${ferm::configdirectory}/chains/${chain}.conf"
+  } else {
+    $filename = "${ferm::configdirectory}/chains/${table}-${chain}.conf"
+  }
+
+  $builtin_chains = {
+    'raw'    => ['PREROUTING', 'OUTPUT'],
+    'nat'    => ['PREROUTING', 'INPUT', 'OUTPUT', 'POSTROUTING'],
+    'mangle' => ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT', 'POSTROUTING'],
+    'filter' => ['INPUT', 'FORWARD', 'OUTPUT'],
+  }
+
+  if $policy and ! ($chain in $builtin_chains[$table]) {
+    fail("Can only set a default policy for builtin chains. '${chain}' is not a builtin chain.")
+  }
 
   # concat resource for the chain
-  $filename = downcase($chain)
-  concat{"${ferm::configdirectory}/chains/${chain}.conf":
+  concat{$filename:
     ensure  => 'present',
   }
 
-  concat::fragment{"${chain}-policy":
-    target  => "${ferm::configdirectory}/chains/${chain}.conf",
+  concat::fragment{"${table}-${chain}-policy":
+    target  => $filename,
     content => epp(
       "${module_name}/ferm_chain_header.conf.epp", {
         'policy'            => $policy,
@@ -28,10 +62,25 @@ define ferm::chain (
   }
 
   if $log_dropped_packets {
-    concat::fragment{"${chain}-footer":
-      target  => "${ferm::configdirectory}/chains/${chain}.conf",
+    concat::fragment{"${table}-${chain}-footer":
+      target  => $filename,
       content => epp("${module_name}/ferm_chain_footer.conf.epp", { 'chain' => $chain }),
       order   => 'zzzzzzzzzzzzzzzzzzzzz',
     }
   }
+
+  # make sure the generated snippet is actually included
+  concat::fragment{"${table}-${chain}-config-include":
+    target  => $ferm::configfile,
+    content => epp(
+      "${module_name}/ferm-table-chain-config-include.epp", {
+        'ip'       => join($ferm::ip_versions, ' '),
+        'table'    => $table,
+        'chain'    => $chain,
+        'filename' => $filename,
+      }
+    ),
+    order   => "${table}-${chain}",
+    require => Concat[$filename],
+  }
 }
index 25607ad9ff6bc74e01f530e751795418b8d76ee7..efabe2bb72920ea49789c183449ee87ba6fb88e9 100644 (file)
@@ -57,4 +57,33 @@ class ferm::config {
     disable_conntrack   => $ferm::disable_conntrack,
     log_dropped_packets => $ferm::output_log_dropped_packets,
   }
+
+  # initialize default tables and chains
+  ['PREROUTING', 'OUTPUT'].each |$raw_chain| {
+    ferm::chain{"raw-${raw_chain}":
+      chain               => $raw_chain,
+      policy              => 'ACCEPT',
+      disable_conntrack   => true,
+      log_dropped_packets => false,
+      table               => 'raw',
+    }
+  }
+  ['PREROUTING', 'INPUT', 'OUTPUT', 'POSTROUTING'].each |$nat_chain| {
+    ferm::chain{"nat-${nat_chain}":
+      chain               => $nat_chain,
+      policy              => 'ACCEPT',
+      disable_conntrack   => true,
+      log_dropped_packets => false,
+      table               => 'nat',
+    }
+  }
+  ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT', 'POSTROUTING'].each |$mangle_chain| {
+    ferm::chain{"mangle-${mangle_chain}":
+      chain               => $mangle_chain,
+      policy              => 'ACCEPT',
+      disable_conntrack   => true,
+      log_dropped_packets => false,
+      table               => 'mangle',
+    }
+  }
 }
index 221e1487a0463e909b9524a5a81e9b53e0e2bd87..d2251c9401c0da8cd7f89c1eab69f87fa490db0f 100644 (file)
 #   Allowed values: (true|false)
 # @param forward_policy Default policy for the FORWARD chain
 #   Default value: DROP
-#   Allowed values: (ACCEPT|DROP|REJECT)
+#   Allowed values: (ACCEPT|DROP)
 # @param output_policy Default policy for the OUTPUT chain
 #   Default value: ACCEPT
-#   Allowed values: (ACCEPT|DROP|REJECT)
+#   Allowed values: (ACCEPT|DROP)
 # @param input_policy Default policy for the INPUT chain
 #   Default value: DROP
-#   Allowed values: (ACCEPT|DROP|REJECT)
+#   Allowed values: (ACCEPT|DROP)
 # @param rules A hash that holds all data for ferm::rule
 #   Default value: Empty Hash
 #   Allowed value: Any Hash
@@ -95,6 +95,9 @@ class ferm (
   -> Class['ferm::config']
   ~> Class['ferm::service']
 
+  Ferm::Chain <| |>
+  ~> Class['ferm::service']
+
   $rules.each |$rulename, $attributes| {
     ferm::rule{$rulename:
       * => $attributes,
index 68e88a2025014a173a0567d42aa692627dcfc98f..4f2c985899b22dd1e1514b8bcdefd0135af98bea 100644 (file)
@@ -1,8 +1,31 @@
-# defined resource which creates a single rule in a specific chain
+# @summary This defined resource manages a single rule in a specific chain
+#
+# @example Jump to the 'SSH' chain for all incoming SSH traffic (see chain.pp examples on how to create the chain)
+#   ferm::rule{'incoming-ssh':
+#     chain  => 'INPUT',
+#     action => 'SSH',
+#     proto  => 'tcp',
+#     dport  => '22',
+#   }
+#
+# @example Create a rule in the 'SSH' chain to allow connections from localhost
+#   ferm::rule{'allow-ssh-localhost':
+#     chain  => 'SSH',
+#     action => 'ACCEPT',
+#     proto  => 'tcp',
+#     dport  => '22',
+#     saddr  => '127.0.0.1',
+#   }
+#
 # @param chain Configure the chain where we want to add the rule
-# @param policy Configure what we want to do with the packet (drop, accept, log...)
 # @param proto Which protocol do we want to match, typically UDP or TCP
 # @param comment A comment that will be added to the ferm config and to ip{,6}tables
+# @param action Configure what we want to do with the packet (drop/accept/reject, can also be a target chain name)
+#   Default value: undef
+#   Allowed values: (RETURN|ACCEPT|DROP|REJECT|NOTRACK|LOG|MARK|DNAT|SNAT|MASQUERADE|REDIRECT|String[1])
+# @param policy Configure what we want to do with the packet (drop/accept/reject, can also be a target chain name) [DEPRECATED]
+#   Default value: undef
+#   Allowed values: (RETURN|ACCEPT|DROP|REJECT|NOTRACK|LOG|MARK|DNAT|SNAT|MASQUERADE|REDIRECT|String[1])
 # @param dport The destination port, can be a range as string or a single port number as integer
 # @param sport The source port, can be a range as string or a single port number as integer
 # @param saddr The source address we want to match
 # @param proto_options Optional parameters that will be passed to the protocol (for example to match specific ICMP types)
 # @param interface an Optional interface where this rule should be applied
 # @param ensure Set the rule to present or absent
+# @param table Select the target table (filter/raw/mangle/nat)
+#   Default value: filter
+#   Allowed values: (filter|raw|mangle|nat) (see Ferm::Tables type)
 define ferm::rule (
   String[1] $chain,
-  Ferm::Policies $policy,
   Ferm::Protocols $proto,
   String $comment = $name,
+  Optional[Ferm::Actions] $action = undef,
+  Optional[Ferm::Policies] $policy = undef,
   Optional[Variant[Stdlib::Port,String[1]]] $dport = undef,
   Optional[Variant[Stdlib::Port,String[1]]] $sport = undef,
   Optional[Variant[Array, String[1]]] $saddr = undef,
@@ -22,7 +49,31 @@ define ferm::rule (
   Optional[String[1]] $proto_options = undef,
   Optional[String[1]] $interface = undef,
   Enum['absent','present'] $ensure = 'present',
+  Ferm::Tables $table = 'filter',
 ){
+
+  if $policy and $action {
+    fail('Cannot specify both policy and action. Do not provide policy when using the new action param.')
+  } elsif $policy and ! $action {
+    warning('The param "policy" is deprecated (superseded by "action") and will be dropped in a future release.')
+    $action_temp = $policy
+  } elsif $action and ! $policy {
+    $action_temp = $action
+  } else {
+    fail('Exactly one of "action" or the deprecated "policy" param is required.')
+  }
+
+  if $action_temp in ['RETURN', 'ACCEPT', 'DROP', 'REJECT', 'NOTRACK', 'LOG',
+                      'MARK', 'DNAT', 'SNAT', 'MASQUERADE', 'REDIRECT'] {
+    $action_real = $action_temp
+  } else {
+    # assume the action contains a target chain, so prefix it with the "jump" statement
+    $action_real = "jump ${action_temp}"
+    # make sure the target chain is created before we try to add rules to it
+    Ferm::Chain <| chain == $action_temp and table == $table |> -> Ferm::Rule[$name]
+  }
+
+
   $proto_real = "proto ${proto}"
 
   $dport_real = $dport ? {
@@ -63,33 +114,42 @@ define ferm::rule (
   }
   $comment_real = "mod comment comment '${comment}'"
 
-  $rule = squeeze("${comment_real} ${proto_real} ${proto_options_real} ${dport_real} ${sport_real} ${daddr_real} ${saddr_real} ${policy};", ' ')
+  # prevent unmanaged files due to new naming schema
+  # keep the default "filter" chains in the original location
+  # only prefix chains in other tables with the table name
+  if $table == 'filter' and $chain in ['INPUT', 'FORWARD', 'OUTPUT'] {
+    $filename = "${ferm::configdirectory}/chains/${chain}.conf"
+  } else {
+    $filename = "${ferm::configdirectory}/chains/${table}-${chain}.conf"
+  }
+
+  $rule = squeeze("${comment_real} ${proto_real} ${proto_options_real} ${dport_real} ${sport_real} ${daddr_real} ${saddr_real} ${action_real};", ' ')
   if $ensure == 'present' {
     if $interface {
       unless defined(Concat::Fragment["${chain}-${interface}-aaa"]) {
         concat::fragment{"${chain}-${interface}-aaa":
-          target  => "${ferm::configdirectory}/chains/${chain}.conf",
+          target  => $filename,
           content => "interface ${interface} {\n",
           order   => $interface,
         }
       }
 
       concat::fragment{"${chain}-${interface}-${name}":
-        target  => "${ferm::configdirectory}/chains/${chain}.conf",
+        target  => $filename,
         content => "  ${rule}\n",
         order   => $interface,
       }
 
       unless defined(Concat::Fragment["${chain}-${interface}-zzz"]) {
         concat::fragment{"${chain}-${interface}-zzz":
-          target  => "${ferm::configdirectory}/chains/${chain}.conf",
+          target  => $filename,
           content => "}\n",
           order   => $interface,
         }
       }
     } else {
       concat::fragment{"${chain}-${name}":
-        target  => "${ferm::configdirectory}/chains/${chain}.conf",
+        target  => $filename,
         content => "${rule}\n",
       }
     }
index 1b0f794f8181e1655de066b4bba725651b1e1d25..b0c41a57ec702ed3e40dc59addb3251d92954b6c 100644 (file)
@@ -12,27 +12,29 @@ manage_initfile = case sut_os
                     false
                   end
 
+basic_manifest = %(
+  class { 'ferm':
+    manage_service    => true,
+    manage_configfile => true,
+    manage_initfile   => #{manage_initfile}, # CentOS-6 does not provide init script
+    forward_policy    => 'DROP',
+    output_policy     => 'DROP',
+    input_policy      => 'DROP',
+    rules             => {
+      'allow_acceptance_tests' => {
+        chain  => 'INPUT',
+        action => 'ACCEPT',
+        proto  => tcp,
+        dport  => 22,
+      },
+    },
+    ip_versions      => ['ip'], #only ipv4 available with CI
+  }
+)
+
 describe 'ferm' do
   context 'with basics settings' do
-    pp = %(
-      class { 'ferm':
-        manage_service    => true,
-        manage_configfile => true,
-        manage_initfile   => #{manage_initfile}, # CentOS-6 does not provide init script
-        forward_policy    => 'DROP',
-        output_policy     => 'DROP',
-        input_policy      => 'DROP',
-        rules             => {
-          'allow acceptance_tests' => {
-            chain  => 'INPUT',
-            policy => 'ACCEPT',
-            proto  => tcp,
-            dport  => 22,
-          },
-        },
-        ip_versions      => ['ip'], #only ipv4 available with CI
-      }
-    )
+    pp = basic_manifest
 
     it 'works with no error' do
       apply_manifest(pp, catch_failures: true)
@@ -54,7 +56,57 @@ describe 'ferm' do
     end
 
     describe iptables do
-      it { is_expected.to have_rule('-A INPUT -p tcp -m comment --comment "allow acceptance_tests" -m tcp --dport 22 -j ACCEPT').with_table('filter').with_chain('INPUT') }
+      it do
+        is_expected.to have_rule('-A INPUT -p tcp -m comment --comment ["]*allow_acceptance_tests["]* -m tcp --dport 22 -j ACCEPT'). \
+          with_table('filter'). \
+          with_chain('INPUT')
+      end
+    end
+
+    context 'with custom chains' do
+      advanced_manifest = %(
+        ferm::chain { 'check-http':
+          chain               => 'HTTP',
+          disable_conntrack   => true,
+          log_dropped_packets => false,
+        }
+        ferm::rule { 'jump_http':
+          chain             => 'INPUT',
+          action            => 'HTTP',
+          proto             => 'tcp',
+          dport             => '80',
+          require           => Ferm::Chain['check-http'],
+        }
+        ferm::rule { 'allow_http_localhost':
+          chain             => 'HTTP',
+          action            => 'ACCEPT',
+          proto             => 'tcp',
+          dport             => '80',
+          saddr             => '127.0.0.1',
+          require           => Ferm::Chain['check-http'],
+        }
+      )
+      pp = [basic_manifest, advanced_manifest].join("\n")
+
+      it 'works with no error' do
+        apply_manifest(pp, catch_failures: true)
+      end
+      it 'works idempotently' do
+        apply_manifest(pp, catch_changes: true)
+      end
+
+      describe iptables do
+        it do
+          is_expected.to have_rule('-A INPUT -p tcp -m comment --comment ["]*jump_http["]* -m tcp --dport 80 -j HTTP'). \
+            with_table('filter'). \
+            with_chain('INPUT')
+        end
+        it do
+          is_expected.to have_rule('-A HTTP -s 127.0.0.1/32 -p tcp -m comment --comment ["]*allow_http_localhost["]* -m tcp --dport 80 -j ACCEPT'). \
+            with_table('filter'). \
+            with_chain('HTTP')
+        end
+      end
     end
   end
 end
index e5669b8b860efee0f7755d1d0ed6ccfce3839415..225577bde9435d76ea174a2177424fb73374dc64 100644 (file)
@@ -64,6 +64,17 @@ describe 'ferm' do
           is_expected.to contain_concat__fragment('ferm.conf'). \
             without_content(%r{@preserve;})
         end
+        it { is_expected.to contain_concat__fragment('raw-PREROUTING-config-include') }
+        it { is_expected.to contain_concat__fragment('raw-OUTPUT-config-include') }
+        it { is_expected.to contain_concat__fragment('nat-PREROUTING-config-include') }
+        it { is_expected.to contain_concat__fragment('nat-INPUT-config-include') }
+        it { is_expected.to contain_concat__fragment('nat-OUTPUT-config-include') }
+        it { is_expected.to contain_concat__fragment('nat-POSTROUTING-config-include') }
+        it { is_expected.to contain_concat__fragment('mangle-PREROUTING-config-include') }
+        it { is_expected.to contain_concat__fragment('mangle-INPUT-config-include') }
+        it { is_expected.to contain_concat__fragment('mangle-FORWARD-config-include') }
+        it { is_expected.to contain_concat__fragment('mangle-OUTPUT-config-include') }
+        it { is_expected.to contain_concat__fragment('mangle-POSTROUTING-config-include') }
       end
       context 'with managed initfile' do
         let :params do
@@ -77,18 +88,62 @@ describe 'ferm' do
         end
       end
       context 'it creates chains' do
-        it { is_expected.to contain_concat__fragment('FORWARD-policy') }
-        it { is_expected.to contain_concat__fragment('INPUT-policy') }
-        it { is_expected.to contain_concat__fragment('OUTPUT-policy') }
+        it { is_expected.to contain_concat__fragment('raw-PREROUTING-policy') }
+        it { is_expected.to contain_concat__fragment('raw-OUTPUT-policy') }
+        it { is_expected.to contain_concat__fragment('nat-PREROUTING-policy') }
+        it { is_expected.to contain_concat__fragment('nat-INPUT-policy') }
+        it { is_expected.to contain_concat__fragment('nat-OUTPUT-policy') }
+        it { is_expected.to contain_concat__fragment('nat-POSTROUTING-policy') }
+        it { is_expected.to contain_concat__fragment('mangle-PREROUTING-policy') }
+        it { is_expected.to contain_concat__fragment('mangle-INPUT-policy') }
+        it { is_expected.to contain_concat__fragment('mangle-FORWARD-policy') }
+        it { is_expected.to contain_concat__fragment('mangle-OUTPUT-policy') }
+        it { is_expected.to contain_concat__fragment('mangle-POSTROUTING-policy') }
+        it { is_expected.to contain_concat__fragment('filter-INPUT-policy') }
+        it { is_expected.to contain_concat__fragment('filter-FORWARD-policy') }
+        it { is_expected.to contain_concat__fragment('filter-OUTPUT-policy') }
         if facts[:os]['release']['major'].to_i == 10
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/raw-PREROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/raw-OUTPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/nat-PREROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/nat-INPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/nat-OUTPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/nat-POSTROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/mangle-PREROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/mangle-INPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/mangle-FORWARD.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/mangle-OUTPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/mangle-POSTROUTING.conf') }
           it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/FORWARD.conf') }
           it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/INPUT.conf') }
           it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/OUTPUT.conf') }
         else
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/raw-PREROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/raw-OUTPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/nat-PREROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/nat-INPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/nat-OUTPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/nat-POSTROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/mangle-PREROUTING.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/mangle-INPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/mangle-FORWARD.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/mangle-OUTPUT.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/mangle-POSTROUTING.conf') }
           it { is_expected.to contain_concat('/etc/ferm.d/chains/FORWARD.conf') }
           it { is_expected.to contain_concat('/etc/ferm.d/chains/INPUT.conf') }
           it { is_expected.to contain_concat('/etc/ferm.d/chains/OUTPUT.conf') }
         end
+        it { is_expected.to contain_ferm__chain('raw-PREROUTING') }
+        it { is_expected.to contain_ferm__chain('raw-OUTPUT') }
+        it { is_expected.to contain_ferm__chain('nat-PREROUTING') }
+        it { is_expected.to contain_ferm__chain('nat-INPUT') }
+        it { is_expected.to contain_ferm__chain('nat-OUTPUT') }
+        it { is_expected.to contain_ferm__chain('nat-POSTROUTING') }
+        it { is_expected.to contain_ferm__chain('mangle-PREROUTING') }
+        it { is_expected.to contain_ferm__chain('mangle-INPUT') }
+        it { is_expected.to contain_ferm__chain('mangle-FORWARD') }
+        it { is_expected.to contain_ferm__chain('mangle-OUTPUT') }
+        it { is_expected.to contain_ferm__chain('mangle-POSTROUTING') }
         it { is_expected.to contain_ferm__chain('FORWARD') }
         it { is_expected.to contain_ferm__chain('OUTPUT') }
         it { is_expected.to contain_ferm__chain('INPUT') }
index 94258215d0bde8c299e6f2a9c299df6faad314bb..4a598b3ae1aa76f5ab5743a0cd4c416e25b21038 100644 (file)
@@ -15,25 +15,25 @@ describe 'ferm::chain', type: :define do
       context 'default params creates INPUT2 chain' do
         let :params do
           {
-            policy: 'DROP',
             disable_conntrack: false,
             log_dropped_packets: true
           }
         end
 
         it { is_expected.to compile.with_all_deps }
+        it { is_expected.to contain_concat__fragment('filter-INPUT2-config-include') }
         it do
-          is_expected.to contain_concat__fragment('INPUT2-policy'). \
+          is_expected.to contain_concat__fragment('filter-INPUT2-policy'). \
             with_content(%r{ESTABLISHED RELATED})
         end
         it do
-          is_expected.to contain_concat__fragment('INPUT2-footer'). \
+          is_expected.to contain_concat__fragment('filter-INPUT2-footer'). \
             with_content(%r{LOG log-prefix 'INPUT2: ';})
         end
         if facts[:os]['release']['major'].to_i == 10
-          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/INPUT2.conf') }
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/filter-INPUT2.conf') }
         else
-          it { is_expected.to contain_concat('/etc/ferm.d/chains/INPUT2.conf') }
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/filter-INPUT2.conf') }
         end
         it { is_expected.to contain_ferm__chain('INPUT2') }
       end
@@ -41,7 +41,6 @@ describe 'ferm::chain', type: :define do
       context 'without conntrack' do
         let :params do
           {
-            policy: 'DROP',
             disable_conntrack: true,
             log_dropped_packets: false
           }
@@ -49,15 +48,28 @@ describe 'ferm::chain', type: :define do
 
         it { is_expected.to compile.with_all_deps }
         it do
-          is_expected.to contain_concat__fragment('INPUT2-policy')
-          is_expected.not_to contain_concat__fragment('INPUT2-policy'). \
+          is_expected.to contain_concat__fragment('filter-INPUT2-policy')
+          is_expected.not_to contain_concat__fragment('filter-INPUT2-policy'). \
             with_content(%r{ESTABLISHED RELATED})
         end
         it do
-          is_expected.not_to contain_concat__fragment('INPUT2-footer'). \
+          is_expected.not_to contain_concat__fragment('filter-INPUT2-footer'). \
             with_content(%r{LOG log-prefix 'INPUT2: ';})
         end
       end
+
+      context 'with policy setting for custom chain' do
+        let :params do
+          {
+            chain: 'INPUT2',
+            policy: 'DROP',
+            disable_conntrack: true,
+            log_dropped_packets: false
+          }
+        end
+
+        it { is_expected.to compile.and_raise_error(%r{Can only set a default policy for builtin chains}) }
+      end
     end
   end
 end
index 1bec758ec77d9131c802dfdcdaee330c1122fd0a..ef20e17c4063cb023ad54421a21dbd9d35be6eac 100644 (file)
@@ -11,7 +11,37 @@ describe 'ferm::rule', type: :define do
         'include ferm'
       end
 
-      context 'without a specific interface' do
+      context 'without policy or action' do
+        let(:title) { 'filter-ssh' }
+        let :params do
+          {
+            chain: 'INPUT',
+            proto: 'tcp',
+            dport: '22',
+            saddr: '127.0.0.1'
+          }
+        end
+
+        it { is_expected.to compile.and_raise_error(%r{Exactly one of "action" or the deprecated "policy" param is required}) }
+      end
+
+      context 'with both policy and action' do
+        let(:title) { 'filter-ssh' }
+        let :params do
+          {
+            chain: 'INPUT',
+            policy: 'ACCEPT',
+            action: 'ACCEPT',
+            proto: 'tcp',
+            dport: '22',
+            saddr: '127.0.0.1'
+          }
+        end
+
+        it { is_expected.to compile.and_raise_error(%r{Cannot specify both policy and action}) }
+      end
+
+      context 'without a specific interface using legacy policy param' do
         let(:title) { 'filter-ssh' }
         let :params do
           {
@@ -26,12 +56,32 @@ describe 'ferm::rule', type: :define do
         it { is_expected.to compile.with_all_deps }
         it { is_expected.to contain_concat__fragment('INPUT-filter-ssh').with_content("mod comment comment 'filter-ssh' proto tcp dport 22 saddr @ipfilter((127.0.0.1)) ACCEPT;\n") }
       end
+
+      context 'without a specific interface' do
+        let(:title) { 'filter-ssh' }
+        let :params do
+          {
+            chain: 'INPUT',
+            action: 'ACCEPT',
+            proto: 'tcp',
+            dport: '22',
+            saddr: '127.0.0.1'
+          }
+        end
+
+        it { is_expected.to compile.with_all_deps }
+        it { is_expected.to contain_concat__fragment('INPUT-filter-ssh').with_content("mod comment comment 'filter-ssh' proto tcp dport 22 saddr @ipfilter((127.0.0.1)) ACCEPT;\n") }
+        it { is_expected.to contain_concat__fragment('filter-INPUT-config-include') }
+        it { is_expected.to contain_concat__fragment('filter-FORWARD-config-include') }
+        it { is_expected.to contain_concat__fragment('filter-OUTPUT-config-include') }
+      end
+
       context 'with a specific interface' do
         let(:title) { 'filter-ssh' }
         let :params do
           {
             chain: 'INPUT',
-            policy: 'ACCEPT',
+            action: 'ACCEPT',
             proto: 'tcp',
             dport: '22',
             saddr: '127.0.0.1',
@@ -44,12 +94,13 @@ describe 'ferm::rule', type: :define do
         it { is_expected.to contain_concat__fragment('INPUT-eth0-aaa').with_content("interface eth0 {\n") }
         it { is_expected.to contain_concat__fragment('INPUT-eth0-zzz').with_content("}\n") }
       end
+
       context 'with a specific interface using array for daddr' do
         let(:title) { 'filter-ssh' }
         let :params do
           {
             chain: 'INPUT',
-            policy: 'ACCEPT',
+            action: 'ACCEPT',
             proto: 'tcp',
             dport: '22',
             daddr: ['127.0.0.1', '123.123.123.123', ['10.0.0.1', '10.0.0.2']],
@@ -62,6 +113,68 @@ describe 'ferm::rule', type: :define do
         it { is_expected.to contain_concat__fragment('INPUT-eth0-aaa').with_content("interface eth0 {\n") }
         it { is_expected.to contain_concat__fragment('INPUT-eth0-zzz').with_content("}\n") }
       end
+
+      context 'with jumping to custom chains' do
+        # create custom chain
+        let(:pre_condition) do
+          'include ferm ;
+          ferm::chain{"check-ssh":
+            chain               => "SSH",
+            disable_conntrack   => true,
+            log_dropped_packets => false,
+          }'
+        end
+        let(:title) { 'filter-ssh' }
+        let :params do
+          {
+            chain: 'INPUT',
+            action: 'SSH',
+            proto: 'tcp',
+            dport: '22'
+          }
+        end
+
+        it { is_expected.to compile.with_all_deps }
+        it { is_expected.to contain_concat__fragment('filter-SSH-policy') }
+        it do
+          is_expected.to contain_concat__fragment('INPUT-filter-ssh').\
+            with_content("mod comment comment 'filter-ssh' proto tcp dport 22 jump SSH;\n"). \
+            that_requires('Ferm::Chain[check-ssh]')
+        end
+        it { is_expected.to contain_concat__fragment('filter-INPUT-config-include') }
+        if facts[:os]['release']['major'].to_i == 10
+          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/filter-SSH.conf') }
+        else
+          it { is_expected.to contain_concat('/etc/ferm.d/chains/filter-SSH.conf') }
+        end
+      end
+
+      context 'definining rules in custom chains' do
+        # create custom chain
+        let(:pre_condition) do
+          'include ferm ;
+          ferm::chain{"check-ssh":
+            chain               => "SSH",
+            disable_conntrack   => true,
+            log_dropped_packets => false,
+          }'
+        end
+        let(:title) { 'allow-ssh-localhost' }
+        let :params do
+          {
+            chain: 'SSH',
+            action: 'ACCEPT',
+            proto: 'tcp',
+            dport: '22',
+            saddr: '127.0.0.1'
+          }
+        end
+
+        it { is_expected.to compile.with_all_deps }
+        it { is_expected.to contain_concat__fragment('SSH-allow-ssh-localhost').with_content("mod comment comment 'allow-ssh-localhost' proto tcp dport 22 saddr @ipfilter((127.0.0.1)) ACCEPT;\n") }
+        it { is_expected.to contain_concat__fragment('filter-INPUT-config-include') }
+        it { is_expected.to contain_concat__fragment('filter-SSH-config-include') }
+      end
     end
   end
 end
index f16fb1528357813629315bd2cac5c96390a0652a..96f14d54356d27090b48a8ec129b9e29be90c52d 100644 (file)
@@ -10,6 +10,11 @@ require 'rspec-puppet-facts'
 require 'bundler'
 include RspecPuppetFacts
 
+if ENV['DEBUG']
+  Puppet::Util::Log.level = :debug
+  Puppet::Util::Log.newdestination(:console)
+end
+
 if File.exist?(File.join(__dir__, 'default_module_facts.yml'))
   facts = YAML.load(File.read(File.join(__dir__, 'default_module_facts.yml')))
   if facts
diff --git a/templates/ferm-table-chain-config-include.epp b/templates/ferm-table-chain-config-include.epp
new file mode 100644 (file)
index 0000000..722d3e7
--- /dev/null
@@ -0,0 +1,14 @@
+<%- | String[1] $ip,
+Ferm::Tables $table,
+String[1] $chain,
+Stdlib::Absolutepath $filename,
+| -%>
+
+domain (<%= $ip %>) table <%= $table %> {
+    chain <%= $chain %> {
+        <%- if $table == 'filter' and $chain == 'INPUT' { -%>
+        interface lo ACCEPT;
+        <%- } -%>
+        @include '<%= $filename %>';
+    }
+}
index 0245a7066cebbe1f143f16bef7f43f9aa5aca160..3b1a21160ba7ccf2797a418f372e9d160904e046 100644 (file)
@@ -2,7 +2,6 @@
 Stdlib::Absolutepath $configdirectory,
 Hash[String[1], Array[String[1]]] $preserve_chains_in_tables,
 | -%>
-# End custom section
 
 <%- $preserve_chains_in_tables.each |$table, $chains| { -%>
 domain (<%= $ip %>) table <%= $table %> {
@@ -11,18 +10,3 @@ domain (<%= $ip %>) table <%= $table %> {
   <%- } -%>
 }
 <%- } -%>
-
-domain (<%= $ip %>) table filter {
-  chain INPUT {
-    interface lo ACCEPT;
-    @include '<%= $configdirectory %>/chains/INPUT.conf';
-  }
-
-  chain OUTPUT {
-    @include '<%= $configdirectory %>/chains/OUTPUT.conf';
-  }
-
-  chain FORWARD {
-    @include '<%= $configdirectory %>/chains/FORWARD.conf';
-  }
-}
index f94b18d7cb0e6fe72cbaed5deccd4f80f18ba2e5..938958b1e824abd55c016891381a861296317c9a 100644 (file)
@@ -1,12 +1,14 @@
-<%- | Ferm::Policies $policy,
+<%- | Optional[Ferm::Policies] $policy,
       Boolean $disable_conntrack,
 | -%>
 # THIS FILE IS MANAGED BY PUPPET
+<%- if $policy { -%>
 # Default policy for this chain
 policy <%= $policy %>;
+<%- } -%>
 
 <% unless $disable_conntrack { -%>
 # connection tracking
-mod state state INVALID DROP;
-mod state state (ESTABLISHED RELATED) ACCEPT;
+mod conntrack ctstate (ESTABLISHED RELATED) ACCEPT;
+mod conntrack ctstate INVALID DROP;
 <% } -%>
index e1a1f1a1c580e7bf311122146eca32cd056c8e57..a29106c09629a93bd7fcb24b3fe0e211af369e26 100644 (file)
@@ -5,5 +5,3 @@
 
 # get all ip definitions
 @include '<%= $configdirectory %>/definitions/';
-
-# Begin custom section
diff --git a/types/actions.pp b/types/actions.pp
new file mode 100644 (file)
index 0000000..49bfd2c
--- /dev/null
@@ -0,0 +1,6 @@
+# @summary a list of allowed actions for a rule
+# As you can also *jump* to other chains, each chain-name is also a valid action/target
+type Ferm::Actions = Variant[
+  Enum['RETURN', 'ACCEPT', 'DROP', 'REJECT', 'NOTRACK', 'LOG', 'MARK', 'DNAT', 'SNAT', 'MASQUERADE', 'REDIRECT'],
+  String[1],
+]
index 03be6ce3efcc0c25db6884402376df3ab66476f2..096309584908c4fb334ace08cc7a318bf78c5f11 100644 (file)
@@ -1,2 +1,2 @@
-# @summary a list of allowed default policies for a chain
-type Ferm::Policies = Enum['ACCEPT','DROP', 'REJECT']
+# @summary a list of allowed policies for a chain
+type Ferm::Policies = Enum['ACCEPT','DROP']
diff --git a/types/tables.pp b/types/tables.pp
new file mode 100644 (file)
index 0000000..89edde7
--- /dev/null
@@ -0,0 +1,2 @@
+# @summary a list of available tables
+type Ferm::Tables = Enum['raw', 'mangle', 'nat', 'filter']