aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md11
-rw-r--r--REFERENCE.md62
-rw-r--r--manifests/chain.pp54
-rw-r--r--manifests/config.pp10
-rw-r--r--manifests/init.pp8
-rw-r--r--manifests/install.pp36
-rw-r--r--manifests/service.pp2
-rw-r--r--metadata.json8
-rw-r--r--spec/acceptance/ferm_spec.rb105
-rw-r--r--spec/defines/chain_spec.rb28
-rw-r--r--templates/dropin_ferm.conf.epp6
-rw-r--r--templates/ferm_chain_custom.conf.epp4
12 files changed, 303 insertions, 31 deletions
diff --git a/README.md b/README.md
index 01217bd..64a9f84 100644
--- a/README.md
+++ b/README.md
@@ -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 %>