diff options
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | REFERENCE.md | 62 | ||||
| -rw-r--r-- | manifests/chain.pp | 54 | ||||
| -rw-r--r-- | manifests/config.pp | 10 | ||||
| -rw-r--r-- | manifests/init.pp | 8 | ||||
| -rw-r--r-- | manifests/install.pp | 36 | ||||
| -rw-r--r-- | manifests/service.pp | 2 | ||||
| -rw-r--r-- | metadata.json | 8 | ||||
| -rw-r--r-- | spec/acceptance/ferm_spec.rb | 105 | ||||
| -rw-r--r-- | spec/defines/chain_spec.rb | 28 | ||||
| -rw-r--r-- | templates/dropin_ferm.conf.epp | 6 | ||||
| -rw-r--r-- | templates/ferm_chain_custom.conf.epp | 4 | 
12 files changed, 303 insertions, 31 deletions
@@ -44,6 +44,17 @@ This will install the package, but nothing more. It won't explicitly enable it  or write any rules. Be careful here: The default Debian package enabled  autostart for the service and only allows incoming SSH/IPSec connections. +It is also possible to install ferm from sources: +```puppet +class {'ferm': +  install_method = 'vcsrepo', +} +``` + +When `install_method` is `vcsrepo`, the `git` binary is required, this module should handle Git installation. + +When `install_method` is `vcsrepo` with `vcstag` >= `v2.5` ferm call "legacy" xtables tools because nft based tools are incompatible. +  You can easily define rules in Puppet (they don't need to be exported resources):  ```puppet diff --git a/REFERENCE.md b/REFERENCE.md index 2d0a4e3..eef0dc5 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -220,11 +220,35 @@ Default value: ['ip','ip6']  Data type: `Hash[String[1],Array[String[1]]]` -Hash with table:chains[] to use ferm @preserve for +Hash with table:chains[] to use ferm @preserve for (since ferm v2.4)  Example: {'nat' => ['PREROUTING', 'POSTROUTING']}  Default value: {} +##### `install_method` + +Data type: `Enum['package','vcsrepo']` + +method used to install ferm + +Default value: 'package' + +##### `vcsrepo` + +Data type: `Stdlib::HTTPSUrl` + +git repository where ferm sources are hosted + +Default value: 'https://github.com/MaxKellermann/ferm.git' + +##### `vcstag` + +Data type: `String[1]` + +git tag used when install_method is vcsrepo + +Default value: 'v2.5.1' +  ## Defined types  ### ferm::chain @@ -243,6 +267,34 @@ ferm::chain{'check-ssh':  }  ``` +##### create a custom chain, e.g. for managing custom FORWARD chain rule for OpenVPN using custom ferm DSL. + +```puppet +$my_rules = @(EOT) +chain OPENVPN_FORWORD_RULES { +  proto udp { +    interface tun0 { +      outerface enp4s0 { +        mod conntrack ctstate (NEW) saddr @ipfilter((10.8.0.0/24)) ACCEPT; +      } +    } +  } +} +| EOT + +ferm::chain{'OPENVPN_FORWORD_RULES': +  chain   => 'OPENVPN_FORWORD_RULES', +  content => $my_rules, +} + +ferm::rule { "OpenVPN - FORWORD all udp traffic from network 10.8.0.0/24 to subchain OPENVPN_FORWORD_RULES": +  chain     => 'FORWARD', +  action    => 'OPENVPN_FORWORD_RULES', +  saddr     => '10.8.0.0/24', +  proto     => 'udp', +} +``` +  #### Parameters  The following parameters are available in the `ferm::chain` defined type. @@ -306,6 +358,14 @@ Set list of versions of ip we want ot use.  Default value: $ferm::ip_versions +##### `content` + +Data type: `Optional[String]` + +Can only be used for custom chains. It allows you to provide your own ferm rules for this chain. Sets the contents of this custom chain to provided value. + +Default value: undef +  ### ferm::ipset  a defined resource that can match for ipsets at the top of a chain. This is a per-chain resource. You cannot mix IPv4 and IPv6 sets. diff --git a/manifests/chain.pp b/manifests/chain.pp index ed58126..91cd930 100644 --- a/manifests/chain.pp +++ b/manifests/chain.pp @@ -25,7 +25,8 @@ define ferm::chain (    String[1] $chain                             = $name,    Optional[Ferm::Policies] $policy             = undef,    Ferm::Tables $table                          = 'filter', -  Array[Enum['ip','ip6']] $ip_versions         = $ferm::ip_versions, +  Array[Enum['ip', 'ip6']] $ip_versions        = $ferm::ip_versions, +  Optional[String[1]] $content                 = undef,  ) {    # prevent unmanaged files due to new naming schema    # keep the default "filter" chains in the original location @@ -43,32 +44,43 @@ define ferm::chain (      'filter' => ['INPUT', 'FORWARD', 'OUTPUT'],    } -  if $policy and ! ($chain in $builtin_chains[$table]) { +  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 -  concat{$filename: -    ensure  => 'present', +  concat { $filename: +    ensure => 'present',    } -  concat::fragment{"${table}-${chain}-policy": -    target  => $filename, -    content => epp( -      "${module_name}/ferm_chain_header.conf.epp", { -        'policy'                              => $policy, -        'disable_conntrack'                   => $disable_conntrack, -        'drop_invalid_packets_with_conntrack' => $drop_invalid_packets_with_conntrack, -      } -    ), -    order   => '01', -  } - -  if $log_dropped_packets { -    concat::fragment{"${table}-${chain}-footer": +  if $content { +    concat::fragment { "${table}-${chain}-custom-content":        target  => $filename, -      content => epp("${module_name}/ferm_chain_footer.conf.epp", { 'chain' => $chain }), -      order   => 'zzzzzzzzzzzzzzzzzzzzz', +      content => epp( +        "${module_name}/ferm_chain_custom.conf.epp", { +          'content' => $content, +        }, +      ), +    } +  } else { +    concat::fragment { "${table}-${chain}-policy": +      target  => $filename, +      content => epp( +        "${module_name}/ferm_chain_header.conf.epp", { +          'policy'                              => $policy, +          'disable_conntrack'                   => $disable_conntrack, +          'drop_invalid_packets_with_conntrack' => $drop_invalid_packets_with_conntrack, +        } +      ), +      order   => '01', +    } + +    if $log_dropped_packets { +      concat::fragment { "${table}-${chain}-footer": +        target  => $filename, +        content => epp("${module_name}/ferm_chain_footer.conf.epp", { 'chain' => $chain }), +        order   => 'zzzzzzzzzzzzzzzzzzzzz', +      }      }    } @@ -77,7 +89,7 @@ define ferm::chain (    # This happens if we add ipset matches. We suffix this ordering with `bbb`. This allows us to    # insert ipset matches before other rules by adding `-aaa` or    # insert them at the end by ordering them with `-ccc`. -  concat::fragment{"${table}-${chain}-config-include": +  concat::fragment { "${table}-${chain}-config-include":      target  => $ferm::configfile,      content => epp(        "${module_name}/ferm-table-chain-config-include.epp", { diff --git a/manifests/config.pp b/manifests/config.pp index 5876bd7..8ed0f57 100644 --- a/manifests/config.pp +++ b/manifests/config.pp @@ -10,6 +10,16 @@ class ferm::config {    $_ip = join($ferm::ip_versions, ' ') +  if $facts['systemd'] { #fact provided by systemd module +    if $ferm::install_method == 'vcsrepo' and $ferm::manage_service { +      systemd::dropin_file { 'ferm.conf': +        unit    => 'ferm.service', +        content => epp("${module_name}/dropin_ferm.conf.epp"), +        before  => Service['ferm'], +      } +    } +  } +    # copy static files to ferm    # on a long term point of view, we want to package this    file{$ferm::configdirectory: diff --git a/manifests/init.pp b/manifests/init.pp index b1d051e..251effe 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -45,8 +45,11 @@  # @param output_log_dropped_packets Enable/Disable logging in the OUTPUT chain of packets to the kernel log, if no explicit chain matched  # @param input_log_dropped_packets Enable/Disable logging in the INPUT chain of packets to the kernel log, if no explicit chain matched  # @param ip_versions Set list of versions of ip we want ot use. -# @param preserve_chains_in_tables Hash with table:chains[] to use ferm @preserve for +# @param preserve_chains_in_tables Hash with table:chains[] to use ferm @preserve for (since ferm v2.4)  #   Example: {'nat' => ['PREROUTING', 'POSTROUTING']} +# @param install_method method used to install ferm +# @param vcsrepo git repository where ferm sources are hosted +# @param vcstag git tag used when install_method is vcsrepo  class ferm (    Stdlib::Absolutepath $configfile,    Stdlib::Absolutepath $configdirectory, @@ -67,6 +70,9 @@ class ferm (    Hash $chains = {},    Array[Enum['ip','ip6']] $ip_versions = ['ip','ip6'],    Hash[String[1],Array[String[1]]] $preserve_chains_in_tables = {}, +  Enum['package','vcsrepo'] $install_method = 'package', +  Stdlib::HTTPSUrl $vcsrepo = 'https://github.com/MaxKellermann/ferm.git', +  String[1] $vcstag = 'v2.5.1',  ) {    contain ferm::install    contain ferm::config diff --git a/manifests/install.pp b/manifests/install.pp index 4337a99..5755ead 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -8,8 +8,40 @@ class ferm::install {    # this is a private class    assert_private("You're not supposed to do that!") -  package{'ferm': -    ensure => 'latest', +  case $ferm::install_method { +    'package': { +      package{'ferm': +        ensure => 'latest', +      } +    } +    'vcsrepo': { +      $_source_path = '/opt/ferm' +      ensure_packages (['git', 'iptables', 'perl', 'make'], { ensure => present }) + +      package{'ferm': +        ensure => absent, +      } +      -> vcsrepo { $_source_path : +        ensure   => present, +        provider => git, +        source   => $ferm::vcsrepo, +        revision => $ferm::vcstag, +      } +      -> exec { 'make install': +        cwd     => $_source_path, +        path    => '/usr/sbin:/usr/bin:/sbin:/bin', +        creates => '/usr/sbin/ferm', +      } +      -> file { '/etc/ferm': +        ensure => directory, +        owner  => 0, +        group  => 0, +        mode   => '0700', +      } +    } +    default: { +      fail("unexpected install_method ${ferm::install_method}") +    }    }    if $ferm::manage_initfile { diff --git a/manifests/service.pp b/manifests/service.pp index e9eb369..9fb1737 100644 --- a/manifests/service.pp +++ b/manifests/service.pp @@ -15,7 +15,7 @@ class ferm::service {      }      # on Ubuntu, we can't start the service, unless we set ENABLED=true in /etc/default/ferm... -    if ($facts['os']['name'] in ['Ubuntu', 'Debian']) { +    if ($facts['os']['name'] in ['Ubuntu', 'Debian']) and ($ferm::install_method == 'package') {        file_line{'enable_ferm':          path   => '/etc/default/ferm',          line   => 'ENABLED="yes"', diff --git a/metadata.json b/metadata.json index 065b9ad..6a114b2 100644 --- a/metadata.json +++ b/metadata.json @@ -15,6 +15,14 @@      {        "name": "puppetlabs/stdlib",        "version_requirement": ">= 4.25.0 < 7.0.0" +    }, +    { +      "name": "puppetlabs/vcsrepo", +      "version_requirement": ">= 3.0.0 < 4.0.0" +    }, +    { +      "name": "camptocamp-systemd", +      "version_requirement": ">= 2.9.0 < 3.0.0"      }    ],    "operatingsystem_support": [ diff --git a/spec/acceptance/ferm_spec.rb b/spec/acceptance/ferm_spec.rb index 0dd2399..f8f0ef4 100644 --- a/spec/acceptance/ferm_spec.rb +++ b/spec/acceptance/ferm_spec.rb @@ -26,6 +26,19 @@ iptables_output = case sut_os                        '-A HTTP -s 127.0.0.1/32 -p tcp -m comment --comment ["]*allow_http_localhost["]* -m tcp --dport 80 -j ACCEPT'                      ]                    end + +iptables_output_custom = ['-A FORWARD -s 10.8.0.0/24 -p udp -m comment --comment "OpenVPN - FORWORD all udp traffic from network 10.8.0.0/24 to subchain OPENVPN_FORWORD_RULES" -j OPENVPN_FORWORD_RULES', +                          '-A OPENVPN_FORWORD_RULES -s 10.8.0.0/24 -i tun0 -o enp4s0 -p udp -m conntrack --ctstate NEW -j ACCEPT'] + +# When `install_method` is `vcsrepo` with `vcstag` >= `v2.5` ferm call "legacy" +# xtables tools because nft based tools are incompatible. +iptables_save_cmd = case sut_os +                    when 'Debian-10' +                      'iptables-legacy-save' +                    else +                      'iptables-save' +                    end +  basic_manifest = %(    class { 'ferm':      manage_service    => true, @@ -43,12 +56,36 @@ basic_manifest = %(        },      },      ip_versions      => ['ip'], #only ipv4 available with CI -  }  )  describe 'ferm' do -  context 'with basics settings' do -    pp = basic_manifest +  context 'with basics settings and vcsrepo install_method' do +    pp = [basic_manifest, "install_method => 'vcsrepo',}"].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 package('ferm') do +      it { is_expected.not_to be_installed } +    end + +    describe service('ferm') do +      it { is_expected.to be_running } +    end + +    describe command("#{iptables_save_cmd} -t filter") do +      its(:stdout) { is_expected.to match %r{.*filter.*:INPUT DROP.*:FORWARD DROP.*:OUTPUT ACCEPT.*}m } +      its(:stdout) { is_expected.not_to match %r{state INVALID -j DROP} } +      its(:stdout) { is_expected.to match %r{allow_acceptance_tests.*-j ACCEPT}m } +    end +  end + +  context 'with basics settings and default install_method' do +    pp = [basic_manifest, '}'].join("\n")      it 'works with no error' do        apply_manifest(pp, catch_failures: true) @@ -101,7 +138,7 @@ describe 'ferm' do            require           => Ferm::Chain['check-http'],          }        ) -      pp = [basic_manifest, advanced_manifest].join("\n") +      pp = [basic_manifest, '}', advanced_manifest].join("\n")        it 'works with no error' do          apply_manifest(pp, catch_failures: true) @@ -124,7 +161,7 @@ describe 'ferm' do        end      end -    context 'with dropping INVALID pakets' do +    context 'with dropping INVALID packets' do        pp2 = %(          class { 'ferm':            manage_service                            => true, @@ -162,4 +199,62 @@ describe 'ferm' do        end      end    end + +  context 'with custom chain using ferm DSL as content' do +    advanced_manifest = %( +      $my_rules = @(EOT) +      chain OPENVPN_FORWORD_RULES { +        proto udp { +          interface tun0 { +            outerface enp4s0 { +              mod conntrack ctstate (NEW) saddr @ipfilter((10.8.0.0/24)) ACCEPT; +            } +          } +        } +      } +      | EOT + +      ferm::chain{'OPENVPN_FORWORD_RULES': +        chain   => 'OPENVPN_FORWORD_RULES', +        content => $my_rules, +      } + +      ferm::rule { "OpenVPN - FORWORD all udp traffic from network 10.8.0.0/24 to subchain OPENVPN_FORWORD_RULES": +        chain     => 'FORWARD', +        action    => 'OPENVPN_FORWORD_RULES', +        saddr     => '10.8.0.0/24', +        proto     => 'udp', +      } +    ) + +    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(iptables_output_custom[0]). \ +          with_table('filter'). \ +          with_chain('FORWARD') +      end +      it do +        is_expected.to have_rule(iptables_output_custom[1]). \ +          with_table('filter'). \ +          with_chain('OPENVPN_FORWORD_RULES') +      end +    end + +    describe service('ferm') do +      it { is_expected.to be_running } +    end + +    describe command('iptables-save') do +      its(:stdout) { is_expected.to match %r{FORWARD.*-j OPENVPN_FORWORD_RULES} } +    end +  end  end diff --git a/spec/defines/chain_spec.rb b/spec/defines/chain_spec.rb index 1a6bb44..52cc88c 100644 --- a/spec/defines/chain_spec.rb +++ b/spec/defines/chain_spec.rb @@ -70,6 +70,34 @@ describe 'ferm::chain', type: :define do          it { is_expected.to compile.and_raise_error(%r{Can only set a default policy for builtin chains}) }        end + +      context 'with custom chain FERM-DSL using content parameter' do +        let(:title) { 'FERM-DSL' } +        let :params do +          { +            content: 'mod rpfilter invert DROP;' +          } +        end + +        it { is_expected.to compile.with_all_deps } +        it { is_expected.to contain_concat__fragment('filter-FERM-DSL-config-include') } +        it do +          is_expected.to contain_concat__fragment('filter-FERM-DSL-custom-content'). \ +            with_content(%r{mod rpfilter invert DROP;}) +        end +        it do +          is_expected.not_to contain_concat__fragment('filter-FERM-DSL-policy') +        end +        it do +          is_expected.not_to contain_concat__fragment('filter-FERM-DSL-footer') +        end +        if facts[:os]['name'] == 'Debian' +          it { is_expected.to contain_concat('/etc/ferm/ferm.d/chains/filter-FERM-DSL.conf') } +        else +          it { is_expected.to contain_concat('/etc/ferm.d/chains/filter-FERM-DSL.conf') } +        end +        it { is_expected.to contain_ferm__chain('FERM-DSL') } +      end      end    end  end diff --git a/templates/dropin_ferm.conf.epp b/templates/dropin_ferm.conf.epp new file mode 100644 index 0000000..d5ed63b --- /dev/null +++ b/templates/dropin_ferm.conf.epp @@ -0,0 +1,6 @@ +# THIS SNIPPET IS MANAGED BY PUPPET +[Service] +ExecStart= +ExecStart=/usr/sbin/ferm <%= $ferm::configfile %> +ExecStop= +ExecStop=/usr/sbin/ferm -F <%= $ferm::configfile %> diff --git a/templates/ferm_chain_custom.conf.epp b/templates/ferm_chain_custom.conf.epp new file mode 100644 index 0000000..356311c --- /dev/null +++ b/templates/ferm_chain_custom.conf.epp @@ -0,0 +1,4 @@ +<%- | String[1] $content, +| -%> +# THIS FILE IS MANAGED BY PUPPET +<%= $content %>  | 
