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 %> |