diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 81c7a25808f07efbc52352dd78f31cccf08c4d49..ce937bb2129f35e510589f1896138400cc5cafb8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,6 +42,7 @@ kitchen: - banner - chef - ferm + - firewall - linuxlogo - mail - nsswitch diff --git a/.kitchen.yml b/.kitchen.yml index 7a0ad448aebdc73ff26dfc20033fa52ec9992379..b0c4387fa96002744935ece0853c93d518ca6a8d 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -170,6 +170,14 @@ suites: - 'policy ACCEPT;' FORWARD: - 'policy DROP;' + - name: sys_firewall + run_list: + - recipe[firewall-test::default] + attributes: + sys: + firewall: + manage: true + disable: false - name: sys_linuxlogo run_list: - recipe[sys::linuxlogo] diff --git a/Berksfile b/Berksfile index c521dfdb52c6fccf9e674fbdb251a4e34d96f321..7346c955a41c2f8c182d1f2cd3634beca1c5bafd 100644 --- a/Berksfile +++ b/Berksfile @@ -8,3 +8,7 @@ cookbook 'fixtures', path: 'test/unit/fixtures', group: :chefspec # avoid https://github.com/sous-chefs/line/issues/92 # by pulling directly from github: cookbook 'line', github: 'sous-chefs/line', tag: "v0.6.3" + +group :integration do + cookbook 'firewall-test', path: 'test/fixtures/cookbooks/firewall-test' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index 81fcea04a5a921e3799af5ac7a09c63016d53259..c05c3175b2ebebeb5b27bd333982689e68421866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [1.64.0] + +### Added +- New recipe [`sys::firewall`](recipes/firewall.rb) +- New resource [`firewall`](resources/firewall_rule.rb) +- New resource [`firewall_rule`](resources/firewall_rule.rb) +- New attributes for configuring [`firewall`](attributes/firewall.rb) +- [`Documentation`](documents/firewall.md) +- Tests + ## [1.63.1] - 2022-02-28 ## Changed diff --git a/attributes/firewall.rb b/attributes/firewall.rb new file mode 100644 index 0000000000000000000000000000000000000000..326db519c8b8bb9c98c8dbc2a0cb5672d1469312 --- /dev/null +++ b/attributes/firewall.rb @@ -0,0 +1,23 @@ +default['sys']['firewall']['manage'] = false +default['sys']['firewall']['disable'] = true +default['sys']['firewall']['allow_ssh'] = true +default['sys']['firewall']['allow_loopback'] = true +default['sys']['firewall']['allow_icmp'] = true +default['sys']['firewall']['allow_established'] = true +default['sys']['firewall']['defaults']['policy'] = { + 'input' => 'drop', + 'forward' => 'drop', + 'output' => 'accept', +} +default['sys']['firewall']['defaults']['ruleset'] = { + 'add table inet filter' => 1, + 'add table ip6 nat' => 1, + 'add table ip nat' => 1, + "add chain inet filter INPUT { type filter hook input priority 0 ; policy #{node['sys']['firewall']['defaults']['policy']['input']}; }" => 2, + "add chain inet filter OUTPUT { type filter hook output priority 0 ; policy #{node['sys']['firewall']['defaults']['policy']['output']}; }" => 2, + "add chain inet filter FOWARD { type filter hook forward priority 0 ; policy #{node['sys']['firewall']['defaults']['policy']['forward']}; }" => 2, + 'add chain ip nat POSTROUTING { type nat hook postrouting priority 100 ;}' => 2, + 'add chain ip nat PREROUTING { type nat hook prerouting priority -100 ;}' => 2, + 'add chain ip6 nat POSTROUTING { type nat hook postrouting priority 100 ;}' => 2, + 'add chain ip6 nat PREROUTING { type nat hook prerouting priority -100 ;}' => 2, +} diff --git a/documents/firewall.md b/documents/firewall.md new file mode 100644 index 0000000000000000000000000000000000000000..820533e0c335566497b6e9b4ba7b7deb23971e99 --- /dev/null +++ b/documents/firewall.md @@ -0,0 +1,86 @@ +# `sys::firewall` + +Use the firewall recipe to configure nftables. + +↪ `attributes/firewall.rb` +↪ `recipes/firewall.rb` +↪ `resources/firewall.rb` +↪ `resources/firewall_rule.rb` +↪ `libraries/sys_helpers_firewall.rb` +↪ `documents/firewall.rb` +↪ `test/unit/recipies/firewall_spec.rb` + +## Basic Usage + +The `sys::firewall` recipe **does nothing unless explicitly activated** by setting `node['sys']['firewall']['manage']` to `true`. + +### Disable nftables + +`node['sys']['firewall']['disable']` will turn off *nftables*: + ```ruby +node['sys']['firewall']['manage'] = true # To manage nftables at all +node['sys']['firewall']['disable'] = true # To disable nftables +``` + +### Default rules + +When `sys::firewall` is activated and *nftables* is not disabled, the default rules are applied via the following self-explaining attributes, all of which default to true. Adjust to your needs: + +↪ `node['sys']['firewall']['allow_established']` +↪ `node['sys']['firewall']['allow_icmp']` +↪ `node['sys']['firewall']['allow_loopback']` +↪ `node['sys']['firewall']['allow_ssh']` + +This will give you the following default rules: + + table inet filter { + chain INPUT { + type filter hook input priority 0; policy drop; + iif "lo" accept comment "allow loopback" + icmp type echo-request accept comment "allow icmp" + tcp dport ssh accept comment "allow world to ssh" + ct state established,related accept comment "established" + } + + chain OUTPUT { + type filter hook output priority 0; policy accept; + } + + chain FOWARD { + type filter hook forward priority 0; policy drop; + } + } + table ip6 nat { + chain POSTROUTING { + type nat hook postrouting priority 100; policy accept; + } + + chain PREROUTING { + type nat hook prerouting priority -100; policy accept; + } + } + table ip nat { + chain POSTROUTING { + type nat hook postrouting priority 100; policy accept; + } + + chain PREROUTING { + type nat hook prerouting priority -100; policy accept; + } + } + + +## Using `sys::firewall` from other recipes + +Depend on the `sys`-cookbook in the `metadata.rb` and include +`sys::firewall` in the runlist. If access via ports `443` and `80` +should be possible, write a resource like this: + +```ruby +firewall_rule 'allow http(s)` do + port [80,443] +end +``` + +For further examples see the recipe +[sys::firewall](recipes/firewall.rb) and the recipe [firewall-test::default](test/fixtures/cookbooks/firewall-test/recipes/default.rb). diff --git a/libraries/sys_helpers_firewall.rb b/libraries/sys_helpers_firewall.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f49519ca217332e668ba499fffd142c477f5f99 --- /dev/null +++ b/libraries/sys_helpers_firewall.rb @@ -0,0 +1,174 @@ +# +# Cookbook Name:: sys +# Library:: Helpers::Firewall +# +# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH +# +# Authors: +# Matthias Pausch (m.pausch@gsi.de) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This code is an adjustment of https://github.com/sous-chefs/firewall +# + +module Sys + module Helpers + module Firewall + require 'ipaddr' + include Chef::Mixin::ShellOut + + def dport(new_resource) + new_resource.dest_port || new_resource.port + end + + def sport(new_resource) + new_resource.source_port + end + + def valid_ips?(ips) + Array(ips).inject(false) do |a, ip| + a || !!IPAddr.new(ip) + end + end + + def port_to_s(p) + if p.is_a?(String) + p + elsif p && p.is_a?(Integer) + p.to_s + elsif p && p.is_a?(Array) + p_strings = p.map { |o| port_to_s(o) } + "{#{p_strings.sort.join(',')}}" + elsif p && p.is_a?(Range) + "#{p.first}-#{p.last}" + end + end + + def disabled? + node['sys']['firewall']['disable'] + end + + def managed? + node['sys']['firewall']['manage'] + end + + def build_rule_file(rules) + contents = [] + sorted_values = rules.values.sort.uniq + sorted_values.each do |sorted_value| + contents << "# position #{sorted_value}" + contents << rules.select { |_,v| v == sorted_value }.keys.join("\n") + end + "#{contents.join("\n")}\n" + end + + unless defined? CHAIN + CHAIN = { + in: 'INPUT', + out: 'OUTPUT', + pre: 'PREROUTING', + post: 'POSTROUTING', + forward: 'FORWARD', + }.freeze + end + + unless defined? TARGET + TARGET = { + accept: 'accept', + allow: 'accept', + deny: 'drop', + drop: 'drop', + log: 'log prefix "nftables:" group 0', + masquerade: 'masquerade', + redirect: 'redirect', + reject: 'reject', + }.freeze + end + + def build_firewall_rule(rule_resource) + return rule_resource.raw.strip if rule_resource.raw + + ip_family = rule_resource.family + table = if [:pre, :post].include?(rule_resource.direction) + 'nat' + else + 'filter' + end + firewall_rule = if table == 'nat' + "add rule #{ip_family} #{table} " + else + "add rule inet #{table} " + end + firewall_rule << CHAIN.fetch(rule_resource.direction.to_sym) + firewall_rule << ' ' + firewall_rule << "iif #{rule_resource.interface} " if rule_resource.interface + firewall_rule << "oif #{rule_resource.dest_interface} " if rule_resource.dest_interface + + if rule_resource.source + source_ips = Array(rule_resource.source).map { |ip| IPAddr.new(ip) } + source_ips.delete(IPAddr.new('0.0.0.0/0')) + source_ips.delete(IPAddr.new('::/128')) + # Only works on buster and newer. In older debian-versions + # there is no prefix-method for IPv4-addresses. + addrs = source_ips.map { |ip| "#{ip}/#{ip.prefix}" } + if addrs.length == 1 + firewall_rule << "#{ip_family} saddr #{addrs.first} " + elsif addrs.length > 1 + firewall_rule << "#{ip_family} saddr {#{addrs.join(',')}} " + end + end + firewall_rule << "#{ip_family} daddr #{rule_resource.destination} " if rule_resource.destination + + case rule_resource.protocol + when :icmp + firewall_rule << 'icmp type echo-request ' + when :'ipv6-icmp', :icmpv6 + firewall_rule << 'icmpv6 type { echo-request, nd-router-solicit, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } ' + when :tcp, :udp + firewall_rule << "#{rule_resource.protocol} sport #{port_to_s(sport(rule_resource))} " if sport(rule_resource) + firewall_rule << "#{rule_resource.protocol} dport #{port_to_s(dport(rule_resource))} " if dport(rule_resource) + when :esp, :ah + firewall_rule << "#{ip_family} #{ip_family == :ip6 ? 'nexthdr' : 'protocol'} #{rule_resource.protocol} " + + # nothing to do default :ipv6, :none + end + + firewall_rule << "ct state #{Array(rule_resource.stateful).join(',').downcase} " if rule_resource.stateful + firewall_rule << "#{TARGET[rule_resource.command.to_sym]} " + firewall_rule << " to #{rule_resource.redirect_port} " if rule_resource.command == :redirect + firewall_rule << "comment \"#{rule_resource.description}\" " if rule_resource.include_comment + firewall_rule.strip! + firewall_rule + end + + def log_nftables + shell_out!('nft -n list ruleset') + rescue Mixlib::ShellOut::ShellCommandFailed + Chef::Log.info('log_nftables failed!') + rescue Mixlib::ShellOut::CommandTimeout + Chef::Log.info('log_nftables timed out!') + end + + def default_ruleset(current_node) + current_node['sys']['firewall']['defaults']['ruleset'].to_h + end + + def ensure_default_rules_exist(current_node, new_resource) + input = new_resource.rules || {} + input.merge!(default_ruleset(current_node).to_h) + new_resource.rules(input) + end + end + end +end diff --git a/metadata.rb b/metadata.rb index b773606e6cb872099ccc1c8878f13bd80f300425..2fe692a4b5a0689f14e01a4044aa9da5e321154e 100644 --- a/metadata.rb +++ b/metadata.rb @@ -16,4 +16,4 @@ supports 'debian' depends 'line', '< 1.0' depends 'chef-vault' -version '1.63.1' +version '1.64.0' diff --git a/recipes/firewall.rb b/recipes/firewall.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac197f30b2644bd732da311ef234943355cb3d0d --- /dev/null +++ b/recipes/firewall.rb @@ -0,0 +1,58 @@ +# +# Cookbook Name:: sys +# Recipe:: firewall +# +# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH +# +# Authors: +# Matthias Pausch (m.pausch@gsi.de) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This code is an adjustment of https://github.com/sous-chefs/firewall +# + +return unless node['sys']['firewall']['manage'] +return unless Gem::Requirement.new('>= 12.15').satisfied_by?(Gem::Version.new(Chef::VERSION)) + +fw_action = node['sys']['firewall']['disable'] ? :disable : :install +firewall 'default' do + action fw_action +end + +firewall_rule 'allow loopback' do + interface 'lo' + protocol :none + command :allow + only_if { node['sys']['firewall']['allow_loopback'] } +end + +firewall_rule 'allow icmp' do + protocol :icmp + command :allow + only_if { node['sys']['firewall']['allow_icmp'] } +end + +firewall_rule 'allow world to ssh' do + port 22 + source '0.0.0.0/0' + only_if { node['sys']['firewall']['allow_ssh'] } +end + +# allow established connections +firewall_rule 'established' do + stateful [:related, :established] + protocol :none # explicitly don't specify protocol + command :allow + only_if { node['sys']['firewall']['allow_established'] } +end diff --git a/recipes/nftables.rb b/recipes/nftables.rb index bb5d279ce5ad037e3bd398e5e0fb08eb66232b1a..25007b8ab31ce987587777c8b77f212e10faca8e 100644 --- a/recipes/nftables.rb +++ b/recipes/nftables.rb @@ -2,41 +2,25 @@ # Cookbook Name:: sys # Recipe:: nftables # -# Copyright 2014, HPC Team +# Copyright 2014-2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH +# +# Authors: +# Matthias Pausch +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # -unless node['sys']['nftables'].empty? - - if node['debian'] && node['debian']['codename'] && node['debian']['codename'].eql?('stretch') - - package 'nftables' do - action :upgrade - end - - # Future version will not include the init-script, cf. https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=804648 - file '/etc/init.d/nftables' do - action :delete - end - - nftables_serviceaction = :enable - nftables_action = :start - - unless node['sys']['nftables']['active'] - nftables_serviceaction = :disable - nftables_action = :stop - end - - template '/etc/nftables.conf' do - source 'etc_nftables.conf.erb' - mode '0644' - owner 'root' - group 'adm' - notifies :reload, 'service[nftables]', :immediately - end +# this recipe has been superseded by sys::firewall +return unless node['sys']['nftables'] - service 'nftables' do - supports :reload => true, :restart => true - action [ nftables_action, nftables_serviceaction ] - end - end -end +Chef::Log.warn('The sys::nftables recipe has been removed. Use sys::firewall instead') diff --git a/resources/firewall.rb b/resources/firewall.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7487171bd7d24017c5017ff2761a8dc776019bc --- /dev/null +++ b/resources/firewall.rb @@ -0,0 +1,119 @@ +# +# Cookbook Name:: sys +# Resource:: firewall +# +# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH +# +# Authors: +# Matthias Pausch (m.pausch@gsi.de) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This code is an adjustment of https://github.com/sous-chefs/firewall +# + +if Gem::Requirement.new('>= 12.15').satisfied_by?(Gem::Version.new(Chef::VERSION)) + + action_class do + + include Sys::Helpers::Firewall + + def lookup_or_create_service(name) + begin + nftables_service = Chef.run_context.resource_collection.find(service: name) + rescue + nftables_service = service name do + action :nothing + end + end + nftables_service + end + + def lookup_or_create_rulesfile(name) + begin + nftables_file = Chef.run_context.resource_collection.find(file: name) + rescue + nftables_file = file name do + action :nothing + end + end + nftables_file + end + end + + #unified_mode true + + provides :firewall, os: 'linux', platform: %w(debian) + + property :rules, Hash + + def whyrun_supported? + false + end + + action :install do + return unless managed? + + # Ensure the package is installed + nft_pkg = package 'nftables' do + action :nothing + end + nft_pkg.run_action(:install) + + with_run_context :root do + edit_resource('sys_firewall', new_resource.name) do + action :nothing + delayed_action :rebuild + end + end + end + + action :rebuild do + return if !managed? + + ensure_default_rules_exist(node, new_resource) + # prints all the firewall rules + log_nftables + + # this takes the commands in each hash entry and builds a rule file + nftables_file = lookup_or_create_rulesfile('/etc/nftables.conf') + nftables_file.content "#!/usr/sbin/nft -f\nflush ruleset\n#{build_rule_file(new_resource.rules)}" + nftables_file.run_action(:create) + if disabled? + new_resource.run_action(:disable) + return + end + + nftables_service = lookup_or_create_service('nftables') + nftables_service.run_action(:enable) + + if nftables_file.updated_by_last_action? + nftables_service.run_action(:restart) + else + nftables_service.run_action(:start) + end + end + + action :restart do + nftables_service = lookup_or_create_service('nftables') + nftables_service.run_action(:restart) + end + + action :disable do + return unless managed? + nftables_service = lookup_or_create_service('nftables') + %i(disable stop).each do |a| + nftables_service.run_action(a) + end + end +end diff --git a/resources/firewall_rule.rb b/resources/firewall_rule.rb new file mode 100644 index 0000000000000000000000000000000000000000..21a56c202ae260d4a4a9717f0a6439dc46ca4b4f --- /dev/null +++ b/resources/firewall_rule.rb @@ -0,0 +1,113 @@ +# +# Cookbook Name:: sys +# Resource:: firewall_rule +# +# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH +# +# Authors: +# Matthias Pausch (m.pausch@gsi.de) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This code is an adjustment of https://github.com/sous-chefs/firewall +# + +if Gem::Requirement.new('>= 12.15').satisfied_by?(Gem::Version.new(Chef::VERSION)) + require 'ipaddr' + action_class do + include Sys::Helpers::Firewall + + def return_early?(new_resource) + !new_resource.notify_firewall || + !(new_resource.action.include?(:create) && + !new_resource.should_skip?(:create)) || + !managed? + end + end + + provides :firewall_rule + default_action :create + + property :firewall_name, String, default: 'default' + + property :command, Symbol, default: :allow, equal_to: %i[ + accept allow deny drop log masquerade redirect reject + ] + + property :protocol, [Integer, Symbol], default: :tcp, + callbacks: { 'must be either :tcp, :udp, :icmp, :\'ipv6-icmp\', :icmpv6, :none, or a valid IP protocol number' => + ->(p) do + %i[udp tcp icmp icmpv6 ipv6-icmp esp ah ipv6 none].include?(p) || (0..142).include?(p) + end } + property :direction, Symbol, equal_to: [:in, :out, :pre, :post, :forward], default: :in + property :logging, Symbol, equal_to: [:connections, :packets] + # nftables handles ip6 and ip simultaneously. Except for directions + # :pre and :post, where where either :ip6 or :ip must be specified. + # callback should prevent from mixing that up. + property :family, Symbol, equal_to: [:ip6, :ip], default: :ip + property :source, [String, Array], callbacks: { + 'must be a valid ip address' => ->(ips) do + Array(ips).inject(false) do |a, ip| + a || !!IPAddr.new(ip) + end + end + } + property :source_port, [Integer, String, Array, Range] # source port + property :interface, String + + # I would rather call them sport and dport, without alternatives. + # However, firewall rules should be kept compatible with future + # releases of the firewall cookbook from the sous-chefs. + property :port, [Integer, String, Array, Range] # shorthand for dest_port + property :destination, [String, Array], callbacks: { + 'must be a valid ip address' => ->(ips) do + Array(ips).inject(false) do |a, ip| + a || !!IPAddr.new(ip) + end + end + } + property :dest_port, [Integer, String, Array, Range] + property :dest_interface, String + + property :position, Integer, default: 50 + property :stateful, [Symbol, Array] + property :redirect_port, Integer + property :description, String, name_property: true + property :include_comment, [true, false], default: true + + # for when you just want to pass a raw rule + property :raw, String + + # do you want this rule to notify the firewall to recalculate + # (and potentially reapply) the firewall_rule(s) it finds? + property :notify_firewall, [true, false], default: true + + action :create do + return if return_early?(new_resource) + + with_run_context :root do + begin + edit_resource!('sys_firewall', new_resource.firewall_name) do |fw_rule| + r = rules.dup || {} + r.merge!({ + build_firewall_rule(fw_rule) => fw_rule.position + }) + rules(r) + delayed_action :rebuild + end + rescue Chef::Exceptions::ResourceNotFound + Chef::Log.warn "Resource firewall['#{new_resource.firewall_name}'] not found in resource collection. Not configuring firewall." + end + end + end +end diff --git a/test/fixtures/cookbooks/firewall-test/metadata.rb b/test/fixtures/cookbooks/firewall-test/metadata.rb new file mode 100644 index 0000000000000000000000000000000000000000..e40dc58ce75f58c308c12be114ffe5a857718689 --- /dev/null +++ b/test/fixtures/cookbooks/firewall-test/metadata.rb @@ -0,0 +1,4 @@ +name 'firewall-test' +version '0.0.0' + +depends 'sys' diff --git a/test/fixtures/cookbooks/firewall-test/recipes/default.rb b/test/fixtures/cookbooks/firewall-test/recipes/default.rb new file mode 100644 index 0000000000000000000000000000000000000000..3777935e5447bf33b7f22fbc5f125541df2e28b8 --- /dev/null +++ b/test/fixtures/cookbooks/firewall-test/recipes/default.rb @@ -0,0 +1,137 @@ +return unless Gem::Requirement.new('>= 12.15').satisfied_by?(Gem::Version.new(Chef::VERSION)) + +include_recipe 'sys::firewall' + +firewall_rule 'ssh22' do + port 22 + command :allow +end + +firewall_rule 'port array' do + port [2222, 2200] + command :allow +end + +firewall_rule 'range' do + port 1000..1100 + command :allow +end + +firewall_rule 'range-udp' do + port 60000..61000 + protocol :udp +end + +firewall_rule 'ping6' do + protocol :icmpv6 +end + +# other rules +firewall_rule 'temp1' do + port 1234 + command :deny +end + +firewall_rule 'temp2' do + port 1235 + command :reject +end + +firewall_rule 'addremove' do + port 1236 + command :allow +end + +firewall_rule 'addremove2' do + port 1236 + command :deny +end + +firewall_rule 'protocolnum' do + protocol 112 + command :allow +end + +firewall_rule 'prepend' do + port 7788 + position 5 +end + +firewall_rule "block single ip" do + source '192.168.99.99' + position 49 + command :reject +end + +firewall_rule 'block ip-range' do + source ['192.168.99.99', '192.168.100.100'] + command :drop +end + +firewall_rule 'ipv6-source' do + port 80 + family :ip6 + source '2001:db8::ff00:42:8329' + command :allow +end + +firewall_rule 'array' do + port [1234, 5000..5100, '5678'] + command :allow + +end + +firewall_rule 'RPC Port Range In' do + port 5000..5100 + protocol :tcp + command :allow + direction :in + +end + +firewall_rule 'HTTP HTTPS' do + port [80, 443] + protocol :tcp + direction :out + command :allow +end + +firewall_rule 'port2433' do + description 'This should not be included' + include_comment false + source '127.0.0.0/8' + port 2433 + direction :in + command :allow +end + +firewall_rule 'esp' do + protocol :esp + command :allow +end + +firewall_rule 'ah' do + protocol :ah + command :allow +end + +firewall_rule 'esp-ipv6' do + source '::' + family :ip6 + protocol :esp + command :allow +end + +firewall_rule 'ah-ipv6' do + source '::' + family :ip6 + protocol :ah + command :allow +end + +firewall_rule 'redirect' do + direction :pre + port 5555 + redirect_port 6666 + command :redirect +end diff --git a/test/integration/sys_firewall/serverspec/Gemfile b/test/integration/sys_firewall/serverspec/Gemfile new file mode 100644 index 0000000000000000000000000000000000000000..3794d9d424524f68c7b1e4e68fb7f7c604b0fe5f --- /dev/null +++ b/test/integration/sys_firewall/serverspec/Gemfile @@ -0,0 +1,10 @@ +source 'http://rubygems.org' + +group :jessie do + # net-ssh >= 5 requires ruby 2.2 and + # net-telnet >= 0.2 requires ruby 2.3 + if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.2.0') + gem 'net-ssh', '< 5' + gem 'net-telnet', '< 0.2' + end +end diff --git a/test/integration/sys_firewall/serverspec/localhost/firewall_spec.rb b/test/integration/sys_firewall/serverspec/localhost/firewall_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..75f877356df111e64dde8fbceff808e626b3fced --- /dev/null +++ b/test/integration/sys_firewall/serverspec/localhost/firewall_spec.rb @@ -0,0 +1,81 @@ +# +# Cookbook Name:: sys +# Integration tests for sys::firewall +# +# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH +# +# Authors: +# Matthias Pausch (m.pausch@gsi.de) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +expected_rules = [ + /^table inet filter {$/, + /\s+type filter hook output priority.*/, + /\s+type filter hook forward priority.*/, + /\s+type filter hook input priority 0; policy drop;/, + /\s+tcp dport 7788 accept.*/, + /\s+ip saddr 192.168.99.99 reject.*/, + /\s+ip saddr { 192.168.99.99, 192.168.100.100 } drop.*/, + /\s+iif "lo" accept comment "allow loopback"/, + /\s+icmp type echo-request accept.*$/, + /\s+tcp dport 22 accept.*$/, + /\s+udp dport 60000-61000 accept.*$/, + /\s+ct state established,related accept.*$/, + /\s+icmpv6 type { echo-request, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept.*$/, + /\s+tcp dport 22 accept.*$/, + /\s+tcp dport { 2200, 2222 } accept.*$/, + /\s+tcp dport 1234 drop.*$/, + /\s+tcp dport 1235 reject.*$/, + /\s+tcp dport 1236 drop.*$/, + /\s+ip6 saddr 2001:db8::ff00:42:8329 tcp dport 80 accept.*$/, + /\s+tcp dport 1000-1100 accept.*$/, + /\s+tcp dport { 1234, 5000-5100, 5678 } accept.*$/, + /\s+tcp dport 5000-5100 accept.*$/, + %r{\s+ip saddr 127.0.0.0/8 tcp dport 2433 accept.*$}, + /\s+ip protocol esp accept.*$/, + /\s+ip protocol ah accept.*$/, + /\s+ip6 nexthdr esp accept.*$/, + /\s+ip6 nexthdr ah accept.*$/, +] + +if os[:release].to_i >= 10 + + describe command('nft -nn list ruleset') do + expected_rules.each do |r| + its(:stdout) { should match(r) } + end + end + + describe package('nftables') do + it { should be_installed } + end + + describe service('nftables') do + it { should be_enabled } + it { should be_running } + end + +else + describe package('nftables') do + it { should_not be_installed } + end + + describe service('nftables') do + it { should_not be_enabled } + it { should_not be_running } + end +end diff --git a/test/integration/sys_firewall/serverspec/spec_helper.rb b/test/integration/sys_firewall/serverspec/spec_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..37af1b4599c1ffc0e7bba821c857879acc06d7e9 --- /dev/null +++ b/test/integration/sys_firewall/serverspec/spec_helper.rb @@ -0,0 +1,3 @@ +require 'serverspec' + +set :backend, :exec diff --git a/test/unit/recipes/firewall_spec.rb b/test/unit/recipes/firewall_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f03b42c2c8e1156681442e7f228382336afc8cf8 --- /dev/null +++ b/test/unit/recipes/firewall_spec.rb @@ -0,0 +1,76 @@ +# +# Cookbook Name:: sys +# Unit tests for sys::firewall +# +# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH +# +# Authors: +# Matthias Pausch +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +describe 'sys::firewall' do + let(:chef_run) { ChefSpec::SoloRunner.new } + + context 'node.sys.ferm.table is empty' do + before do + chef_run.converge(described_recipe) + end + + it 'does nothing' do + expect(chef_run.run_context.resource_collection).to be_empty + end + end + + context 'with some rules' do + before do + chef_run.node.default[:sys][:ferm][:rules][:ip][:filter][:OUTPUT] = [ + 'policy ACCEPT;', + 'mod state state (ESTABLISHED RELATED) ACCEPT;' + ] + chef_run.converge(described_recipe) + end + + it 'upgrades package nftables' do + expect(chef_run).to install_package('nftables') + end + + it 'manages /etc/nftables.conf' do + expect(chef_run).to create_template('/etc/nftables.conf').with_mode('0644').with_owner('root').with_group('adm') + template = chef_run.template('/etc/nftables.conf') + expect(template).to notify('service[nftables]').to(:reload).immediately + end + + it 'enables and starts service "nftables"' do + expect(chef_run).to enable_service('nftables') + expect(chef_run).to start_service('nftables') + end + end + + context "with node['sys']['firewall']['active'] == false" do + before do + chef_run.node.default[:sys][:ferm][:rules][:ip][:filter][:OUTPUT] = [ + 'policy ACCEPT;', + 'mod state state (ESTABLISHED RELATED) ACCEPT;' + ] + chef_run.node.default['sys']['firewall']['active'] = false + chef_run.converge(described_recipe) + end + + it 'disables and stops service "nftables"' do + expect(chef_run).to disable_service('nftables') + expect(chef_run).to stop_service('nftables') + end + end +end