From f26f6d87e94a3c66455818c627e7e816448a5045 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Wed, 18 Dec 2024 14:09:37 -0800 Subject: [PATCH 01/24] Remove FreeBSD from Test Kitchen; it's not supported by this cookbook Signed-off-by: Joseph Larionov --- kitchen.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/kitchen.yml b/kitchen.yml index 4791d8d8..d8f6d4ff 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -30,8 +30,6 @@ platforms: - name: debian-11 - name: fedora-34 - name: fedora-latest - - name: freebsd-11 - - name: freebsd-12 - name: opensuse-leap-15 - name: oracle-8 - name: ubuntu-18.04 From d5c69f5ea566a662ad5d5a869a6a91c4a84ee372 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Wed, 18 Dec 2024 14:21:54 -0800 Subject: [PATCH 02/24] Correct the suite platform matrix in Test Kitchen Suites now correctly test against all compatible platforms for each firewall solution. Signed-off-by: Joseph Larionov --- kitchen.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/kitchen.yml b/kitchen.yml index d8f6d4ff..d777e5c0 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -34,6 +34,8 @@ platforms: - name: oracle-8 - name: ubuntu-18.04 - name: ubuntu-20.04 + - name: ubuntu-22.04 + - name: ubuntu-24.04 - name: windows-2016 driver_config: box: tas50/windows_2016 @@ -49,11 +51,9 @@ suites: - name: firewalld excludes: - - almalinux-8 - - centos-8 - debian-9 - debian-10 - - oracle-8 + - debian-11 - ubuntu-18.04 - ubuntu-20.04 - windows-2016 @@ -67,10 +67,9 @@ suites: - almalinux-8 - amazonlinux-2 - centos-7 - - centos-8 + - centos-stream-8 + - fedora-34 - fedora-latest - - freebsd-11 - - freebsd-12 - opensuse-leap-15 - oracle-8 - windows-2016 @@ -83,9 +82,7 @@ suites: excludes: - debian-9 - debian-10 - - oracle-8 - - ubuntu-18.04 - - ubuntu-20.04 + - debian-11 - windows-2016 - windows-2019 run_list: From 624f186c7976ccfbdcb629011d8d96e1b4e0d4aa Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Wed, 18 Dec 2024 14:59:16 -0800 Subject: [PATCH 03/24] Add modern platforms and remove obsolete ones Signed-off-by: Joseph Larionov --- kitchen.dokken.yml | 4 ++-- kitchen.yml | 39 +++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/kitchen.dokken.yml b/kitchen.dokken.yml index 998bb20c..5b4adc79 100644 --- a/kitchen.dokken.yml +++ b/kitchen.dokken.yml @@ -57,12 +57,12 @@ platforms: image: dokken/opensuse-leap-15 pid_one_command: /usr/lib/systemd/systemd - - name: oraclelinux-8 + - name: oracle-8 driver: image: dokken/oraclelinux-8 pid_one_command: /usr/lib/systemd/systemd - - name: oraclelinux-9 + - name: oracle-9 driver: image: dokken/oraclelinux-9 pid_one_command: /usr/lib/systemd/systemd diff --git a/kitchen.yml b/kitchen.yml index d777e5c0..0b9df6d6 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -22,17 +22,18 @@ verifier: platforms: - name: almalinux-8 + - name: almalinux-9 - name: amazonlinux-2 - - name: centos-7 - - name: centos-stream-8 - - name: debian-9 - - name: debian-10 + - name: amazonlinux-2023 + - name: centos-stream-9 - name: debian-11 - - name: fedora-34 + - name: debian-12 - name: fedora-latest - name: opensuse-leap-15 - name: oracle-8 - - name: ubuntu-18.04 + - name: oracle-9 + - name: rockylinux-8 + - name: rockylinux-9 - name: ubuntu-20.04 - name: ubuntu-22.04 - name: ubuntu-24.04 @@ -51,11 +52,11 @@ suites: - name: firewalld excludes: - - debian-9 - - debian-10 - debian-11 - - ubuntu-18.04 + - debian-12 - ubuntu-20.04 + - ubuntu-22.04 + - ubuntu-24.04 - windows-2016 - windows-2019 run_list: @@ -65,13 +66,18 @@ suites: - name: ufw excludes: - almalinux-8 + - almalinux-9 + - almalinux-10 - amazonlinux-2 - - centos-7 - - centos-stream-8 - - fedora-34 + - amazonlinux-2023 + - centos-stream-9 + - centos-stream-10 - fedora-latest - opensuse-leap-15 - oracle-8 + - oracle-9 + - rockylinux-8 + - rockylinux-9 - windows-2016 - windows-2019 run_list: @@ -80,9 +86,7 @@ suites: - name: iptables excludes: - - debian-9 - - debian-10 - - debian-11 + - opensuse-leap-15 - windows-2016 - windows-2019 run_list: @@ -96,14 +100,17 @@ suites: - name: nftables includes: - debian-11 + - debian-12 - oracle-8 + - oracle-9 run_list: - recipe[nftables-test] - name: firewalld-dbus includes: - debian-11 - - fedora-34 + - debian-12 + - fedora-latest run_list: - recipe[firewalld-test] From 4ddbcd7db8631f0608c52c2d0f69159185ed4974 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Wed, 18 Dec 2024 14:59:54 -0800 Subject: [PATCH 04/24] Update list of platforms that the cookbook supports Signed-off-by: Joseph Larionov --- metadata.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/metadata.rb b/metadata.rb index 397c8efa..efdf8283 100644 --- a/metadata.rb +++ b/metadata.rb @@ -9,7 +9,12 @@ chef_version '>= 15.5' supports 'amazon' +supports 'almalinux' supports 'centos' supports 'debian' +supports 'fedora' +supports 'oracle' +supports 'rocky' +supports 'suse' supports 'ubuntu' supports 'windows' From 057c7f384950e0f7f2d31a36d1af9c58935fa191 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Wed, 18 Dec 2024 15:54:39 -0800 Subject: [PATCH 05/24] Get firewalld working in kitchen-dokken containers Signed-off-by: Joseph Larionov --- kitchen.dokken.yml | 26 ++++++++++++------- .../firewalld-test/recipes/default.rb | 23 ++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/kitchen.dokken.yml b/kitchen.dokken.yml index 5b4adc79..cb194450 100644 --- a/kitchen.dokken.yml +++ b/kitchen.dokken.yml @@ -2,6 +2,11 @@ driver: name: dokken privileged: true chef_version: <%= ENV['CHEF_VERSION'] || 'current' %> + intermediate_instructions: + - | # Need to set "IPv6_rpfilter=no" otherwise firewalld won't start inside Docker container + RUN mkdir -p /etc/firewalld && \ + echo "DefaultZone=public" > /etc/firewalld/firewalld.conf && \ + echo "IPv6_rpfilter=no" >> /etc/firewalld/firewalld.conf transport: { name: dokken } provisioner: { name: dokken } @@ -47,15 +52,18 @@ platforms: image: dokken/debian-12 pid_one_command: /bin/systemd - - name: fedora-latest - driver: - image: dokken/fedora-latest - pid_one_command: /usr/lib/systemd/systemd - - - name: opensuse-leap-15 - driver: - image: dokken/opensuse-leap-15 - pid_one_command: /usr/lib/systemd/systemd + # TODO: Fix. Resources uses dnf when it should use yum + # - name: fedora-latest + # driver: + # image: dokken/fedora-latest + # pid_one_command: /usr/lib/systemd/systemd + # intermediate_instructions: *firewalld_fix + + # # TODO: Add support for openSUSE in resource + # - name: opensuse-leap-15 + # driver: + # image: dokken/opensuse-leap-15 + # pid_one_command: /usr/lib/systemd/systemd - name: oracle-8 driver: diff --git a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb index aeadcebf..a6343013 100644 --- a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb @@ -2,6 +2,14 @@ only_if { platform?('debian') } end +# Workaround for a bug when using firewalld: +# * Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1074789 +# * Ubuntu: https://bugs.launchpad.net/ubuntu/+source/policykit-1/+bug/2054716 +user 'polkitd' do + system true + only_if { platform?('debian', 'ubuntu') } +end + firewalld 'default' firewalld_config 'set some values' do @@ -147,3 +155,18 @@ sources '192.0.2.0/24' version '1' end + +test_zone_priority = + (platform?('ubuntu') && node['platform_version'].to_f >= 24.04) || + (platform?('rocky') && node['platform_version'] >= 10) + +firewalld_zone 'zpriority1' do + priority -10 + only_if { test_zone_priority } +end + +firewalld_zone 'zpriority2' do + ingress_priority 100 + egress_priority 200 + only_if { test_zone_priority } +end From 348e80ca488a0108c323d4974d52245e4a616dfd Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Wed, 18 Dec 2024 15:46:29 -0800 Subject: [PATCH 06/24] Fixed: firewall_rule resource fails on firewalld The resource was specifying --zone when creating --direct rules, which is not allowed for direct rules in firewalld. Fixes #298 Signed-off-by: Joseph Larionov --- CHANGELOG.md | 4 ++++ libraries/provider_firewall_firewalld.rb | 12 ++++++++++-- libraries/resource_firewall_rule.rb | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 044df83d..457315cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This file is used to list changes made in each version of the firewall cookbook. ## Unreleased +### Fixed + +- Fixed: `firewall_rule` resource fails with a `--zone is an invalid option with --direct` error on firewalld. + ## 6.3.9 - *2024-12-05* ## 6.3.8 - *2024-11-18* diff --git a/libraries/provider_firewall_firewalld.rb b/libraries/provider_firewall_firewalld.rb index e75fdfff..626a2ca2 100644 --- a/libraries/provider_firewall_firewalld.rb +++ b/libraries/provider_firewall_firewalld.rb @@ -64,8 +64,16 @@ def whyrun_supported? next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) ip_versions(firewall_rule).each do |ip_version| + if firewall_rule.zone + # Bug introduced in https://github.com/sous-chefs/firewall/pull/206 + # firewalld zones are not compatible with direct rules. Need to + # migrate to firewalld rich rules in order to support configuring a + # rule in a specific zone. + Chef::Log.warn "zone \"#{firewall_rule.zone}\" on firewall_rule \"#{firewall_rule.name}\" is ignored. The zone property exists but has not been implemented internally in the firewall_rule resource." + end + # build rules to apply with weight - k = "firewall-cmd --zone=#{firewall_rule.zone} --direct --add-rule #{build_firewall_rule(firewall_rule, ip_version)}" + k = "firewall-cmd --direct --add-rule #{build_firewall_rule(firewall_rule, ip_version)}" v = firewall_rule.position # unless we're adding them for the first time.... bail out. @@ -75,7 +83,7 @@ def whyrun_supported? # If persistent rules is enabled (default) make sure we add a permanent rule at the same time perm_rules = node && node['firewall'] && node['firewall']['firewalld'] && node['firewall']['firewalld']['permanent'] if firewall_rule.permanent || perm_rules - k = "firewall-cmd --zone=#{firewall_rule.zone} --permanent --direct --add-rule #{build_firewall_rule(firewall_rule, ip_version)}" + k = "firewall-cmd --permanent --direct --add-rule #{build_firewall_rule(firewall_rule, ip_version)}" new_resource.rules['firewalld'][k] = v end end diff --git a/libraries/resource_firewall_rule.rb b/libraries/resource_firewall_rule.rb index c85deda2..18972d6d 100644 --- a/libraries/resource_firewall_rule.rb +++ b/libraries/resource_firewall_rule.rb @@ -36,7 +36,7 @@ class Resource::FirewallRule < Chef::Resource::LWRPBase # only used for firewalld attribute(:permanent, kind_of: [TrueClass, FalseClass], default: false) - attribute(:zone, kind_of: String, default: 'drop') + attribute(:zone, kind_of: String) # only used for Windows Firewalls attribute(:program, kind_of: String) From 455427e1db43fdd8a21fbe8f1c6555e849569359 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Fri, 20 Dec 2024 11:57:18 -0800 Subject: [PATCH 07/24] Ensure firewalld service remains enabled and started when installed Signed-off-by: Joseph Larionov --- CHANGELOG.md | 4 ++++ resources/firewalld.rb | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 457315cc..03bb7a17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This file is used to list changes made in each version of the firewall cookbook. ## Unreleased +### Changed + +- Ensure `firewalld` service remains enabled and started when installed. + ### Fixed - Fixed: `firewall_rule` resource fails with a `--zone is an invalid option with --direct` error on firewalld. diff --git a/resources/firewalld.rb b/resources/firewalld.rb index c805ea79..88661730 100644 --- a/resources/firewalld.rb +++ b/resources/firewalld.rb @@ -6,7 +6,14 @@ action :install do chef_gem 'ruby-dbus' require 'dbus' - package 'firewalld' + + package 'firewalld' do + action :install + end + + service 'firewalld' do + action [:enable, :start] + end end action :reload do From 461146053f93930fef770e9d55405def79f4624e Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Fri, 20 Dec 2024 14:04:03 -0800 Subject: [PATCH 08/24] Fixed: New zones are created with forwarding enabled Due to https://github.com/firewalld/firewalld/issues/1438 Signed-off-by: Joseph Larionov --- CHANGELOG.md | 1 + resources/firewalld_zone.rb | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bb7a17..f9e2f9cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This file is used to list changes made in each version of the firewall cookbook. ### Fixed - Fixed: `firewall_rule` resource fails with a `--zone is an invalid option with --direct` error on firewalld. +- Fixed: New zones created by `firewalld_zone` unexpectedly have forwarding enabled by default. ## 6.3.9 - *2024-12-05* diff --git a/resources/firewalld_zone.rb b/resources/firewalld_zone.rb index d4841721..36b72772 100644 --- a/resources/firewalld_zone.rb +++ b/resources/firewalld_zone.rb @@ -85,7 +85,10 @@ fw_config = config_interface(dbus) unless fw_config.getZoneNames.include?(new_resource.short) - fw_config.addZone2(new_resource.short, {}) + # Need to explicity disable "forward" when creating a zone via DBus + # Due to https://github.com/firewalld/firewalld/issues/1438 + zone_settings = { 'forward' => false } + fw_config.addZone2(new_resource.short, zone_settings) end zone_path = fw_config.getZoneByName(new_resource.short) zone = zone_interface(dbus, zone_path) From 60f28122ea4e1c09f713bf78692619ed398282e8 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Fri, 20 Dec 2024 16:00:10 -0800 Subject: [PATCH 09/24] Fixed: firewalld resources ignore properties whose value is false Signed-off-by: Joseph Larionov --- CHANGELOG.md | 1 + resources/firewalld_service.rb | 3 ++- resources/firewalld_zone.rb | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e2f9cd..5da18058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This file is used to list changes made in each version of the firewall cookbook. - Fixed: `firewall_rule` resource fails with a `--zone is an invalid option with --direct` error on firewalld. - Fixed: New zones created by `firewalld_zone` unexpectedly have forwarding enabled by default. +- Fixed: `firewalld_*` resources ignore properties whose value is `false`. ## 6.3.9 - *2024-12-05* diff --git a/resources/firewalld_service.rb b/resources/firewalld_service.rb index 44791259..70cfdbfb 100644 --- a/resources/firewalld_service.rb +++ b/resources/firewalld_service.rb @@ -71,8 +71,9 @@ service = service_interface(dbus, service_path) properties = new_resource.class.state_properties.map(&:name) properties.each do |property| + next unless property_is_set?(property) new_value = new_resource.send(property) - next unless new_value + if [:ports, :source_ports].include?(property) new_value = DBus.variant('a(ss)', new_value.map { |e| e.split('/') }) elsif property == :description diff --git a/resources/firewalld_zone.rb b/resources/firewalld_zone.rb index 36b72772..100a75fb 100644 --- a/resources/firewalld_zone.rb +++ b/resources/firewalld_zone.rb @@ -96,8 +96,9 @@ reload = false properties = new_resource.class.state_properties.map(&:name) properties.each do |property| + next unless property_is_set?(property) new_value = new_resource.send(property) - next unless new_value + if [:ports, :source_ports].include?(property) new_value = DBus.variant('a(ss)', new_value.map { |e| e.split('/') }) elsif [:forward_ports].include?(property) From 339737fa5c26a1c35388e04161a712607995142b Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Fri, 20 Dec 2024 16:05:57 -0800 Subject: [PATCH 10/24] Test firewalld on all compatible Linux platforms Signed-off-by: Joseph Larionov --- kitchen.yml | 11 +++++++---- .../firewalld-dbus/inspec/firewalld-dbus_spec.rb | 7 ++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/kitchen.yml b/kitchen.yml index 0b9df6d6..ca11a2de 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -107,10 +107,13 @@ suites: - recipe[nftables-test] - name: firewalld-dbus - includes: - - debian-11 - - debian-12 - - fedora-latest + excludes: + - almalinux-10 # TODO: Get working with firewalld (undefined method `ingress_priority'). + - centos-stream-10 # TODO: Get working with firewalld (undefined method `ingress_priority'). + - ubuntu-20.04 # firewalld version too old. + - ubuntu-24.04 # TODO: Get working with firewalld (undefined method `ingress_priority'). + - windows-2016 + - windows-2019 run_list: - recipe[firewalld-test] diff --git a/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb b/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb index 01cfd24e..f3ee7a38 100644 --- a/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb +++ b/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb @@ -149,13 +149,14 @@ end describe command('firewall-cmd --info-zone=home') do - ptest_config = <<~EOF + cockpit = %w(almalinux centos oracle rocky).include?(os.name) ? 'cockpit ' : '' + expected_config = <<~EOF home (active) target: default icmp-block-inversion: yes interfaces: eth0 sources:#{' '} - services: dhcpv6-client mdns samba-client ssh + services: #{cockpit}dhcpv6-client mdns samba-client ssh ports:#{' '} protocols:#{' '} forward: no @@ -165,7 +166,7 @@ icmp-blocks:#{' '} rich rules:#{' '} EOF - its(:stdout) { should cmp ptest_config } + its(:stdout) { should cmp expected_config } end describe command('firewall-cmd --info-zone=ztest') do From df13e29839e652f7a4dfb9ca99e0cd19ad5b5b15 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Fri, 20 Dec 2024 21:31:26 -0800 Subject: [PATCH 11/24] Add support for firewalld 2.0.0 The firewalld_zone resource has been updated to support priority, ingress_priority, and egress_priority zone options introduced in firewalld 2.0.0. As a result, this update extends support to RHEL 10, its derivatives, and Ubuntu 24.04, all of which utilize firewalld 2.0.0 or later. Signed-off-by: Joseph Larionov --- CHANGELOG.md | 5 ++ documentation/resources/firewalld_zone.md | 3 + kitchen.yml | 3 - libraries/helpers_firewalld_dbus.rb | 4 + resources/firewalld_zone.rb | 20 +++++ .../firewalld-test/recipes/default.rb | 2 +- .../inspec/firewalld-dbus_spec.rb | 77 +++++++++++++++++-- 7 files changed, 103 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da18058..a8735242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ This file is used to list changes made in each version of the firewall cookbook. ## Unreleased +### Added + +- Support for firewalld 2.0.0 and the platforms that use it; RHEL 10 and Ubuntu 24.04. + - `priority`, `ingress_priority`, `egress_priority` properties added to `firewalld_zone`. + ### Changed - Ensure `firewalld` service remains enabled and started when installed. diff --git a/documentation/resources/firewalld_zone.md b/documentation/resources/firewalld_zone.md index 05e93846..3f0ec110 100644 --- a/documentation/resources/firewalld_zone.md +++ b/documentation/resources/firewalld_zone.md @@ -15,13 +15,16 @@ | Name | Type | Default | Description | | ----------- | ------------- | ------- | ------------------------------------ | | `description` | `String` | | see description tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | +| `egress_priority` | `Integer` | | set the zone priority for egress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See for more information. | | `forward` | `[true, false]` | | see forward tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `forward_ports` | `[Array, String]` | | array of (port, protocol, to-port, to-addr). See forward-port tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `icmp_block_inversion` | `[true, false]` | | see icmp-block-inversion tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `icmp_blocks` | `[Array, String]` | | array of icmp-blocks. See icmp-block tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | +| `ingress_priority` | `Integer` | | set the zone priority for ingress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See for more information. | | `interfaces` | `[Array, String]` | | array of interfaces. See interface tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `masquerade` | `[true, false]` | | see masquerade tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `ports` | `[Array, String]` | | array of port and protocol pairs. See port tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | +| `priority` | `Integer` | | set the zone priority for both ingress and egress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See https://firewalld.org/2023/04/zone-priorities for more information. | | `protocols` | `[Array, String]` | | array of protocols, see protocol tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `rules_str` | `[Array, String]` | | array of rich-language rules. See rule tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `services` | `[Array, String]` | | array of service names, see service tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | diff --git a/kitchen.yml b/kitchen.yml index ca11a2de..8a0d2543 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -108,10 +108,7 @@ suites: - name: firewalld-dbus excludes: - - almalinux-10 # TODO: Get working with firewalld (undefined method `ingress_priority'). - - centos-stream-10 # TODO: Get working with firewalld (undefined method `ingress_priority'). - ubuntu-20.04 # firewalld version too old. - - ubuntu-24.04 # TODO: Get working with firewalld (undefined method `ingress_priority'). - windows-2016 - windows-2019 run_list: diff --git a/libraries/helpers_firewalld_dbus.rb b/libraries/helpers_firewalld_dbus.rb index c78b9049..c656cd97 100644 --- a/libraries/helpers_firewalld_dbus.rb +++ b/libraries/helpers_firewalld_dbus.rb @@ -21,6 +21,10 @@ def config_interface(system_bus) config_object(system_bus)['org.fedoraproject.FirewallD1.config'] end + def get_firewalld_version(system_bus) + firewalld_interface(system_bus)['version'] + end + def icmptype_interface(dbus, icmptype_path) icmptype_object = firewalld(dbus)[icmptype_path] icmptype_object['org.fedoraproject.FirewallD1.config.icmptype'] diff --git a/resources/firewalld_zone.rb b/resources/firewalld_zone.rb index 100a75fb..3a232c1d 100644 --- a/resources/firewalld_zone.rb +++ b/resources/firewalld_zone.rb @@ -6,6 +6,9 @@ property :description, String, description: 'see description tag in firewalld.zone(5).' +property :egress_priority, + [Integer], + description: 'set the zone priority for egress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See https://firewalld.org/2023/04/zone-priorities for more information.' property :forward, [true, false], description: 'see forward tag in firewalld.zone(5).' @@ -20,6 +23,9 @@ [Array, String], description: 'array of icmp-blocks. See icmp-block tag in firewalld.zone(5).', coerce: proc { |o| Array(o) } +property :ingress_priority, + [Integer], + description: 'set the zone priority for ingress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See https://firewalld.org/2023/04/zone-priorities for more information.' property :interfaces, [Array, String], description: 'array of interfaces. See interface tag in firewalld.zone(5).', @@ -31,6 +37,9 @@ [Array, String], description: 'array of port and protocol pairs. See port tag in firewalld.zone(5).', coerce: proc { |o| Array(o) } +property :priority, + [Integer], + description: 'set the zone priority for both ingress and egress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See https://firewalld.org/2023/04/zone-priorities for more information.' property :protocols, [Array, String], description: 'array of protocols, see protocol tag in firewalld.zone(5).', @@ -93,9 +102,20 @@ zone_path = fw_config.getZoneByName(new_resource.short) zone = zone_interface(dbus, zone_path) + if property_is_set?(:priority) && (property_is_set?(:ingress_priority) || property_is_set?(:egress_priority)) + raise 'The "priority" property cannot be used together with "ingress_priority" or "egress_priority". ' \ + 'You may either specify "priority" alone or one/both of "ingress_priority" and "egress_priority", ' \ + 'but not both types together.' + end + reload = false properties = new_resource.class.state_properties.map(&:name) properties.each do |property| + if [:ingress_priority, :egress_priority].include?(property) && property_is_set?(:priority) + new_resource.send("#{property}=", new_resource.priority) + end + next if property == :priority # Shorthand property that sets :ingress_priority and :egress_priority to the same value + next unless property_is_set?(property) new_value = new_resource.send(property) diff --git a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb index a6343013..8b1f3f03 100644 --- a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb @@ -161,7 +161,7 @@ (platform?('rocky') && node['platform_version'] >= 10) firewalld_zone 'zpriority1' do - priority -10 + priority(-10) only_if { test_zone_priority } end diff --git a/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb b/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb index f3ee7a38..e718993b 100644 --- a/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb +++ b/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb @@ -77,9 +77,15 @@ its(:stdout) { should cmp single_ip_config } end +firewalld_version = command('firewall-cmd --version').stdout.strip +supports_zone_priority = Gem::Version.new(firewalld_version) >= Gem::Version.new('2.0.0') + +# https://github.com/firewalld/firewalld/commit/b2024038623c94784321053dc766bf961c3db79d +supports_runtime_active_only = Gem::Version.new(firewalld_version) >= Gem::Version.new('2.0.1') + describe command('firewall-cmd --info-policy=ptest') do ptest_config = <<~EOF - ptest (active) + ptest#{' (active)' unless supports_runtime_active_only} priority: 10 target: ACCEPT ingress-zones: home @@ -101,7 +107,7 @@ describe command('firewall-cmd --info-policy=pminimal') do pminimal = <<~EOF - pminimal (active) + pminimal#{' (active)' unless supports_runtime_active_only} priority: -1 target: CONTINUE ingress-zones: internal @@ -148,11 +154,14 @@ its(:stdout) { should cmp ssh2_config } end +cockpit = %w(almalinux centos oracle rocky).include?(os.name) ? 'cockpit ' : '' + describe command('firewall-cmd --info-zone=home') do - cockpit = %w(almalinux centos oracle rocky).include?(os.name) ? 'cockpit ' : '' - expected_config = <<~EOF - home (active) + expected_config = <<~EOF.gsub(/(\n[ ]{2}){2,}/, "\n ") + home (#{'default, ' if supports_runtime_active_only}active) target: default + #{'ingress-priority: 0' if supports_zone_priority} + #{'egress-priority: 0' if supports_zone_priority} icmp-block-inversion: yes interfaces: eth0 sources:#{' '} @@ -170,9 +179,11 @@ end describe command('firewall-cmd --info-zone=ztest') do - ptest_config = <<~EOF + ptest_config = <<~EOF.gsub(/(\n[ ]{2}){2,}/, "\n ") ztest (active) target: ACCEPT + #{'ingress-priority: 0' if supports_zone_priority} + #{'egress-priority: 0' if supports_zone_priority} icmp-block-inversion: yes interfaces: eth1337 eth2337 sources: 192.0.2.2 @@ -192,9 +203,11 @@ end describe command('firewall-cmd --info-zone=ztest2') do - ptest_config = <<~EOF + ptest_config = <<~EOF.gsub(/(\n[ ]{2}){2,}/, "\n ") ztest2 (active) target: default + #{'ingress-priority: 0' if supports_zone_priority} + #{'egress-priority: 0' if supports_zone_priority} icmp-block-inversion: no interfaces:#{' '} sources: 192.0.2.0/24 @@ -211,6 +224,56 @@ its(:stdout) { should cmp ptest_config } end +test_zone_priority = + (os.name == 'ubuntu' && os.release.to_f >= 24.04) || + (os.name == 'rocky' && os.release.to_i >= 10) + +if test_zone_priority + describe command('firewall-cmd --info-zone=zpriority1') do + ptest_config = <<~EOF + zpriority1 + target: default + ingress-priority: -10 + egress-priority: -10 + icmp-block-inversion: no + interfaces:#{' '} + sources:#{' '} + services:#{' '} + ports:#{' '} + protocols:#{' '} + forward: no + masquerade: no + forward-ports:#{' '} + source-ports:#{' '} + icmp-blocks:#{' '} + rich rules:#{' '} + EOF + its(:stdout) { should cmp ptest_config } + end + + describe command('firewall-cmd --info-zone=zpriority2') do + ptest_config = <<~EOF + zpriority2 + target: default + ingress-priority: 100 + egress-priority: 200 + icmp-block-inversion: no + interfaces:#{' '} + sources:#{' '} + services:#{' '} + ports:#{' '} + protocols:#{' '} + forward: no + masquerade: no + forward-ports:#{' '} + source-ports:#{' '} + icmp-blocks:#{' '} + rich rules:#{' '} + EOF + its(:stdout) { should cmp ptest_config } + end +end + describe service('firewalld') do it { should be_installed } it { should be_enabled } From 750fd4c4594c52bfebae77e4df03615c951db5fe Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Mon, 23 Dec 2024 10:47:24 -0800 Subject: [PATCH 12/24] Fixed ufw test when running in kitchen-dokken Signed-off-by: Joseph Larionov --- test/fixtures/cookbooks/firewall-test/recipes/default.rb | 2 +- test/integration/ufw/inspec/ufw_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/cookbooks/firewall-test/recipes/default.rb b/test/fixtures/cookbooks/firewall-test/recipes/default.rb index f6183f5a..23744770 100644 --- a/test/fixtures/cookbooks/firewall-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewall-test/recipes/default.rb @@ -76,7 +76,7 @@ # if using with iptables-restart, this produces an unreadable line; no problem, IF disabled firewall_rule 'ufw raw test' do - raw 'limit 23/tcp' + raw 'allow from 192.168.1.1 to 192.168.2.1 port 25 proto tcp' only_if { platform_family?('debian') && !node['firewall']['ubuntu_iptables'] } end diff --git a/test/integration/ufw/inspec/ufw_spec.rb b/test/integration/ufw/inspec/ufw_spec.rb index ef5e847a..2a09a343 100644 --- a/test/integration/ufw/inspec/ufw_spec.rb +++ b/test/integration/ufw/inspec/ufw_spec.rb @@ -9,7 +9,7 @@ %r{ 80/tcp + ALLOW IN +2001:db8::ff00:42:8329}, %r{ 1000:1100/tcp + ALLOW IN +Anywhere}, %r{ 1234,5000:5100,5678/tcp + ALLOW IN +Anywhere}, - %r{ 23/tcp + LIMIT IN +Anywhere}, + %r{ 192.168.2.1 25/tcp + ALLOW IN + 192.168.1.1}, /# ssh22/, ] From 6c23c16d1e2ba962c9a70c4b1dc320b02b972e7d Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Mon, 23 Dec 2024 12:58:19 -0800 Subject: [PATCH 13/24] Disable Oracle 9 iptables test, its iptables package fails to install Signed-off-by: Joseph Larionov --- kitchen.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/kitchen.yml b/kitchen.yml index 8a0d2543..f9681f3e 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -86,6 +86,7 @@ suites: - name: iptables excludes: + - oracle-9 # iptables fails to install from Oracle repo - opensuse-leap-15 - windows-2016 - windows-2019 From 6243de36c1591a558be97a411b5754589b28bec1 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Mon, 23 Dec 2024 17:14:53 -0800 Subject: [PATCH 14/24] Fixed: firewalld resources were not idempotent Signed-off-by: Joseph Larionov --- CHANGELOG.md | 1 + documentation/resources/firewalld_helpers.md | 2 +- documentation/resources/firewalld_policy.md | 2 +- documentation/resources/firewalld_service.md | 4 +-- documentation/resources/firewalld_zone.md | 4 +-- kitchen.yml | 3 ++ resources/firewalld_helpers.rb | 5 +-- resources/firewalld_policy.rb | 12 ++++++- resources/firewalld_service.rb | 6 ++-- resources/firewalld_zone.rb | 13 ++++++-- .../firewalld-test/recipes/default.rb | 32 +++---------------- 11 files changed, 43 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8735242..6c8e318f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This file is used to list changes made in each version of the firewall cookbook. - Fixed: `firewall_rule` resource fails with a `--zone is an invalid option with --direct` error on firewalld. - Fixed: New zones created by `firewalld_zone` unexpectedly have forwarding enabled by default. - Fixed: `firewalld_*` resources ignore properties whose value is `false`. +- Fixed: `firewalld_*` resources were not idempotent when using `ports`, `source_ports`, and `rich_rules` properties. ## 6.3.9 - *2024-12-05* diff --git a/documentation/resources/firewalld_helpers.md b/documentation/resources/firewalld_helpers.md index 2a97793f..4f22ed75 100644 --- a/documentation/resources/firewalld_helpers.md +++ b/documentation/resources/firewalld_helpers.md @@ -19,7 +19,7 @@ |`description` ||String | |see description tag in [firewalld.helper(5)](https://firewalld.org/documentation/man-pages/firewalld.helper.html). | | |`family` ||String | `'ipv4'` |see family tag in [firewalld.helper(5)](https://firewalld.org/documentation/man-pages/firewalld.helper.html). | `'ipv4'`, `'ipv6'` | |`nf_module` ||String | |see module tag in [firewalld.helper(5)](https://firewalld.org/documentation/man-pages/firewalld.helper.html). | | -|`ports` ||Array, String | |array of port and protocol pairs. See port tag in [firewalld.helper(5)](https://firewalld.org/documentation/man-pages/firewalld.helper.html).| | +|`ports` ||Array, String | | array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in [firewalld.helper(5)](https://firewalld.org/documentation/man-pages/firewalld.helper.html).| | ## Examples diff --git a/documentation/resources/firewalld_policy.md b/documentation/resources/firewalld_policy.md index e0753155..bd1292b5 100644 --- a/documentation/resources/firewalld_policy.md +++ b/documentation/resources/firewalld_policy.md @@ -20,7 +20,7 @@ | `icmp_blocks` | | `[Array, String]` | | array of icmp-blocks. See icmp-block tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | | `ingress_zones` | | `[Array, String]` | | array of zone names. See ingress-zone tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | | `masquerade` | | `[true, false]` | | see masquerade tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | -| `ports` | | `[Array, String]` | | array of port and protocol pairs. See port tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | +| `ports` | | `[Array, String]` | | array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | | `priority` | | `Integer` | | see priority tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | | `protocols` | | `[Array, String]` | | array of protocols, see protocol tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | | `rich_rules` | | `[Array, String]` | | array of rich-language rules. See rule tag in [firewalld.policy(5)](https://firewalld.org/documentation/man-pages/firewalld.policy.html). | | diff --git a/documentation/resources/firewalld_service.md b/documentation/resources/firewalld_service.md index d0f1e32f..d9185dd4 100644 --- a/documentation/resources/firewalld_service.md +++ b/documentation/resources/firewalld_service.md @@ -17,11 +17,11 @@ |`version` ||String | |see version attribute of service tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | |`short` |✓|String | |see short tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html). | | |`description` ||String | |see description tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html). | | -|`ports` ||Array, String | |array of port and protocol pairs. See port tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | +|`ports` ||Array, String | |array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | |`module_names` ||Array, String | |array of kernel netfilter helpers, see module tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | |`destination` ||Hash | |hash of {IP family : IP address} where 'IP family' key can be either 'ipv4' or 'ipv6'. See destination tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | |`protocols` ||Array, String | |array of protocols, see protocol tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | -|`source_ports` ||Array, String | |array of port and protocol pairs. See source-port tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | +|`source_ports` ||Array, String | |array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See source-port tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | |`includes` ||Array, String | |array of service includes, see include tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | |`helpers` ||Array, String | |array of service helpers, see helper tag in [firewalld.service(5)](https://firewalld.org/documentation/man-pages/firewalld.service.html).| | diff --git a/documentation/resources/firewalld_zone.md b/documentation/resources/firewalld_zone.md index 3f0ec110..e157048b 100644 --- a/documentation/resources/firewalld_zone.md +++ b/documentation/resources/firewalld_zone.md @@ -23,13 +23,13 @@ | `ingress_priority` | `Integer` | | set the zone priority for ingress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See for more information. | | `interfaces` | `[Array, String]` | | array of interfaces. See interface tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `masquerade` | `[true, false]` | | see masquerade tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | -| `ports` | `[Array, String]` | | array of port and protocol pairs. See port tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | +| `ports` | `[Array, String]` | | array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `priority` | `Integer` | | set the zone priority for both ingress and egress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See https://firewalld.org/2023/04/zone-priorities for more information. | | `protocols` | `[Array, String]` | | array of protocols, see protocol tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `rules_str` | `[Array, String]` | | array of rich-language rules. See rule tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `services` | `[Array, String]` | | array of service names, see service tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `short` | `String` | | see short tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | -| `source_ports` | `[Array, String]` | | array of port and protocol pairs. See source-port tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | +| `source_ports` | `[Array, String]` | | array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See source-port tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `sources` | `[Array, String]` | | array of source addresses. See source tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `target` | `String` | | see target attribute of zone tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `version` | `String` | | see version attribute of zone tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | diff --git a/kitchen.yml b/kitchen.yml index f9681f3e..2da2f8fa 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -114,6 +114,9 @@ suites: - windows-2019 run_list: - recipe[firewalld-test] + provisioner: + enforce_idempotency: true + multiple_converge: 2 - name: windows includes: diff --git a/resources/firewalld_helpers.rb b/resources/firewalld_helpers.rb index 44fdeb90..bdfc35d3 100644 --- a/resources/firewalld_helpers.rb +++ b/resources/firewalld_helpers.rb @@ -25,7 +25,7 @@ property :ports, [Array, String], default: [], - description: 'array of port and protocol pairs. See port tag in firewalld.helper(5).', + description: 'array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in firewalld.helper(5).', coerce: proc { |o| Array(o) } load_current_value do |new_resource| @@ -43,7 +43,8 @@ description settings[2] family settings[3] nf_module settings[4] - ports settings[5] + # Load the current value of ports in the same format as the resource property to make it idempotent + ports settings[5].map { |port, protocol| "#{port}/#{protocol}" } else Chef::Log.info "Helper #{new_resource.short} does not exist. Will be created." end diff --git a/resources/firewalld_policy.rb b/resources/firewalld_policy.rb index 985d29c7..8b5fb731 100644 --- a/resources/firewalld_policy.rb +++ b/resources/firewalld_policy.rb @@ -26,7 +26,7 @@ description: 'see masquerade tag in firewalld.policy(5).' property :ports, [Array, String], - description: 'array of port and protocol pairs. See port tag in firewalld.policy(5).', + description: 'array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in firewalld.policy(5).', coerce: proc { |o| Array(o) } property :priority, Integer, @@ -68,6 +68,8 @@ object = firewalld_service[policy_path] config_policy = object['org.fedoraproject.FirewallD1.config.policy'] config_policy.getSettings.each do |k, v| + # Load the current value of ports in the same format as the resource property to make it idempotent + v = v.map { |port, protocol| "#{port}/#{protocol}" } if %w(ports source_ports).include?(k) send(k, v) end else @@ -90,6 +92,14 @@ properties.each do |property| new_value = new_resource.send(property) next if new_value.nil? + + if property == :rich_rules + # quote the values in the rich rule just like firewalld does so it's idempotent + # 'rule family=ipv4 source address=192.168.0.14 accept' ➔ 'rule family="ipv4" source address="192.168.0.14" accept' + new_value = new_value.map { |rule| rule.gsub(/(\b\w+=)([^"\s]+)/, '\1"\2"') } + new_resource.rich_rules = new_value + end + if [:ports, :source_ports].include?(property) new_value = DBus.variant('a(ss)', new_value.map { |e| e.split('/') }) elsif [:forward_ports].include?(property) diff --git a/resources/firewalld_service.rb b/resources/firewalld_service.rb index 70cfdbfb..55901de9 100644 --- a/resources/firewalld_service.rb +++ b/resources/firewalld_service.rb @@ -15,7 +15,7 @@ description: 'see description tag in firewalld.service(5).' property :ports, [Array, String], - description: 'array of port and protocol pairs. See port tag in firewalld.service(5).', + description: 'array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in firewalld.service(5).', coerce: proc { |o| Array(o) } property :module_names, [Array, String], @@ -30,7 +30,7 @@ coerce: proc { |o| Array(o) } property :source_ports, [Array, String], - description: 'array of port and protocol pairs. See source-port tag in firewalld.service(5).', + description: 'array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See source-port tag in firewalld.service(5).', coerce: proc { |o| Array(o) } property :includes, [Array, String], @@ -51,6 +51,8 @@ object = firewalld_service[service_path] config_service = object['org.fedoraproject.FirewallD1.config.service'] config_service.getSettings2.each do |k, v| + # Load the current value of ports in the same format as the resource property to make it idempotent + v = v.map { |port, protocol| "#{port}/#{protocol}" } if %w(ports source_ports).include?(k) send(k, v) end else diff --git a/resources/firewalld_zone.rb b/resources/firewalld_zone.rb index 3a232c1d..7b52ea60 100644 --- a/resources/firewalld_zone.rb +++ b/resources/firewalld_zone.rb @@ -35,7 +35,7 @@ description: 'see masquerade tag in firewalld.zone(5).' property :ports, [Array, String], - description: 'array of port and protocol pairs. See port tag in firewalld.zone(5).', + description: 'array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in firewalld.zone(5).', coerce: proc { |o| Array(o) } property :priority, [Integer], @@ -58,7 +58,7 @@ description: 'see short tag in firewalld.zone(5).' property :source_ports, [Array, String], - description: 'array of port and protocol pairs. See source-port tag in firewalld.zone(5).', + description: 'array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See source-port tag in firewalld.zone(5).', coerce: proc { |o| Array(o) } property :sources, [Array, String], @@ -81,6 +81,8 @@ object = firewalld_service[zone_path] config_zone = object['org.fedoraproject.FirewallD1.config.zone'] config_zone.getSettings2.each do |k, v| + # Load the current value of ports in the same format as the resource property to make it idempotent + v = v.map { |port, protocol| "#{port}/#{protocol}" } if %w(ports source_ports).include?(k) send(k, v) end else @@ -119,6 +121,13 @@ next unless property_is_set?(property) new_value = new_resource.send(property) + if property == :rules_str + # quote the values in the rich rule just like firewalld does so it's idempotent + # 'rule family=ipv4 source address=192.168.0.14 accept' ➔ 'rule family="ipv4" source address="192.168.0.14" accept' + new_value = new_value.map { |rule| rule.gsub(/(\b\w+=)([^"\s]+)/, '\1"\2"') } + new_resource.rules_str = new_value + end + if [:ports, :source_ports].include?(property) new_value = DBus.variant('a(ss)', new_value.map { |e| e.split('/') }) elsif [:forward_ports].include?(property) diff --git a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb index 8b1f3f03..34347b71 100644 --- a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb @@ -27,33 +27,23 @@ firewalld_helper 'minimal-helper' do nf_module 'nf_conntrack_netbios_ns' - ports '7777/udp' -end - -firewalld_helper 'change-minimal-helper' do - short 'minimal-helper' ports '7778/udp' end -firewalld_icmptype 'rick-rolled' do - version '1' - description 'never gonna give you up' - destinations %w(ipv4 ipv6) -end - firewalld_icmptype 'change-rick-rolled' do short 'rick-rolled' + description 'never gonna give you up' + version '1' destinations 'ipv4' end -firewalld_icmptype 'minimal-icmptype' - firewalld_icmptype 'change-minimal-icmptype' do short 'minimal-icmptype' destinations %w(ipv4 ipv6) end -firewalld_ipset 'example-ips' do +firewalld_ipset 'change-example-ips' do + short 'example-ips' version '1' description 'some ips as an example ipset' type 'hash:ip' @@ -67,11 +57,6 @@ entries ['192.0.2.16', '192.0.2.32'] end -firewalld_ipset 'change-example-ips' do - short 'example-ips' - entries ['192.0.2.16', '192.0.2.32'] -end - firewalld_ipset 'single-ip' do entries '192.0.2.22' end @@ -98,11 +83,6 @@ firewalld_policy 'pminimal' do egress_zones 'external' ingress_zones 'internal' - masquerade true -end - -firewalld_policy 'change-pminimal' do - short 'pminimal' masquerade false end @@ -118,10 +98,6 @@ helpers 'tftp' end -firewalld_service 'minimal-service' do - ports '2/tcp' -end - firewalld_service 'change-minimal-service' do short 'minimal-service' ports '1/udp' From c7e353b357ce689eee247544eed4af42b219ac5f Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Tue, 24 Dec 2024 14:42:12 -0800 Subject: [PATCH 15/24] Add firewalld_rich_rule resource Signed-off-by: Joseph Larionov --- CHANGELOG.md | 1 + documentation/README.md | 1 + .../resources/firewalld_rich_rule.md | 55 +++ libraries/helpers_firewalld_dbus.rb | 8 + resources/firewalld_config.rb | 10 +- resources/firewalld_rich_rule.rb | 313 ++++++++++++++++++ .../firewalld-test/recipes/default.rb | 2 + .../firewalld-test/recipes/rich_rules.rb | 171 ++++++++++ .../inspec/firewalld-dbus_spec.rb | 44 ++- 9 files changed, 586 insertions(+), 19 deletions(-) create mode 100644 documentation/resources/firewalld_rich_rule.md create mode 100644 resources/firewalld_rich_rule.rb create mode 100644 test/fixtures/cookbooks/firewalld-test/recipes/rich_rules.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8e318f..bab0c481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This file is used to list changes made in each version of the firewall cookbook. - Support for firewalld 2.0.0 and the platforms that use it; RHEL 10 and Ubuntu 24.04. - `priority`, `ingress_priority`, `egress_priority` properties added to `firewalld_zone`. +- Added `firewalld_rich_rule` resource for adding/removing rich rules to/from firewalld zones. ### Changed diff --git a/documentation/README.md b/documentation/README.md index 6f295d3b..c7ff31e3 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -10,5 +10,6 @@ - [firewalld_icmptype](resources/firewalld_icmptype.md) - [firewalld_ipset](resources/firewalld_ipset.md) - [firewalld_policy](resources/firewalld_policy.md) +- [firewalld_rich_rule](resources/firewalld_rich_rule.md) - [firewalld_service](resources/firewalld_service.md) - [firewalld_zone](resources/firewalld_zone.md) diff --git a/documentation/resources/firewalld_rich_rule.md b/documentation/resources/firewalld_rich_rule.md new file mode 100644 index 00000000..1bd363aa --- /dev/null +++ b/documentation/resources/firewalld_rich_rule.md @@ -0,0 +1,55 @@ +# firewalld_rich_rule + +[Back to resource list](../README.md#resources) + +## Provides + +- `firewalld_rich_rule` + +## Actions + +- `:add` +- `:remove` + +## Properties + +| Name | Type | Default | Description | +|--------------------|---------------------------------------------------------------------|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `family` | `[:ipv4, :ipv6]` | `:ipv4` | IP family for the rule, either IPv4 or IPv6. If the family is not provided, the rule will be added for both IPv4 and IPv6. If source or destination addresses are used in the rule, then the rule family defaults to the type of address given in source and/or destination. This is also the case for port/packet forwarding. See rule "family" option in firewalld.richlanguage(5). | +| `zone` | `String` | | Zone in which to apply the rule. If not specified, the rule will be applied to the systems's default zone. | +| `priority` | `Integer` | | Specifies the priority of the rule, ranging from -32768 to 32767. Lower values have higher precedence. See rule option "priority" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `source` | `String` | | Source address or network range in CIDR notation to match for the rule. See "Source" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `source_not` | `String` | | Excludes a specific source address or network range in CIDR notation from matching the rule. See "Source" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `destination` | `String` | | Destination address or network range in CIDR notation to match for the rule. See "Destination" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `destination_not` | `String` | | Excludes a specific destination address or network range in CIDR notation from matching the rule. See "Destination" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `service` | `String` | | Service to apply the rule to, using firewalld's predefined service names. See \"Service\" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `port` | `[Integer, String]` | | Single port or a range of ports to match for the rule. Requires the `protocol` property to also be specified. See "Port" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `protocol` | `String` | | Defines the protocol by name or ID (e.g. tcp, udp, icmp). See "Protocol" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `tcp_mss_clamp` | `[String, Integer]` | | Sets the maximum segment size (MSS) for TCP connections. See "Tcp-Mss-Clamp" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `icmp_block` | `String` | | Blocks specific ICMP types (e.g. "echo-request") as defined by firewalld. A `rule_action` is not allowed with this property, "reject" is used implicitly. See "ICMP-Block" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `masquerade` | `[true, false]` | `false` | Enables masquerading (NAT) for the rule. A `rule_action` is not allowed with this property, IP forwarding will be implicitly enabled. See "Masquerade" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `icmp_type` | `String` | | Specifies the ICMP type to match (e.g. "echo-request"). See "ICMP-Type" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `forward_port` | `[Integer, String]` | | Forward a local port or a range of local ports to another port locally or to another machine. Requires the `protocol` property and one or both of `to_port`/`to_address` properties. A `rule_action` is not allowed with this property, uses the action "accept" internally. See "Forward-Port" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `to_port` | `Integer` | | The port to forward traffic to. See "Forward-Port" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `to_address` | `String` | | The IP address to forward traffic to. See "Forward-Port" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `source_port` | `[Integer, String]` | | The source port or range of source ports to match for the rule. Requires the `protocol` property to also be specified. See "Source-Port" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `log` | `[true, false]` | `false` | Enables logging for the rule. See "Log" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `log_prefix` | `String` | | Adds a prefix to log messages for identification. Maximum length is 127 characters. See "Log" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `log_level` | `[:emerg, :alert, :crit, :error, :warning, :notice, :info, :debug]` | `:warning` | Log level can be one of `:emerg`, `:alert`, `:crit`, `:error`, `:warning`, `:notice`, `:info`, `:debug`. Default is `:warning`. See "Log" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `log_limit` | `String` | | Limits the rate of log messages in the format "rate/duration" (e.g., `5/s`). See "Limit" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `nflog` | `[true, false]` | `false` | Log new connection attempts using kernel logging to pass the packets through a "netlink" socket to users or applications monitoring the multicast "group". See "NFLog" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `nflog_group` | `Integer` | | The NFLOG group ID for the rule (0-65535). See NETLINK_NETFILTER in netlink(7) man page and NFLOG in both iptables-extensions(8) and nft(8) man pages for a more detailed description. | +| `nflog_prefix` | `String` | | Adds a prefix to NFLog messages for identification. Maximum length is 127 characters. See "NFLog" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `nflog_queue_size` | `Integer` | | Sets the queue size for NFLog messages. See "NFLog" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `nflog_limit` | `String` | | Limits the rate of NFLog messages in the format "rate/duration" (e.g. `10/m`). See "Limit" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `audit` | `[true, false]` | `false` | Audit provides an alternative way for logging using audit records sent to the service auditd. Audit type will be discovered from the rule action automatically. See "Audit" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `audit_limit` | `String` | | Limits the rate of audit messages in the format "rate/duration" (e.g. `1/s`). See "Limit" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `rule_action` | `[:accept, :reject, :drop, :mark]` | | The action for the rule. One of `:accept`, `:reject`, `:drop`, `:mark`. See "Action" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `reject_type` | `String` | | Optional reject type to use with the `:reject` rule_action. See "Action" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `mark_set` | `String` | | The mark value to set for matched packets (e.g. "0x1"). See "Action" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `action_limit` | `String` | | Specifies a general rate limit for the rule in the format "rate/duration" (e.g. `20/s`). See "Limit" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | +| `raw` | `String` | | A raw rule string (e.g. \"rule ...\") passed directly to firewalld, bypassing this resource's internal validation and handling. All other properties are ignored. See \"Rule\" in [firewalld.richlanguage(5)](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). | + +## Examples + +See the [recipe used for testing](../../test/fixtures/cookbooks/firewalld-test/recipes/default.rb). diff --git a/libraries/helpers_firewalld_dbus.rb b/libraries/helpers_firewalld_dbus.rb index c656cd97..4c18bb60 100644 --- a/libraries/helpers_firewalld_dbus.rb +++ b/libraries/helpers_firewalld_dbus.rb @@ -21,10 +21,18 @@ def config_interface(system_bus) config_object(system_bus)['org.fedoraproject.FirewallD1.config'] end + def get_default_zone(system_bus) + firewalld_interface(system_bus).getDefaultZone + end + def get_firewalld_version(system_bus) firewalld_interface(system_bus)['version'] end + def get_log_denied(system_bus) + firewalld_interface(system_bus).getLogDenied + end + def icmptype_interface(dbus, icmptype_path) icmptype_object = firewalld(dbus)[icmptype_path] icmptype_object['org.fedoraproject.FirewallD1.config.icmptype'] diff --git a/resources/firewalld_config.rb b/resources/firewalld_config.rb index 4860a42c..aeb29f47 100644 --- a/resources/firewalld_config.rb +++ b/resources/firewalld_config.rb @@ -11,14 +11,12 @@ equal_to: %w(all unicast broadcast multicast off), description: 'Set LogDenied value to value. If LogDenied is enabled, then logging rules are added right before reject and drop rules in the INPUT, FORWARD and OUTPUT chains for the default rules and also final reject and drop rules in zones.' +include FirewallCookbook::Helpers::FirewalldDBus + load_current_value do |_new_resource| sysbus = DBus.system_bus - firewalld_service = sysbus['org.fedoraproject.FirewallD1'] - firewalld_object = firewalld_service['/org/fedoraproject/FirewallD1'] - interface = firewalld_object['org.fedoraproject.FirewallD1'] - - default_zone interface.getDefaultZone - log_denied interface.getLogDenied + default_zone get_default_zone(sysbus) + log_denied get_log_denied(sysbus) end action :update do diff --git a/resources/firewalld_rich_rule.rb b/resources/firewalld_rich_rule.rb new file mode 100644 index 00000000..550321a8 --- /dev/null +++ b/resources/firewalld_rich_rule.rb @@ -0,0 +1,313 @@ + +require 'ipaddr' + +unified_mode true + +provides :firewalld_rich_rule, os: 'linux' + +default_action :add + +property :family, + Symbol, + equal_to: [:ipv4, :ipv6], + description: 'IP family for the rule, either IPv4 or IPv6. If the family is not provided, the rule will be added for both IPv4 and IPv6. If source or destination addresses are used in the rule, then the rule family defaults to the type of address given in source and/or destination. This is also the case for port/packet forwarding. See rule "family" option in firewalld.richlanguage(5).' + +property :zone, + String, + description: "Zone in which to apply the rule. If not specified, the rule will be applied to the systems's default zone." + +property :priority, + Integer, + description: 'Specifies the priority of the rule, ranging from -32768 to 32767. Lower values have higher precedence. See rule option "priority" in firewalld.richlanguage(5).' + +property :source, + String, + callbacks: { 'must be a valid IP address or network range in CIDR notation' => ->(ip) { !!IPAddr.new(ip) } }, + description: 'Source address or network range in CIDR notation to match for the rule. See "Source" in firewalld.richlanguage(5).' + +property :source_not, + String, + callbacks: { 'must be a valid IP address or network range in CIDR notation' => ->(ip) { !!IPAddr.new(ip) } }, + description: 'Excludes a specific source address or network range in CIDR notation from matching the rule. See "Source" in firewalld.richlanguage(5).' + +property :destination, + String, + callbacks: { 'must be a valid IP address or network range in CIDR notation' => ->(ip) { !!IPAddr.new(ip) } }, + description: 'Destination address or network range in CIDR notation to match for the rule. See "Destination" in firewalld.richlanguage(5).' + +property :destination_not, + String, + callbacks: { 'must be a valid IP address or range in CIDR notation' => ->(ip) { !!IPAddr.new(ip) } }, + description: 'Excludes a specific destination address or network range in CIDR notation from matching the rule. See "Destination" in firewalld.richlanguage(5).' + +property :service, + String, + description: "Service to apply the rule to, using firewalld's predefined service names. See \"Service\" in firewalld.richlanguage(5)." + +property :port, + [Integer, String], + description: 'Single port or a range of ports to match for the rule. Requires the `protocol` property to also be specified. See "Port" in firewalld.richlanguage(5).' + +property :protocol, + String, + description: 'Defines the protocol by name or ID (e.g. tcp, udp, icmp). See "Protocol" in firewalld.richlanguage(5).' + +property :tcp_mss_clamp, + [String, Integer], + description: 'Sets the maximum segment size (MSS) for TCP connections. See "Tcp-Mss-Clamp" in firewalld.richlanguage(5).' + +property :icmp_block, + String, + description: 'Blocks specific ICMP types (e.g. "echo-request") as defined by firewalld. A `rule_action` is not allowed with this property, "reject" is used implicitly. See "ICMP-Block" in firewalld.richlanguage(5).' + +property :masquerade, + [true, false], + default: false, + description: 'Enables masquerading (NAT) for the rule. A `rule_action` is not allowed with this property, IP forwarding will be implicitly enabled. See "Masquerade" in firewalld.richlanguage(5).' + +property :icmp_type, + String, + description: 'Specifies the ICMP type to match (e.g. "echo-request"). See "ICMP-Type" in firewalld.richlanguage(5).' + +property :forward_port, + [Integer, String], + description: 'Forward a local port or a range of local ports to another port locally or to another machine. Requires the `protocol` property and one or both of `to_port`/`to_address` properties. A `rule_action` is not allowed with this property, uses the action "accept" internally. See "Forward-Port" in firewalld.richlanguage(5).' + +property :to_port, + Integer, + description: 'The port to forward traffic to. See "Forward-Port" in firewalld.richlanguage(5).' + +property :to_address, + String, + callbacks: { 'must be a valid IP address' => ->(ip) { !!IPAddr.new(ip) } }, + description: 'The IP address to forward traffic to. See "Forward-Port" in firewalld.richlanguage(5).' + +property :source_port, + [Integer, String], + description: 'The source port or range of source ports to match for the rule. Requires the `protocol` property to also be specified. See "Source-Port" in firewalld.richlanguage(5).' + +property :log, + [true, false], + default: false, + description: 'Enables logging for the rule. See "Log" in firewalld.richlanguage(5).' + +property :log_prefix, + String, + description: 'Adds a prefix to log messages for identification. Maximum length is 127 characters. See "Log" in firewalld.richlanguage(5).' + +property :log_level, + Symbol, + equal_to: [:emerg, :alert, :crit, :error, :warning, :notice, :info, :debug], + default: :warning, + description: 'Log level can be one of `:emerg`, `:alert`, `:crit`, `:error`, `:warning`, `:notice`, `:info`, `:debug`. Default is `:warning`. See "Log" in firewalld.richlanguage(5).' + +property :log_limit, + String, + description: 'Limits the rate of log messages in the format "rate/duration" (e.g., `5/s`). See "Limit" in firewalld.richlanguage(5).' + +property :nflog, + [true, false], + default: false, + description: 'Log new connection attempts using kernel logging to pass the packets through a "netlink" socket to users or applications monitoring the multicast "group". See "NFLog" in firewalld.richlanguage(5).' + +property :nflog_group, + Integer, + description: 'The NFLOG group ID for the rule (0-65535). See NETLINK_NETFILTER in netlink(7) man page and NFLOG in both iptables-extensions(8) and nft(8) man pages for a more detailed description.' + +property :nflog_prefix, + String, + description: 'Adds a prefix to NFLog messages for identification. Maximum length is 127 characters. See "NFLog" in firewalld.richlanguage(5).' + +property :nflog_queue_size, + Integer, + description: 'Sets the queue size for NFLog messages. See "NFLog" in firewalld.richlanguage(5).' + +property :nflog_limit, + String, + description: 'Limits the rate of NFLog messages in the format "rate/duration" (e.g. `10/m`). See "Limit" in firewalld.richlanguage(5).' + +property :audit, + [true, false], + default: false, + description: 'Audit provides an alternative way for logging using audit records sent to the service auditd. Audit type will be discovered from the rule action automatically. See "Audit" in firewalld.richlanguage(5).' + +property :audit_limit, + String, + description: 'Limits the rate of audit messages in the format "rate/duration" (e.g. `1/s`). See "Limit" in firewalld.richlanguage(5).' + +property :rule_action, + Symbol, + equal_to: [:accept, :reject, :drop, :mark], + description: 'The action for the rule. One of `:accept`, `:reject`, `:drop`, `:mark`. See "Action" in firewalld.richlanguage(5).' + +property :reject_type, + String, + description: 'Optional reject type to use with the `:reject` rule_action. See "Action" in firewalld.richlanguage(5).' + +property :mark_set, + String, + description: 'The mark value to set for matched packets (e.g. "0x1"). See "Action" in firewalld.richlanguage(5).' + +property :action_limit, + String, + description: 'Specifies a general rate limit for the rule in the format "rate/duration" (e.g. `20/s`). See "Limit" in firewalld.richlanguage(5).' + +property :raw, + String, + description: "A raw rule string (e.g. \"rule ...\") passed directly to firewalld, bypassing this resource's internal validation and handling. All other properties are ignored. See \"Rule\" in firewalld.richlanguage(5)." + +action :add do + rule = get_rich_rule_string() + + begin + sysbus = DBus.system_bus + zone_config = get_zone_config(sysbus) + rule_exists = zone_config.queryRichRule(rule) + + unless rule_exists + converge_by rule do + zone_config.addRichRule(rule) + firewalld_interface(sysbus).reload + end + end + rescue DBus::Error => e + raise "Failed to add firewalld rich rule \"#{rule}\": #{e.message}" + end +end + +action :remove do + rule = get_rich_rule_string() + + begin + sysbus = DBus.system_bus + zone_config = get_zone_config(sysbus) + rule_exists = zone_config.queryRichRule(rule) + + if rule_exists + converge_by rule do + zone_config.removeRichRule(rule) + firewalld_interface(sysbus).reload + end + end + rescue DBus::Error => e + raise "Failed to remove firewalld rich rule \"#{rule}\": #{e.message}" + end +end + +action_class do + include FirewallCookbook::Helpers::FirewalldDBus + + def get_rich_rule_string + if (property_is_set?(:port) || property_is_set?(:forward_port) || property_is_set?(:source_port)) && + !property_is_set?(:protocol) + raise 'Property "protocol" is required when using "port", "forward_port", or "source_port" properties.' + end + + if new_resource.rule_action == :mark && !property_is_set?(:mark_set) + raise 'Property "mark_set" is required when using the "mark" rule_action.' + end + + # :raw takes precedence if specified, ignoring all other properties. + # Otherwise, build the rich rule string based on given properties + rule = property_is_set?(:raw) ? new_resource.raw : build_rich_rule() + Chef::Log.debug rule + rule + end + + def get_zone_config(sysbus) + firewalld_service = sysbus['org.fedoraproject.FirewallD1'] + firewalld_object = firewalld_service['/org/fedoraproject/FirewallD1/config'] + fw_config = firewalld_object['org.fedoraproject.FirewallD1.config'] + + zone_name = property_is_set?(:zone) ? new_resource.zone : get_default_zone(sysbus) + zone_path = fw_config.getZoneByName(zone_name) + zone_object = firewalld_service[zone_path] + zone_object['org.fedoraproject.FirewallD1.config.zone'] + end + + def requires_family? + property_is_set?(:family) || property_is_set?(:forward_port) || + property_is_set?(:source) || property_is_set?(:source_not) || + property_is_set?(:destination) || property_is_set?(:destination_not) || + property_is_set?(:to_address) + end + + def ip_family + # If they explicitly set family then leave as-is + return new_resource.family if property_is_set?(:family) + + return unless requires_family?() + + # Determine the IP address family based on provided properties + [:source, :source_not, :destination, :destination_not, :to_address].each do |property| + next unless new_resource.property_is_set?(property) + ip = IPAddr.new(new_resource.send(property)) + return ip.ipv4? ? :ipv4 : :ipv6 + end + + nil + end + + # A helper method within action_class to construct the rich rule string + def build_rich_rule + r = new_resource + + rule_parts = [] + rule_parts << 'rule' + rule_parts << "family=\"#{ip_family()}\"" if ip_family() + rule_parts << "priority=\"#{r.priority}\"" if property_is_set?(:priority) + rule_parts << "source address=\"#{r.source}\"" if property_is_set?(:source) + rule_parts << "source not address=\"#{r.source_not}\"" if property_is_set?(:source_not) + rule_parts << "destination address=\"#{r.destination}\"" if property_is_set?(:destination) + rule_parts << "destination not address=\"#{r.destination_not}\"" if property_is_set?(:destination_not) + rule_parts << "service name=\"#{r.service}\"" if property_is_set?(:service) + rule_parts << "port port=\"#{r.port}\" protocol=\"#{r.protocol}\"" if property_is_set?(:port) + rule_parts << "tcp-mss-clamp value=\"#{r.tcp_mss_clamp}\"" if property_is_set?(:tcp_mss_clamp) + rule_parts << "icmp-block name=\"#{r.icmp_block}\"" if property_is_set?(:icmp_block) + rule_parts << 'masquerade' if property_is_set?(:masquerade) + rule_parts << "icmp-type name=\"#{r.icmp_type}\"" if property_is_set?(:icmp_type) + + if property_is_set?(:protocol) && + !(property_is_set?(:port) || property_is_set?(:forward_port) || property_is_set?(:source_port)) + rule_parts << "protocol value=\"#{r.protocol}\"" + end + + if property_is_set?(:forward_port) + rule_parts << "forward-port port=\"#{r.forward_port}\" protocol=\"#{r.protocol}\"" + rule_parts << "to-port=\"#{r.to_port}\"" if property_is_set?(:to_port) + rule_parts << "to-addr=\"#{r.to_address}\"" if property_is_set?(:to_address) + end + + rule_parts << "source-port port=\"#{r.source_port}\" protocol=\"#{r.protocol}\"" if property_is_set?(:source_port) + + if property_is_set?(:log) + rule_parts << 'log' + rule_parts << "prefix=\"#{r.log_prefix}\"" if property_is_set?(:log_prefix) + rule_parts << "level=\"#{r.log_level}\"" if property_is_set?(:log_level) + rule_parts << "limit value=\"#{r.log_limit}\"" if property_is_set?(:log_limit) + end + + if property_is_set?(:nflog) + rule_parts << 'nflog' + rule_parts << "group=\"#{r.nflog_group}\"" if property_is_set?(:nflog_group) + rule_parts << "prefix=\"#{r.nflog_prefix}\"" if property_is_set?(:nflog_prefix) + rule_parts << "queue-size=\"#{r.nflog_queue_size}\"" if property_is_set?(:nflog_queue_size) + rule_parts << "limit value=\"#{r.nflog_limit}\"" if property_is_set?(:nflog_limit) + end + + if property_is_set?(:audit) + rule_parts << 'audit' + rule_parts << "limit value=\"#{r.audit_limit}\"" if property_is_set?(:audit_limit) + end + + if property_is_set?(:rule_action) + rule_parts << "#{r.rule_action}" + + rule_parts << "type=\"#{r.reject_type}\"" if property_is_set?(:reject_type) + rule_parts << "set=\"#{r.mark_set}\"" if property_is_set?(:mark_set) + rule_parts << "limit value=\"#{r.action_limit}\"" if property_is_set?(:action_limit) + end + + rule_parts.join(' ') + end +end diff --git a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb index 34347b71..85f5e09e 100644 --- a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb @@ -146,3 +146,5 @@ egress_priority 200 only_if { test_zone_priority } end + +include_recipe 'firewalld-test::rich_rules' diff --git a/test/fixtures/cookbooks/firewalld-test/recipes/rich_rules.rb b/test/fixtures/cookbooks/firewalld-test/recipes/rich_rules.rb new file mode 100644 index 00000000..2ca0d5a0 --- /dev/null +++ b/test/fixtures/cookbooks/firewalld-test/recipes/rich_rules.rb @@ -0,0 +1,171 @@ + +firewalld_zone 'rich-rules' + +# Leave this rule in the default zone, i.e. don't use "zone" property, so that +# we test the default zone handling. +firewalld_rich_rule '443/tcp' do + port 443 + protocol 'tcp' + audit true + rule_action :accept +end + +firewalld_rich_rule 'Allow SSH service' do + zone 'rich-rules' + service 'ssh' + rule_action :accept +end + +firewalld_rich_rule 'Log and accept from 192.168.1.1' do + zone 'rich-rules' + source '192.168.1.1' + log true + log_prefix 'LOG_PREFIX' + rule_action :accept +end + +firewalld_rich_rule 'Reject traffic from 172.16.0.0/12 with priority' do + zone 'rich-rules' + family :ipv4 + source '172.16.0.0/12' + priority(-10) + rule_action :reject +end + +firewalld_rich_rule 'Accept traffic not from 172.16.2.1' do + zone 'rich-rules' + source_not '172.16.2.1' + rule_action :accept +end + +firewalld_rich_rule 'Accept traffic to 10.0.0.0/8' do + zone 'rich-rules' + family :ipv4 + destination '10.0.0.0/8' + rule_action :accept +end + +firewalld_rich_rule 'Accept traffic not to 172.16.2.1' do + zone 'rich-rules' + destination_not '172.16.2.1' + rule_action :accept +end + +firewalld_rich_rule 'Drop UDP traffic on ports 1000-2000' do + zone 'rich-rules' + port '1000-2000' + protocol 'udp' + rule_action :drop +end + +firewalld_rich_rule 'Audit and accept HTTPS traffic' do + zone 'rich-rules' + port 443 + protocol 'tcp' + audit true + rule_action :accept +end + +firewalld_rich_rule 'Accept ICMP protocol traffic' do + zone 'rich-rules' + protocol 'icmp' + rule_action :accept +end + +firewalld_rich_rule 'Block neighbor solicitation ICMP' do + zone 'rich-rules' + icmp_block 'neighbour-solicitation' +end + +firewalld_rich_rule 'Drop router-advertisement ICMP type' do + zone 'rich-rules' + icmp_type 'router-advertisement' + rule_action :drop +end + +firewalld_rich_rule 'Enable masquerading' do + zone 'rich-rules' + masquerade true +end + +firewalld_rich_rule 'Forward port 8080 to 80' do + zone 'rich-rules' + family :ipv4 + forward_port 8080 + protocol 'tcp' + to_port 80 +end + +firewalld_rich_rule 'Forward port 5353 to 53 at 10.10.10.10' do + zone 'rich-rules' + forward_port 5353 + protocol 'udp' + to_port 53 + to_address '10.10.10.10' +end + +firewalld_rich_rule 'Accept TCP traffic from source port 25' do + zone 'rich-rules' + source_port 25 + protocol 'tcp' + rule_action :accept +end + +firewalld_rich_rule 'Log and accept TCP traffic on ports 12000-12999' do + zone 'rich-rules' + source_port '12000-12999' + protocol 'tcp' + log true + log_level :info + log_limit '5/s' + rule_action :accept +end + +firewalld_rich_rule 'Mark FTP traffic with 0x1' do + zone 'rich-rules' + service 'ftp' + rule_action :mark + mark_set '0x1' +end + +firewalld_rich_rule 'Log and accept TFTP traffic from 192.168.0.0/24' do + zone 'rich-rules' + source '192.168.0.0/24' + service 'tftp' + log true + log_prefix 'tftp' + log_level :info + log_limit '1/m' + rule_action :accept +end + +firewalld_rich_rule 'Accept RADIUS traffic on IPV6' do + zone 'rich-rules' + family :ipv6 + service 'radius' + rule_action :accept + action_limit '100/m' +end + +firewalld_rich_rule 'Forward RADIUS port on IPv6' do + zone 'rich-rules' + family :ipv6 + source '1:2:3:4:6::' + forward_port 4011 + protocol 'tcp' + to_port 4012 + to_address '1::2:3:4:7' +end + +firewalld_rich_rule 'Reject traffic from 192.168.2.3 with ICMP admin prohibited' do + zone 'rich-rules' + family :ipv4 + source '192.168.2.3' + rule_action :reject + reject_type 'icmp-admin-prohibited' +end + +firewalld_rich_rule 'Raw rule' do + zone 'rich-rules' + raw 'rule family="ipv6" source address="::1" log prefix="RAW_RULE" accept' +end diff --git a/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb b/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb index e718993b..9c803f15 100644 --- a/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb +++ b/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb @@ -2,7 +2,37 @@ it { should be_installed } it { should be_running } its('default_zone') { should eq 'home' } + it { should have_rule_enabled('rule port port="443" protocol="tcp" audit accept', 'home') } it { should have_zone('ztest') } + it { should have_zone('rich-rules') } + + # Test rich rules in rich-rules zone + [ + 'rule priority="-10" family="ipv4" source address="172.16.0.0/12" reject', + 'rule family="ipv4" destination NOT address="172.16.2.1" accept', + 'rule family="ipv4" forward-port port="8080" protocol="tcp" to-port="80"', + 'rule icmp-block name="neighbour-solicitation"', + 'rule port port="1000-2000" protocol="udp" drop', + 'rule service name="ssh" accept', + 'rule family="ipv4" forward-port port="5353" protocol="udp" to-port="53" to-addr="10.10.10.10"', + 'rule family="ipv6" service name="radius" accept limit value="100/m"', + 'rule family="ipv4" source address="192.168.1.1" log prefix="LOG_PREFIX" accept', + 'rule source-port port="12000-12999" protocol="tcp" log level="info" limit value="5/s" accept', + 'rule service name="ftp" mark set=0x1', + 'rule family="ipv6" source address="1:2:3:4:6::" forward-port port="4011" protocol="tcp" to-port="4012" to-addr="1::2:3:4:7"', + 'rule protocol value="icmp" accept', + 'rule family="ipv4" source address="192.168.2.3" reject type="icmp-admin-prohibited"', + 'rule source-port port="25" protocol="tcp" accept', + 'rule family="ipv4" source NOT address="172.16.2.1" accept', + 'rule masquerade', + 'rule icmp-type name="router-advertisement" drop', + 'rule family="ipv4" destination address="10.0.0.0/8" accept', + 'rule family="ipv4" source address="192.168.0.0/24" service name="tftp" log prefix="tftp" level="info" limit value="1/m" accept', + 'rule port port="443" protocol="tcp" audit accept', + 'rule family="ipv6" source address="::1" log prefix="RAW_RULE" accept', + ].each do |rule| + it { should have_rule_enabled(rule, 'rich-rules') } + end end describe firewalld.where(zone: 'ztest') do @@ -11,19 +41,6 @@ its('services') { should cmp [['ssh']] } end -# Why does it give me undefined method `target' for Firewall Rules with zone == "ztest"? -# describe firewalld.where (zone: 'ztest') do -# its('target') { should cmp 'asdf' } -# its('ports') { should cmp 'asdf' } -# its('protocols') { should cmp 'asdf' } -# its('forward_ports') { should cmp 'asdf' } -# its('source_ports') { should cmp 'asdf' } -# its('icmp_blocks') { should cmp 'asdf' } -# its('rich_rules') { should cmp 'asdf' } -# it { should have_icmp_block_inversion_enabled } -# it { should have_masquerade_enabled } -# end - describe command('firewall-cmd --info-helper=example-helper') do example_helper = <<~EOF example-helper @@ -174,6 +191,7 @@ source-ports:#{' '} icmp-blocks:#{' '} rich rules:#{' '} + \trule port port="443" protocol="tcp" audit accept EOF its(:stdout) { should cmp expected_config } end From 7213e70ea985c3d3a507b6cfe5cf8e1373d74edd Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Thu, 26 Dec 2024 11:42:15 -0800 Subject: [PATCH 16/24] Migrate firewall_rule to a modern custom resource Signed-off-by: Joseph Larionov --- libraries/provider_firewall_firewalld.rb | 2 +- libraries/provider_firewall_iptables.rb | 2 +- .../provider_firewall_iptables_ubuntu.rb | 2 +- .../provider_firewall_iptables_ubuntu1404.rb | 2 +- libraries/provider_firewall_rule.rb | 34 ----------- libraries/provider_firewall_ufw.rb | 2 +- libraries/provider_firewall_windows.rb | 2 +- libraries/resource_firewall_rule.rb | 52 ----------------- resources/firewall_rule.rb | 58 +++++++++++++++++++ 9 files changed, 64 insertions(+), 92 deletions(-) delete mode 100644 libraries/provider_firewall_rule.rb delete mode 100644 libraries/resource_firewall_rule.rb create mode 100644 resources/firewall_rule.rb diff --git a/libraries/provider_firewall_firewalld.rb b/libraries/provider_firewall_firewalld.rb index 626a2ca2..1d17ac35 100644 --- a/libraries/provider_firewall_firewalld.rb +++ b/libraries/provider_firewall_firewalld.rb @@ -59,7 +59,7 @@ def whyrun_supported? new_resource.rules['firewalld'] = {} unless new_resource.rules['firewalld'] # this populates the hash of rules from firewall_rule resources - firewall_rules = Chef.run_context.resource_collection.select { |item| item.is_a?(Chef::Resource::FirewallRule) } + firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } firewall_rules.each do |firewall_rule| next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) diff --git a/libraries/provider_firewall_iptables.rb b/libraries/provider_firewall_iptables.rb index 1878861b..4bec8f8e 100644 --- a/libraries/provider_firewall_iptables.rb +++ b/libraries/provider_firewall_iptables.rb @@ -71,7 +71,7 @@ def whyrun_supported? ensure_default_rules_exist(node, new_resource) # this populates the hash of rules from firewall_rule resources - firewall_rules = Chef.run_context.resource_collection.select { |item| item.is_a?(Chef::Resource::FirewallRule) } + firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } firewall_rules.each do |firewall_rule| next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) diff --git a/libraries/provider_firewall_iptables_ubuntu.rb b/libraries/provider_firewall_iptables_ubuntu.rb index 35a31732..78d1db73 100644 --- a/libraries/provider_firewall_iptables_ubuntu.rb +++ b/libraries/provider_firewall_iptables_ubuntu.rb @@ -75,7 +75,7 @@ def whyrun_supported? ensure_default_rules_exist(node, new_resource) # this populates the hash of rules from firewall_rule resources - firewall_rules = Chef.run_context.resource_collection.select { |item| item.is_a?(Chef::Resource::FirewallRule) } + firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } firewall_rules.each do |firewall_rule| next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) diff --git a/libraries/provider_firewall_iptables_ubuntu1404.rb b/libraries/provider_firewall_iptables_ubuntu1404.rb index b8b277a8..87db6c2a 100644 --- a/libraries/provider_firewall_iptables_ubuntu1404.rb +++ b/libraries/provider_firewall_iptables_ubuntu1404.rb @@ -75,7 +75,7 @@ def whyrun_supported? ensure_default_rules_exist(node, new_resource) # this populates the hash of rules from firewall_rule resources - firewall_rules = Chef.run_context.resource_collection.select { |item| item.is_a?(Chef::Resource::FirewallRule) } + firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } firewall_rules.each do |firewall_rule| next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) diff --git a/libraries/provider_firewall_rule.rb b/libraries/provider_firewall_rule.rb deleted file mode 100644 index 2352cbc0..00000000 --- a/libraries/provider_firewall_rule.rb +++ /dev/null @@ -1,34 +0,0 @@ -# -# Author:: Ronald Doorn () -# Cookbook:: firewall -# Provider:: rule_iptables -# -# Copyright:: 2015-2016, computerlyrik -# -# 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. -# -class Chef - class Provider::FirewallRuleGeneric < Chef::Provider::LWRPBase - provides :firewall_rule - - action :create do - return unless new_resource.notify_firewall - - firewall_resource = Chef.run_context.resource_collection.find(firewall: new_resource.firewall_name) - raise 'could not find a firewall resource' unless firewall_resource - - new_resource.notifies(:restart, firewall_resource, :delayed) - new_resource.updated_by_last_action(true) - end - end -end diff --git a/libraries/provider_firewall_ufw.rb b/libraries/provider_firewall_ufw.rb index ce059048..1c8ed50d 100644 --- a/libraries/provider_firewall_ufw.rb +++ b/libraries/provider_firewall_ufw.rb @@ -66,7 +66,7 @@ def whyrun_supported? new_resource.rules['ufw'] = {} unless new_resource.rules['ufw'] # this populates the hash of rules from firewall_rule resources - firewall_rules = Chef.run_context.resource_collection.select { |item| item.is_a?(Chef::Resource::FirewallRule) } + firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } firewall_rules.each do |firewall_rule| next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) diff --git a/libraries/provider_firewall_windows.rb b/libraries/provider_firewall_windows.rb index a886ec3a..9117a2d4 100644 --- a/libraries/provider_firewall_windows.rb +++ b/libraries/provider_firewall_windows.rb @@ -46,7 +46,7 @@ def whyrun_supported? new_resource.rules({}) unless new_resource.rules new_resource.rules['windows'] = {} unless new_resource.rules['windows'] - firewall_rules = Chef.run_context.resource_collection.select { |item| item.is_a?(Chef::Resource::FirewallRule) } + firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } firewall_rules.each do |firewall_rule| next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) diff --git a/libraries/resource_firewall_rule.rb b/libraries/resource_firewall_rule.rb deleted file mode 100644 index 18972d6d..00000000 --- a/libraries/resource_firewall_rule.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'ipaddr' - -class Chef - class Resource::FirewallRule < Chef::Resource::LWRPBase - include FirewallCookbook::Helpers - - resource_name(:firewall_rule) - provides(:firewall_rule) - default_action(:create) - - attribute(:firewall_name, kind_of: String, default: 'default') - - attribute(:command, kind_of: Symbol, equal_to: [:reject, :allow, :deny, :masquerade, :redirect, :log], default: :allow) - - attribute(:protocol, kind_of: [Integer, Symbol], default: :tcp, - callbacks: { 'must be either :tcp, :udp, :icmp, :\'ipv6-icmp\', :icmpv6, :none, or a valid IP protocol number' => lambda do |p| - !!(p.to_s =~ /(udp|tcp|icmp|icmpv6|ipv6-icmp|esp|ah|ipv6|none)/ || (p.to_s =~ /^\d+$/ && p.between?(0, 142))) - end }) - attribute(:direction, kind_of: Symbol, equal_to: [:in, :out, :pre, :post], default: :in) - attribute(:logging, kind_of: Symbol, equal_to: [:connections, :packets]) - - attribute(:source, kind_of: String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } }) - attribute(:source_port, kind_of: [Integer, Array, Range]) # source port - attribute(:interface, kind_of: String) - - attribute(:port, kind_of: [Integer, Array, Range]) # shorthand for dest_port - attribute(:destination, kind_of: String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } }) - attribute(:dest_port, kind_of: [Integer, Array, Range]) - attribute(:dest_interface, kind_of: String) - - attribute(:position, kind_of: Integer, default: 50) - attribute(:stateful, kind_of: [Symbol, Array]) - attribute(:redirect_port, kind_of: Integer) - attribute(:description, kind_of: String, name_attribute: true) - attribute(:include_comment, kind_of: [TrueClass, FalseClass], default: true) - - # only used for firewalld - attribute(:permanent, kind_of: [TrueClass, FalseClass], default: false) - attribute(:zone, kind_of: String) - - # only used for Windows Firewalls - attribute(:program, kind_of: String) - attribute(:service, kind_of: String) - - # for when you just want to pass a raw rule - attribute(:raw, kind_of: String) - - # do you want this rule to notify the firewall to recalculate - # (and potentially reapply) the firewall_rule(s) it finds? - attribute(:notify_firewall, kind_of: [TrueClass, FalseClass], default: true) - end -end diff --git a/resources/firewall_rule.rb b/resources/firewall_rule.rb new file mode 100644 index 00000000..4a7fc29b --- /dev/null +++ b/resources/firewall_rule.rb @@ -0,0 +1,58 @@ + +require 'ipaddr' + +unified_mode true + +provides :firewall_rule + +default_action :create + +property :firewall_name, String, default: 'default' +property :command, Symbol, equal_to: [:reject, :allow, :deny, :masquerade, :redirect, :log], default: :allow +property :protocol, [Integer, Symbol], default: :tcp, + callbacks: { + 'must be either :tcp, :udp, :icmp, :\'ipv6-icmp\', :icmpv6, :none, or a valid IP protocol number' => + lambda do |p| + !!(p.to_s =~ /(udp|tcp|icmp|icmpv6|ipv6-icmp|esp|ah|ipv6|none)/ || (p.to_s =~ /^\d+$/ && p.between?(0, 142))) + end } + +property :direction, Symbol, equal_to: [:in, :out, :pre, :post], default: :in +property :logging, Symbol, equal_to: [:connections, :packets] +property :source, String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } } +property :source_port, [Integer, Array, Range] +property :interface, String +property :port, [Integer, Array, Range] +property :destination, String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } } +property :dest_port, [Integer, 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 + +# only used for firewalld +property :permanent, [true, false], default: false + +property :zone, String + +# only used for Windows Firewalls +property :program, String +property :service, String + +# 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 unless new_resource.notify_firewall + + firewall_resource = Chef.run_context.resource_collection.find(firewall: new_resource.firewall_name) + raise 'could not find a firewall resource' unless firewall_resource + + new_resource.notifies(:restart, firewall_resource, :delayed) + new_resource.updated_by_last_action(true) +end From 9dc78fa12c2aba960e5a667021e5753368f4ffda Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Fri, 27 Dec 2024 12:35:25 -0800 Subject: [PATCH 17/24] Remove deprecated disabled property from firewall resource Signed-off-by: Joseph Larionov --- CHANGELOG.md | 4 ++++ README.md | 4 ++-- libraries/helpers.rb | 3 +-- libraries/resource_firewall.rb | 3 --- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bab0c481..9ff437e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ This file is used to list changes made in each version of the firewall cookbook. - Fixed: `firewalld_*` resources ignore properties whose value is `false`. - Fixed: `firewalld_*` resources were not idempotent when using `ports`, `source_ports`, and `rich_rules` properties. +### Removed + +- Removed deprecated `disabled` property from `firewall` resource. + ## 6.3.9 - *2024-12-05* ## 6.3.8 - *2024-11-18* diff --git a/README.md b/README.md index cb2eb017..c5852e15 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,8 @@ There is a separate folder for [`firewalld` resources](documentation/README.md). #### Parameters -- `disabled` (default to `false`): If set to true, all actions will no-op on this resource. This is a way to prevent included cookbooks from configuring a firewall. -- `ipv6_enabled` (default to `true`): If set to false, firewall will not perform any ipv6 related work. Currently only supported in iptables. +- `enabled` (default to `true`): If set to `false`, all actions will no-op on this resource. This is a way to prevent + included cookbooks from configuring a firewall. - `log_level`: UFW only. Level of verbosity the firewall should log at. valid values are: :low, :medium, :high, :full, :off. default is :low. - `rules`: This is used internally for firewall_rule resources to append their rules. You should NOT touch this value unless you plan to supply an entire firewall ruleset at once, and skip using firewall_rule resources. - `disabled_zone` (firewalld only): The zone to set on firewalld when the firewall should be disabled. Can be any string in symbol form, e.g. :public, :drop, etc. Defaults to `:public.` diff --git a/libraries/helpers.rb b/libraries/helpers.rb index 497c586d..fd26822d 100644 --- a/libraries/helpers.rb +++ b/libraries/helpers.rb @@ -26,8 +26,7 @@ def ipv6_enabled?(new_resource) end def disabled?(new_resource) - # if either flag is found in the non-default boolean state - disable_flag = !(new_resource.enabled && !new_resource.disabled) + disable_flag = !new_resource.enabled Chef::Log.warn("#{new_resource} has been disabled, not proceeding") if disable_flag disable_flag diff --git a/libraries/resource_firewall.rb b/libraries/resource_firewall.rb index 3920f8ff..ff576611 100644 --- a/libraries/resource_firewall.rb +++ b/libraries/resource_firewall.rb @@ -5,9 +5,6 @@ class Resource::Firewall < Chef::Resource::LWRPBase actions(:install, :restart, :disable, :flush, :save) default_action(:install) - # allow both kinds of logic -- eventually remove the :disabled one. - # the positive logic is much easier to follow. - attribute(:disabled, kind_of: [TrueClass, FalseClass], default: false) attribute(:enabled, kind_of: [TrueClass, FalseClass], default: true) attribute(:log_level, kind_of: Symbol, equal_to: [:low, :medium, :high, :full, :off], default: :low) From 59fda7b394f56ddf06b93b0e50a8afe84c9fb2e6 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Fri, 27 Dec 2024 16:30:55 -0800 Subject: [PATCH 18/24] firewall_rule now implements firewalld rich rules on firewalld platforms Refactors firewalld support to use rich rules instead of the "--direct" interface, which was deprecated with the firewalld 1.0.0 release [1]. Adds IPv6 support for firewalld platforms (fixes #86). [1] https://firewalld.org/2021/06/the-upcoming-1-0-0 Signed-off-by: Joseph Larionov --- CHANGELOG.md | 7 + README.md | 198 ++++++++++-------- TODO.md | 1 - attributes/firewalld.rb | 8 - kitchen.yml | 2 - libraries/helpers_firewalld.rb | 116 ---------- libraries/provider_firewall_firewalld.rb | 187 ----------------- libraries/resource_firewall.rb | 6 +- recipes/default.rb | 48 +++-- recipes/firewalld.rb | 87 -------- resources/_partial/_firewall.rb | 8 + resources/_partial/_firewall_rule.rb | 24 +++ resources/firewall_firewalld.rb | 35 ++++ resources/firewall_rule.rb | 32 +-- resources/firewall_rule_firewalld.rb | 115 ++++++++++ resources/firewalld.rb | 4 + .../firewall-test/recipes/default.rb | 85 ++++---- .../default/inspec/default_spec.rb | 2 +- .../firewalld/inspec/firewalld_spec.rb | 56 ++--- test/integration/helpers/spec_helper.rb | 8 +- 20 files changed, 412 insertions(+), 617 deletions(-) delete mode 100644 attributes/firewalld.rb delete mode 100644 libraries/helpers_firewalld.rb delete mode 100644 libraries/provider_firewall_firewalld.rb delete mode 100644 recipes/firewalld.rb create mode 100644 resources/_partial/_firewall.rb create mode 100644 resources/_partial/_firewall_rule.rb create mode 100644 resources/firewall_firewalld.rb create mode 100644 resources/firewall_rule_firewalld.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff437e9..48169dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ This file is used to list changes made in each version of the firewall cookbook. - Support for firewalld 2.0.0 and the platforms that use it; RHEL 10 and Ubuntu 24.04. - `priority`, `ingress_priority`, `egress_priority` properties added to `firewalld_zone`. - Added `firewalld_rich_rule` resource for adding/removing rich rules to/from firewalld zones. +- Support for IPv6 rules on firewalld platforms. ### Changed - Ensure `firewalld` service remains enabled and started when installed. +- `firewall_rule` resource now creates rich rules on firewalld platforms, instead of the using the deprecated `--direct` firewalld interface. ### Fixed @@ -24,6 +26,11 @@ This file is used to list changes made in each version of the firewall cookbook. ### Removed - Removed deprecated `disabled` property from `firewall` resource. +- Removed all `default['firewall']['firewalld']` attributes. Use the `firewalld_zone` resource to manage firewalld zone configuration. +- Removed firewalld action `:save` from `firewall` resource. Firewalld rules are now always added permanently. +- Removed firewalld property `permanent` from `firewall_rule` resource. Firewalld rules are now always added permanently. +- Removed properties `disabled_zone` and `enabled_zone` from `firewall` resource. Use the `firewalld_zone` resource to manage firewalld zone configuration. +- Removed recipe `firewall::firewalld`. Its functionality has been merged into the `firewall::default` recipe. ## 6.3.9 - *2024-12-05* diff --git a/README.md b/README.md index c5852e15..ad2ae9df 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ depends 'firewall' - UFW - Ubuntu, Debian (except 9) - IPTables - Red Hat & CentOS, Ubuntu -- FirewallD - Red Hat & CentOS >= 7.0 (IPv4 only support, [needs contributions/testing](https://github.com/chef-cookbooks/firewall/issues/86)) +- FirewallD - Red Hat & CentOS >= 7.0 - Windows Advanced Firewall - 2012 R2 - nftables @@ -57,15 +57,32 @@ In order to use nftables, just use the resource `nftables` and `nftables_rule`. These resources are written in more modern design styles and are not configurable by node attributes. -## Considerations that apply to all firewall providers and resources +## Quickstart -This cookbook comes with two resources, firewall and firewall rule. The typical usage scenario is as follows: +To use this cookbook to open a port in the system's default firewall: -- run the `:install` action on the `firewall` resource named 'default', which installs appropriate packages and configures services to start on boot and starts them -- run the `:create` action on every `firewall_rule` resource, which adds to the list of rules that should be configured on the firewall. `firewall_rule` then automatically sends a delayed notification to the `firewall['default']` resource to run the `:restart` action. -- run the delayed notification with action `:restart` on the `firewall` resource. if any rules are different than the last run, the provider will update the current state of the firewall rules to match the expected rules. +```ruby +include_recipe 'firewall' + +firewall_rule 'ssh' do + port 22 +end +``` -There is a fundamental mismatch between the idea of a chef action and the action that should be taken on a firewall rule. For this reason, the chef action for a firewall_rule may be `:nothing` (the rule should not be present in the firewall) or `:create` (the rule should be present in the firewall), but the action taken on a packet in a firewall (`DROP`, `ACCEPT`, etc) is denoted as a `command` parameter on the `firewall_rule` resource. +## How it works + +The most basic use involves two resources, `firewall` and `firewall_rule`. The typical usage scenario is as follows: + +- include the `'firewall::default'` recipe or run the `:install` action on the `firewall` resource named `'default'`, which installs appropriate packages and configures services to start on boot and starts them. +- run the `:create` action on every `firewall_rule` resource, which adds to the list of rules that should be configured on the firewall. How the rules are implemented depends on the firewall platform: + - **firewalld**: `firewall_rule` implements the rules under the hood as firewalld [rich rules](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html) in the system's default zone. + - **iptables, ufw, windows**: `firewall_rule` automatically sends a delayed notification to the `firewall['default']` resource to run the `:restart` action. + - when the delayed `:restart` notification on the `firewall` resource fires, if any rules are different than the last run, the provider will update the current state of the firewall rules to match the expected rules. + +There is a fundamental mismatch between the idea of a Chef action and the action that should be taken on a firewall +rule. For this reason, the Chef action for a `firewall_rule` may be `:create` (the rule should be present in the +firewall) but the action taken on a packet in a firewall (`DROP`, `ACCEPT`, etc) is denoted as a `command` property on +the `firewall_rule` resource. The same points hold for the `nftables`- and `nftables_rule`-resources. @@ -108,17 +125,24 @@ Please read the documentation for the [`nftables` resource](documentation/resource_nftables.md) and the [`nftables_rule` resource](documentation/resource_nftables_rule.md) -### default +## firewalld -The default recipe creates a firewall resource with action install. +For most rules it's sufficient to simply use the `firewall_rule` resource which is a platform-agnostic way to add +firewall rules. On firewalld systems it adds rules to the default zone as firewalld [rich +rules](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). See the +[`firewall_rule`](#firewall_rule) section for examples. -### disable_firewall +For more advanced firewalld configuration, see the documentation for the various [`firewalld` resources](documentation/README.md). -Used to disable platform specific firewall. Many clouds have their own firewall configured outside of the OS instance such as AWS Security Groups. +## Recipes -### firewalld +### `firewall::default` -A firewalld specific recipe creates a firewall resource with action install with the default zone (default: `drop`) +The default recipe creates a firewall resource with action install. + +### `firewall::disable_firewall` + +Used to disable platform specific firewall. Many clouds have their own firewall configured outside of the OS instance such as AWS Security Groups. ## Attributes @@ -134,39 +158,30 @@ A firewalld specific recipe creates a firewall resource with action install with - `default['firewall']['windows']['defaults']` hash to define inbound / outbound firewall policy on Windows platform - `default['firewall']['allow_established'] = true`, set to false if you don't want a related/established default rule on iptables - `default['firewall']['ipv6_enabled'] = true`, set to false if you don't want IPv6 related/established default rule on iptables (this enables ICMPv6, which is required for much of IPv6 communication) -- `default['firewall']['firewalld']['permanent'] = false`, set to true if you want firewalld rules to be added with `--permanent` so they survive a reboot. This will be changed to `true` by default in a future major version release. -- `default['firewall']['firewalld']['permanent'] = false`, set to true if you want firewalld rules to be added with `--permanent` so they survive a reboot. This will be changed to `true` by default in a future major version release. -- `default['firewall']['firewalld']['zone'] = 'drop'`, Default zone for firewall -- `default['firewall']['firewalld']['loopback_zone'] = 'trusted'`, zone for loopback to be enabled (using `allow_loopback`) -- `default['firewall']['firewalld']['icmp_zone'] = 'public'`, zone for icmp to be enabled (using `allow_icmp`) -- `default['firewall']['firewalld']['ssh_zone'] = 'public'`, zone for ssh to be enabled (using `allow_ssh`) -- `default['firewall']['firewalld']['mosh_zone'] = 'public'`, zone for mosh to be enabled (using `allow_mosh`) -- `default['firewall']['firewalld']['established_zone'] = 'public'`, zone for loopback to be enabled (using `allow_established`) ## Resources -There is a separate folder for [`firewalld` resources](documentation/README.md). +### `firewall` -### firewall +It's not recommended to use this resource directly. Instead simply `include_recipe 'firewall'` and then add your desired +`firewall_rule` resources. See the [`firewall_rule`](#firewall_rule) section for examples. ***NB***: The name 'default' of this resource is important as it is used for firewall_rule providers to locate the firewall resource. If you change it, you must also supply the same value to any firewall_rule resources using the `firewall_name` parameter. #### Actions - `:install` (*default action*): Install and Enable the firewall. This will ensure the appropriate packages are installed and that any services have been started. +- `:reload`: _firewalld only_. Reloads the runtime state to match the permanent configuration. All runtime-only rules are flushed out. - `:disable`: Disable the firewall. Drop any rules and put the node in an unprotected state. Flush all current rules. Also erase any internal state used to detect when rules should be applied. -- `:flush`: Flush all current rules. Also erase any internal state used to detect when rules should be applied. -- `:save`: Ensure all rules are added permanently under firewalld using `--permanent`. Not supported on ufw, iptables. You must notify this action at the end of the chef run if you want permanent firewalld rules (they are not persistent by default). +- `:flush`: _Except firewalld_. Flush all current rules. Also erase any internal state used to detect when rules should be applied. -#### Parameters +#### Properties - `enabled` (default to `true`): If set to `false`, all actions will no-op on this resource. This is a way to prevent included cookbooks from configuring a firewall. +- `ipv6_enabled` (default to `true`): _Iptables only_. If set to false, firewall will not perform any ipv6 related work. - `log_level`: UFW only. Level of verbosity the firewall should log at. valid values are: :low, :medium, :high, :full, :off. default is :low. -- `rules`: This is used internally for firewall_rule resources to append their rules. You should NOT touch this value unless you plan to supply an entire firewall ruleset at once, and skip using firewall_rule resources. -- `disabled_zone` (firewalld only): The zone to set on firewalld when the firewall should be disabled. Can be any string in symbol form, e.g. :public, :drop, etc. Defaults to `:public.` -- `enabled_zone` (firewalld only): The zone to set on firewalld when the firewall should be enabled. Can be any string in symbol form, e.g. :public, :drop, etc. Defaults to `:drop.` -- `package_options`: Used to pass options to the package install of firewall +- `package_options`: Pass additional options to the package manager when installing the firewall. ```ruby # all defaults @@ -184,41 +199,87 @@ firewall 'default' do end ``` -### firewall_rule +### `firewall_rule` #### Actions -- `:create` (*default action*): If a firewall_rule runs this action, the rule will be recorded in a chef resource's internal state, and applied when providers automatically notify the firewall resource with action `:reload`. The notification happens automatically. +- `:create`: Create the firewall rule and notify the firewall to reload after the rule has been saved. On firewalld systems, the rules are added to the default zone as firewalld [rich rules](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). + +#### Properties + +The full syntax for all properties and which platforms they're available for: + +```ruby +firewall_rule 'name' do + firewall_name String # Default: 'default' + command Symbol # Default: :allow + protocol Integer, Symbol # Default: :tcp + source String + source_port Integer, Array, Range + port Integer, Array, Range + dest_port Integer, Array, Range + destination String + position Integer # Default: 50 + description String # Default: 'name' unless specified + + # Platform-specific properties + zone String # Platforms: firewalld + logging Symbol # platforms: ufw + redirect_port Integer # platforms: iptables, firewalld + dest_interface String # platforms: iptables, windows + interface String # platforms: iptables, ufw, windows + include_comment true, false # platforms: iptables, ufw. Default: true + stateful Symbol, Array # platforms: iptables, ufw + raw String # platforms: iptables, ufw + direction Symbol # Platforms: iptables, ufw, windows. Default: :in + notify_firewall true, false # platforms: iptables, ufw, windows. Default: true + program String # platforms: windows + service String # platforms: windows +end +``` -#### Parameters +Platform-agnostic properties that can be used with `firewall_rule` on any firewall system: - `firewall_name`: the matching firewall resource that this rule applies to. Default value: `default` -- `raw`: Used to pass an entire rule as a string, omitting all other parameters. This line will be directly loaded by `iptables-restore`, fed directly into `ufw` on the command line, or run using `firewall-cmd`. - `description` (*default: same as rule name*): Used to provide a comment that will be included when adding the firewall rule. -- `include_comment` (*default: true*): Used to optionally exclude the comment in the rule. -- `position` (*default: 50*): **relative** position to insert rule at. Position may be any integer between 0 < n < 100 (exclusive), and more than one rule may specify the same position. - `command`: What action to take on a particular packet - `:allow` (*default action*): the rule should allow matching packets - - `:deny`: the rule should deny matching packets + - `:deny`: the rule should deny (drop) matching packets - `:reject`: the rule should reject matching packets - `:masquerade`: Masquerade the matching packets - `:redirect`: Redirect the matching packets - `:log`: Configure logging -- `stateful`: a symbol or array of symbols, such as ``[:related, :established]` that will be passed to the state module in iptables or firewalld. -- `zone`: (*firewalld only*), a string, such as `public` that the rule will be applied. -- `protocol`: `:tcp` (*default*), `:udp`, `:icmp`, `:none` or protocol number. Using protocol numbers is not supported using the ufw provider (default for debian/ubuntu systems). -- `direction`: For ufw, direction of the rule. valid values are: `:in` (*default*), `:out`, `:pre`, `:post`. +- `protocol`: `:tcp` (*default*), `:udp`, `:icmp`, `:none`, or protocol number. Using protocol numbers is not supported + using the ufw provider (default for debian/ubuntu systems). - `source` (*Default is `0.0.0.0/0` or `Anywhere`*): source ip address or subnet to filter. - `source_port` (*Default is nil*): source port for filtering packets. +- `port` or `dest_port`: target port number (ie. `22` to allow inbound SSH), an array of incoming port numbers (ie. + `[80,443]` to allow inbound HTTP & HTTPS), or a range of incoming ports `(12000..12100)`. - `destination`: ip address or subnet to filter on packet destination, must be a valid IP -- `port` or `dest_port`: target port number (ie. 22 to allow inbound SSH), or an array of incoming port numbers (ie. [80,443] to allow inbound HTTP & HTTPS). - NOTE: `protocol` attribute is required with multiple ports, or a range of incoming port numbers (ie. 60000..61000 to allow inbound mobile-shell. NOTE: `protocol`, or an attribute is required with a range of ports. -- `interface`: (source) interface to apply rule (ie. `eth0`). -- `dest_interface`: interface where packets may be destined to go -- `redirect_port`: redirected port for rules with command `:redirect` -- `logging`: may be added to enable logging for a particular rule. valid values are: `:connections`, `:packets`. In the ufw provider, `:connections` logs new connections while `:packets` logs all packets. +- `position` (*default: 50*): **relative** position to insert rule at. Position may be any integer between 0 < n < 100 (exclusive), and more than one rule may specify the same position. + +Additional properties for advanced firewall rules that tied to specific firewall solutions. **Note: These properties are _not_ platform-agnostic, so you must ensure they are used only on the appropriate platforms**: + +- `zone`: (*firewalld*), a string, such as `public` that the rule will be applied. Defaults to the system's configured + default zone. +- `logging` (*ufw*): may be added to enable logging for a particular rule. valid values are: `:connections`, `:packets`. + In the ufw provider, `:connections` logs new connections while `:packets` logs all packets. +- `redirect_port` (*iptables, firewalld*): redirected port for rules with command `:redirect`. +- `dest_interface` (*iptables, windows*): interface where packets may be destined to go. +- `interface` (*iptables, ufw, windows*): (source) interface to apply rule (ie. `eth0`). +- `include_comment` (*iptables, ufw*): Used to optionally exclude the comment in the rule. Default: `true`. +- `stateful` (*iptables, ufw*): a symbol or array of symbols, such as ``[:related, :established]` that will be passed to the state module in iptables or firewalld. +- `raw` (*iptables, ufw*): Used to pass an entire rule as a string, omitting all other parameters. This line will be directly loaded by `iptables-restore`/fed directly into `ufw` on the command line. +- `direction` (*iptables, ufw, windows*): Direction of the rule. Valid values are: `:in` (*default*), `:out`, `:pre`, +`:post`. +- `notify_firewall` (*iptables, ufw, windows*): Notify the firewall to recalculate (and potentially reapply) the + `firewall_rule`(s) it finds. Default: `true` + +#### Examples ```ruby +include_recipe 'firewall' + # open standard ssh port firewall_rule 'ssh' do port 22 @@ -233,39 +294,11 @@ firewall_rule 'http' do command :allow end -# restrict port 13579 to 10.0.111.0/24 on eth0 -firewall_rule 'myapplication' do - port 13579 - source '10.0.111.0/24' - direction :in - interface 'eth0' - command :allow -end - -# specify a protocol number (supported on centos/redhat) -firewall_rule 'vrrp' do - protocol 112 - command :allow -end - -# use the iptables provider to specify protocol number on debian/ubuntu -firewall_rule 'vrrp' do - provider Chef::Provider::FirewallRuleIptables - protocol 112 - command :allow -end - -# can use :raw command with UFW provider for VRRP -firewall_rule "VRRP" do - command :allow - raw "allow to 224.0.0.18" -end - # open UDP ports 60000..61000 for mobile shell (mosh.org), note # that the protocol attribute is required when using port_range firewall_rule 'mosh' do - protocol :udp - port 60000..61000 + protocol :udp + port 60000..61000 command :allow end @@ -283,19 +316,8 @@ firewall_rule 'ssh' do zone "public" command :allow end - -firewall 'default' do - enabled false - action :nothing -end ``` -#### Providers - -- See `libraries/z_provider_mapping.rb` for a full list of providers for each platform and version. - -Different providers will determine the current state of the rules differently -- parsing the output of a command, maintaining the state in a file, or some other way. If the firewall is adjusted from outside of chef (non-idempotent), it's possible that chef may be caught unaware of the current state of the firewall. The best workaround is to add a `:flush` action to the firewall resource as early as possible in the chef run, if you plan to modify the firewall state outside of chef. - ## Troubleshooting To figure out what the position values are for current rules, print the hash that contains the weights: diff --git a/TODO.md b/TODO.md index 34997ff6..54bb036e 100644 --- a/TODO.md +++ b/TODO.md @@ -3,4 +3,3 @@ - update for rhel-8+ nftables, RHEL docs recommend nftables for new firewalls - fix windows tests - iptables' `-S` not supported in libraries/provider_firewall_iptables.rb -- save action might not make sense for firewalls diff --git a/attributes/firewalld.rb b/attributes/firewalld.rb deleted file mode 100644 index f8b8dbdb..00000000 --- a/attributes/firewalld.rb +++ /dev/null @@ -1,8 +0,0 @@ -default['firewall']['firewalld']['permanent'] = false -default['firewall']['firewalld']['zone'] = 'drop' - -default['firewall']['firewalld']['loopback_zone'] = 'trusted' -default['firewall']['firewalld']['icmp_zone'] = 'public' -default['firewall']['firewalld']['ssh_zone'] = 'public' -default['firewall']['firewalld']['mosh_zone'] = 'public' -default['firewall']['firewalld']['established_zone'] = 'public' diff --git a/kitchen.yml b/kitchen.yml index 2da2f8fa..6be0b33d 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -14,8 +14,6 @@ provisioner: allow_mosh: true allow_loopback: true allow_icmp: true - firewalld: - permanent: true verifier: name: inspec diff --git a/libraries/helpers_firewalld.rb b/libraries/helpers_firewalld.rb deleted file mode 100644 index 5deb4025..00000000 --- a/libraries/helpers_firewalld.rb +++ /dev/null @@ -1,116 +0,0 @@ -module FirewallCookbook - module Helpers - module Firewalld - include FirewallCookbook::Helpers - include Chef::Mixin::ShellOut - - def firewalld_rules_filename - '/etc/sysconfig/firewalld-chef.rules' - end - - def firewalld_rule!(cmd) - shell_out!(cmd, input: 'yes') - end - - def firewalld_active? - cmd = shell_out('firewall-cmd', '--state') - cmd.stdout =~ /^running$/ - end - - def firewalld_default_zone?(z) - return false unless firewalld_active? - - cmd = shell_out('firewall-cmd', '--get-default-zone') - cmd.stdout =~ /^#{z}$/ - end - - def firewalld_default_zone!(z) - raise 'firewalld not active' unless firewalld_active? - - shell_out!('firewall-cmd', "--set-default-zone=#{z}") - end - - def log_current_firewalld - shell_out!('firewall-cmd --direct --get-all-rules') - end - - def firewalld_flush! - raise 'firewall not active' unless firewalld_active? - - shell_out!('firewall-cmd', '--direct', '--remove-rules', 'ipv4', 'filter', 'INPUT') - shell_out!('firewall-cmd', '--direct', '--remove-rules', 'ipv4', 'filter', 'OUTPUT') - shell_out!('firewall-cmd', '--direct', '--permanent', '--remove-rules', 'ipv4', 'filter', 'INPUT') - shell_out!('firewall-cmd', '--direct', '--permanent', '--remove-rules', 'ipv4', 'filter', 'OUTPUT') - end - - def firewalld_all_rules_permanent! - raise 'firewall not active' unless firewalld_active? - - rules = shell_out!('firewall-cmd', '--direct', '--get-all-rules').stdout - perm_rules = shell_out!('firewall-cmd', '--direct', '--permanent', '--get-all-rules').stdout - rules == perm_rules - end - - def firewalld_save! - raise 'firewall not active' unless firewalld_active? - - shell_out!('firewall-cmd', '--direct', '--permanent', '--remove-rules', 'ipv4', 'filter', 'INPUT') - shell_out!('firewall-cmd', '--direct', '--permanent', '--remove-rules', 'ipv4', 'filter', 'OUTPUT') - shell_out!('firewall-cmd', '--direct', '--get-all-rules').stdout.lines do |line| - shell_out!("firewall-cmd --direct --permanent --add-rule #{line}") - end - end - - def ip_versions(resource) - if ipv4_rule?(resource) - %w(ipv4) - elsif ipv6_rule?(resource) - %w(ipv6) - else # no source or destination address, add rules for both ipv4 and ipv6 - %w(ipv4 ipv6) - end - end - - CHAIN = { in: 'INPUT', out: 'OUTPUT', pre: 'PREROUTING', post: 'POSTROUTING' }.freeze unless defined? CHAIN # , nil => "FORWARD"} - TARGET = { allow: 'ACCEPT', reject: 'REJECT', deny: 'DROP', masquerade: 'MASQUERADE', redirect: 'REDIRECT', log: 'LOG --log-prefix \'iptables: \' --log-level 7' }.freeze unless defined? TARGET - - def build_firewall_rule(new_resource, ip_version = 'ipv4') - return new_resource.raw.strip if new_resource.raw - - type = new_resource.command - firewall_rule = if new_resource.direction - "#{ip_version} filter #{CHAIN[new_resource.direction.to_sym]} " - else - "#{ip_version} filter FORWARD " - end - firewall_rule << "#{new_resource.position} " - - if [:pre, :post].include?(new_resource.direction) - firewall_rule << '-t nat ' - end - - # Firewalld order of prameters is important here see example output below: - # ipv4 filter INPUT 1 -s 1.2.3.4/32 -d 5.6.7.8/32 -i lo -p tcp -m tcp -m state --state NEW -m comment --comment "hello" -j DROP - firewall_rule << "-s #{ip_with_mask(new_resource, new_resource.source)} " if new_resource.source && new_resource.source != '0.0.0.0/0' - firewall_rule << "-d #{new_resource.destination} " if new_resource.destination - - firewall_rule << "-i #{new_resource.interface} " if new_resource.interface - firewall_rule << "-o #{new_resource.dest_interface} " if new_resource.dest_interface - - firewall_rule << "-p #{new_resource.protocol} " if new_resource.protocol && new_resource.protocol.to_s.to_sym != :none - firewall_rule << '-m tcp ' if new_resource.protocol && new_resource.protocol.to_s.to_sym == :tcp - - # using multiport here allows us to simplify our greps and rule building - firewall_rule << "-m multiport --sports #{port_to_s(new_resource.source_port)} " if new_resource.source_port - firewall_rule << "-m multiport --dports #{port_to_s(dport_calc(new_resource))} " if dport_calc(new_resource) - - firewall_rule << "-m state --state #{new_resource.stateful.is_a?(Array) ? new_resource.stateful.join(',').upcase : new_resource.stateful.to_s.upcase} " if new_resource.stateful - firewall_rule << "-m comment --comment '#{new_resource.description}' " if new_resource.include_comment - firewall_rule << "-j #{TARGET[type]} " - firewall_rule << "--to-ports #{new_resource.redirect_port} " if type == :redirect - firewall_rule.strip! - firewall_rule - end - end - end -end diff --git a/libraries/provider_firewall_firewalld.rb b/libraries/provider_firewall_firewalld.rb deleted file mode 100644 index 1d17ac35..00000000 --- a/libraries/provider_firewall_firewalld.rb +++ /dev/null @@ -1,187 +0,0 @@ -# -# Author:: Ronald Doorn () -# Cookbook:: firewall -# Resource:: default -# -# 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. -# -class Chef - class Provider::FirewallFirewalld < Chef::Provider::LWRPBase - include FirewallCookbook::Helpers::Firewalld - - provides :firewall, os: 'linux', platform_family: %w(rhel fedora amazon) do |node| - (node['platform_version'].to_i >= 7 && !node['firewall']['redhat7_iptables']) || (amazon_linux? && !node['firewall']['redhat7_iptables']) - end - - def whyrun_supported? - false - end - - action :install do - return if disabled?(new_resource) - - firewalld_package = package 'firewalld' do - action :nothing - options new_resource.package_options - end - firewalld_package.run_action(:install) - new_resource.updated_by_last_action(firewalld_package.updated_by_last_action?) - - unless ::File.exist?(firewalld_rules_filename) - rules_file = lookup_or_create_rulesfile - rules_file.content '# created by chef to allow service to start' - rules_file.run_action(:create) - new_resource.updated_by_last_action(rules_file.updated_by_last_action?) - end - - firewalld_service = lookup_or_create_service - [:enable, :start].each do |a| - firewalld_service.run_action(a) - new_resource.updated_by_last_action(firewalld_service.updated_by_last_action?) - end - end - - action :restart do - return if disabled?(new_resource) - - # ensure it's initialized - new_resource.rules({}) unless new_resource.rules - new_resource.rules['firewalld'] = {} unless new_resource.rules['firewalld'] - - # this populates the hash of rules from firewall_rule resources - firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } - firewall_rules.each do |firewall_rule| - next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) - - ip_versions(firewall_rule).each do |ip_version| - if firewall_rule.zone - # Bug introduced in https://github.com/sous-chefs/firewall/pull/206 - # firewalld zones are not compatible with direct rules. Need to - # migrate to firewalld rich rules in order to support configuring a - # rule in a specific zone. - Chef::Log.warn "zone \"#{firewall_rule.zone}\" on firewall_rule \"#{firewall_rule.name}\" is ignored. The zone property exists but has not been implemented internally in the firewall_rule resource." - end - - # build rules to apply with weight - k = "firewall-cmd --direct --add-rule #{build_firewall_rule(firewall_rule, ip_version)}" - v = firewall_rule.position - - # unless we're adding them for the first time.... bail out. - next if new_resource.rules['firewalld'].key?(k) && new_resource.rules['firewalld'][k] == v - new_resource.rules['firewalld'][k] = v - - # If persistent rules is enabled (default) make sure we add a permanent rule at the same time - perm_rules = node && node['firewall'] && node['firewall']['firewalld'] && node['firewall']['firewalld']['permanent'] - if firewall_rule.permanent || perm_rules - k = "firewall-cmd --permanent --direct --add-rule #{build_firewall_rule(firewall_rule, ip_version)}" - new_resource.rules['firewalld'][k] = v - end - end - end - - # ensure a file resource exists with the current firewalld rules - rules_file = lookup_or_create_rulesfile - rules_file.content build_rule_file(new_resource.rules['firewalld']) - rules_file.run_action(:create) - - # ensure the service is running without waiting. - firewalld_service = lookup_or_create_service - [:enable, :start].each do |a| - firewalld_service.run_action(a) - new_resource.updated_by_last_action(firewalld_service.updated_by_last_action?) - end - - # mark updated if we changed the zone - unless firewalld_default_zone?(new_resource.enabled_zone) - firewalld_default_zone!(new_resource.enabled_zone) - new_resource.updated_by_last_action(true) - end - - # if the file was changed, load new ruleset - return unless rules_file.updated_by_last_action? - firewalld_flush! - # TODO: support logging - - new_resource.rules['firewalld'].sort_by { |_k, v| v }.map { |k, _v| k }.each do |cmd| - firewalld_rule!(cmd) - end - - new_resource.updated_by_last_action(true) - end - - action :disable do - return if disabled?(new_resource) - - if firewalld_active? - firewalld_flush! - firewalld_default_zone!(new_resource.disabled_zone) - new_resource.updated_by_last_action(true) - end - - # ensure the service is stopped without waiting. - firewalld_service = lookup_or_create_service - [:disable, :stop].each do |a| - firewalld_service.run_action(a) - new_resource.updated_by_last_action(firewalld_service.updated_by_last_action?) - end - - rules_file = lookup_or_create_rulesfile - rules_file.content '# created by chef to allow service to start' - rules_file.run_action(:create) - new_resource.updated_by_last_action(rules_file.updated_by_last_action?) - end - - action :flush do - return if disabled?(new_resource) - return unless firewalld_active? - - firewalld_flush! - new_resource.updated_by_last_action(true) - - rules_file = lookup_or_create_rulesfile - rules_file.content '# created by chef to allow service to start' - rules_file.run_action(:create) - new_resource.updated_by_last_action(rules_file.updated_by_last_action?) - end - - action :save do - return if disabled?(new_resource) - return if firewalld_all_rules_permanent! - - firewalld_save! - new_resource.updated_by_last_action(true) - end - - def lookup_or_create_service - begin - firewalld_service = Chef.run_context.resource_collection.find(service: 'firewalld') - rescue - firewalld_service = service 'firewalld' do - action :nothing - end - end - firewalld_service - end - - def lookup_or_create_rulesfile - begin - firewalld_file = Chef.run_context.resource_collection.find(file: firewalld_rules_filename) - rescue - firewalld_file = file firewalld_rules_filename do - action :nothing - end - end - firewalld_file - end - end -end diff --git a/libraries/resource_firewall.rb b/libraries/resource_firewall.rb index ff576611..d59c2b7a 100644 --- a/libraries/resource_firewall.rb +++ b/libraries/resource_firewall.rb @@ -2,7 +2,7 @@ class Chef class Resource::Firewall < Chef::Resource::LWRPBase resource_name(:firewall) provides(:firewall) - actions(:install, :restart, :disable, :flush, :save) + actions(:install, :restart, :disable, :flush) default_action(:install) attribute(:enabled, kind_of: [TrueClass, FalseClass], default: true) @@ -10,10 +10,6 @@ class Resource::Firewall < Chef::Resource::LWRPBase attribute(:log_level, kind_of: Symbol, equal_to: [:low, :medium, :high, :full, :off], default: :low) attribute(:rules, kind_of: Hash) - # for firewalld, specify the zone when firewall is disable and enabled - attribute(:disabled_zone, kind_of: Symbol, default: :public) - attribute(:enabled_zone, kind_of: Symbol, default: :drop) - # for firewall implementations where ipv6 can be skipped (currently iptables-specific) attribute(:ipv6_enabled, kind_of: [TrueClass, FalseClass], default: true) diff --git a/recipes/default.rb b/recipes/default.rb index f9927946..5c393a5b 100644 --- a/recipes/default.rb +++ b/recipes/default.rb @@ -17,27 +17,12 @@ # limitations under the License. # -firewall 'default' do - ipv6_enabled node['firewall']['ipv6_enabled'] - action :install -end - # create a variable to use as a condition on some rules that follow -iptables_firewall = rhel? || amazon_linux? || node['firewall']['ubuntu_iptables'] - -firewall_rule 'allow loopback' do - interface 'lo' - protocol :none - command :allow - only_if { linux? && node['firewall']['allow_loopback'] } -end +iptables_firewall = !platform_family?('rhel', 'amazon') && node['firewall']['ubuntu_iptables'] -firewall_rule 'allow icmp' do - protocol :icmp - command :allow - # debian ufw doesn't allow 'icmp' protocol, but does open - # icmp by default, so we skip it in default recipe - only_if { iptables_firewall && node['firewall']['allow_icmp'] } +firewall 'default' do + ipv6_enabled node['firewall']['ipv6_enabled'] if iptables_firewall + action :install end firewall_rule 'allow world to ssh' do @@ -59,6 +44,31 @@ only_if { linux? && node['firewall']['allow_mosh'] } end +return unless iptables_firewall + +# iptables only rules below + +# TODO: These should probably just be removed. They are a bit deceiving because +# if one sets `node['firewall']['allow_icmp'] = false`, one would expect that to +# mean disable/remove the rule but really it means "ignore", which leaves the +# port open instead of closing it. + +firewall_rule 'allow loopback' do + interface 'lo' + protocol :none + command :allow + # Modern firewalls allow loopback by default, limit to just iptables + only_if { iptables_firewall && node['firewall']['allow_loopback'] } +end + +firewall_rule 'allow icmp' do + protocol :icmp + command :allow + # debian ufw doesn't allow 'icmp' protocol, but does open + # icmp by default, so we skip it in default recipe + only_if { iptables_firewall && node['firewall']['allow_icmp'] } +end + # allow established connections, ufw defaults to this but iptables does not firewall_rule 'established' do stateful [:related, :established] diff --git a/recipes/firewalld.rb b/recipes/firewalld.rb deleted file mode 100644 index 59af563a..00000000 --- a/recipes/firewalld.rb +++ /dev/null @@ -1,87 +0,0 @@ -# -# Cookbook:: firewall -# Recipe:: firewalld -# -# Copyright:: 2011-2016, Chef Software, Inc. -# -# 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. -# - -chef_sugar_cookbook_version = Gem::Version.new(run_context.cookbook_collection['chef-sugar'].metadata.version) - -include_recipe 'chef-sugar' if chef_sugar_cookbook_version < Gem::Version.new('4.0.0') - -firewall 'default' do - ipv6_enabled node['firewall']['ipv6_enabled'] - enabled_zone node['firewall']['firewalld']['zone'].to_sym - action :install -end - -# create a variable to use as a condition on some rules that follow -iptables_firewall = rhel? || node['firewall']['ubuntu_iptables'] - -firewall_rule 'allow loopback' do - interface 'lo' - protocol :none - command :allow - zone node['firewall']['firewalld']['loopback_zone'] - only_if { linux? && node['firewall']['allow_loopback'] } -end - -firewall_rule 'allow icmp' do - protocol :icmp - command :allow - zone node['firewall']['firewalld']['icmp_zone'] - # debian ufw doesn't allow 'icmp' protocol, but does open - # icmp by default, so we skip it in default recipe - only_if { (!debian?(node) || iptables_firewall) && node['firewall']['allow_icmp'] } -end - -firewall_rule 'allow world to ssh' do - port 22 - source '0.0.0.0/0' - zone node['firewall']['firewalld']['ssh_zone'] - only_if { linux? && node['firewall']['allow_ssh'] } -end - -firewall_rule 'allow world to winrm' do - port 5989 - source '0.0.0.0/0' - only_if { windows? && node['firewall']['allow_winrm'] } -end - -firewall_rule 'allow world to mosh' do - protocol :udp - port 60000..61000 - source '0.0.0.0/0' - zone node['firewall']['firewalld']['mosh_zone'] - only_if { linux? && node['firewall']['allow_mosh'] } -end - -# allow established connections, ufw defaults to this but iptables does not -firewall_rule 'established' do - stateful [:related, :established] - protocol :none # explicitly don't specify protocol - command :allow - zone node['firewall']['firewalld']['established_zone'] - only_if { node['firewall']['allow_established'] && iptables_firewall } -end - -# ipv6 needs ICMP to reliably work, so ensure it's enabled if ipv6 -# allow established connections, ufw defaults to this but iptables does not -firewall_rule 'ipv6_icmp' do - protocol :'ipv6-icmp' - command :allow - zone node['firewall']['firewalld']['icmp_zone'] - only_if { node['firewall']['ipv6_enabled'] && node['firewall']['allow_established'] && iptables_firewall } -end diff --git a/resources/_partial/_firewall.rb b/resources/_partial/_firewall.rb new file mode 100644 index 00000000..d43c27ba --- /dev/null +++ b/resources/_partial/_firewall.rb @@ -0,0 +1,8 @@ + +unified_mode true + +default_action :install + +property :package_options, + String, + description: 'Pass additional options to the package manager when installing the firewall.' diff --git a/resources/_partial/_firewall_rule.rb b/resources/_partial/_firewall_rule.rb new file mode 100644 index 00000000..2325504b --- /dev/null +++ b/resources/_partial/_firewall_rule.rb @@ -0,0 +1,24 @@ + +require 'ipaddr' + +unified_mode true + +default_action :create + +property :firewall_name, String, default: 'default' +property :command, Symbol, equal_to: [:reject, :allow, :deny, :masquerade, :redirect, :log], default: :allow +property :protocol, [Integer, Symbol], default: :tcp, + callbacks: { + 'must be either :tcp, :udp, :icmp, :\'ipv6-icmp\', :icmpv6, :none, or a valid IP protocol number' => + lambda do |p| + !!(p.to_s =~ /(udp|tcp|icmp|icmpv6|ipv6-icmp|esp|ah|ipv6|none)/ || (p.to_s =~ /^\d+$/ && p.between?(0, 142))) + end } + +property :source, String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } } +property :source_port, [Integer, Array, Range] +property :port, [Integer, Array, Range] # shorthand for :dest_port +property :dest_port, [Integer, Array, Range] +property :destination, String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } } +property :position, Integer, default: 50 +property :description, String, name_property: true +property :redirect_port, Integer diff --git a/resources/firewall_firewalld.rb b/resources/firewall_firewalld.rb new file mode 100644 index 00000000..7f5b8e8d --- /dev/null +++ b/resources/firewall_firewalld.rb @@ -0,0 +1,35 @@ + +unified_mode true + +# Common properties defined in a resource partial +use '_partial/_firewall' + +# firewalld platforms only +provides :firewall, os: 'linux', platform_family: %w(rhel fedora amazon) do |node| + (node['platform_version'].to_i >= 7 && !node['firewall']['redhat7_iptables']) || (amazon_linux? && !node['firewall']['redhat7_iptables']) +end + +action :install do + firewalld new_resource.name do + package_options new_resource.package_options if property_is_set?(:package_options) + action :install + end +end + +action :reload do + firewalld new_resource.name do + action :reload + end +end + +action :restart do + firewalld new_resource.name do + action :restart + end +end + +action :disable do + firewalld new_resource.name do + action :disable + end +end diff --git a/resources/firewall_rule.rb b/resources/firewall_rule.rb index 4a7fc29b..6a80620a 100644 --- a/resources/firewall_rule.rb +++ b/resources/firewall_rule.rb @@ -1,41 +1,21 @@ -require 'ipaddr' - unified_mode true -provides :firewall_rule - -default_action :create +# Common properties defined in a resource partial +use '_partial/_firewall_rule' -property :firewall_name, String, default: 'default' -property :command, Symbol, equal_to: [:reject, :allow, :deny, :masquerade, :redirect, :log], default: :allow -property :protocol, [Integer, Symbol], default: :tcp, - callbacks: { - 'must be either :tcp, :udp, :icmp, :\'ipv6-icmp\', :icmpv6, :none, or a valid IP protocol number' => - lambda do |p| - !!(p.to_s =~ /(udp|tcp|icmp|icmpv6|ipv6-icmp|esp|ah|ipv6|none)/ || (p.to_s =~ /^\d+$/ && p.between?(0, 142))) - end } +# non-firewalld platforms only +provides :firewall_rule do + !platform_family?('rhel', 'fedora', 'amazon') +end property :direction, Symbol, equal_to: [:in, :out, :pre, :post], default: :in property :logging, Symbol, equal_to: [:connections, :packets] -property :source, String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } } -property :source_port, [Integer, Array, Range] property :interface, String -property :port, [Integer, Array, Range] -property :destination, String, callbacks: { 'must be a valid ip address' => ->(ip) { !!IPAddr.new(ip) } } -property :dest_port, [Integer, 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 -# only used for firewalld -property :permanent, [true, false], default: false - -property :zone, String - # only used for Windows Firewalls property :program, String property :service, String diff --git a/resources/firewall_rule_firewalld.rb b/resources/firewall_rule_firewalld.rb new file mode 100644 index 00000000..d6d3aa0d --- /dev/null +++ b/resources/firewall_rule_firewalld.rb @@ -0,0 +1,115 @@ + +unified_mode true + +# Common properties defined in a resource partial +use '_partial/_firewall_rule' + +# firewalld platforms only +provides :firewall_rule, os: 'linux', platform_family: %w(rhel fedora amazon) do |node| + (node['platform_version'].to_i >= 7 && !node['firewall']['redhat7_iptables']) || (amazon_linux? && !node['firewall']['redhat7_iptables']) +end + +# Additional firewalld-only properties +property :zone, + String, + description: "Zone in which to apply the rule. If not specified, the rule will be applied to the systems's default zone." + +action :create do + if property_is_set?(:port) && property_is_set?(:dest_port) + raise 'The "port" property is a shorthand for "dest_port" and cannot be set together with "dest_port". Please set only one of them.' + end + + if new_resource.command == :masquerade + firewalld_rich_rule new_resource.description do + masquerade true + end + + return + end + + if new_resource.command == :redirect + # An iptables REDIRECT is just a DNAT shorthand to the IP of the incoming + # interface, i.e. the destination address is implied + firewalld_rich_rule new_resource.description do + forward_port new_resource.source_port + to_port new_resource.redirect_port + protocol new_resource.protocol.to_s + end + + return + end + + rule_action_map = { + reject: :reject, + allow: :accept, + deny: :drop, + } + + protocol_required = property_is_set?(:protocol) || property_is_set?(:port) | property_is_set?(:source_port) + array_property = check_for_port_array_property(new_resource) + + if array_property + # firewalld doesn't support port arrays, so we must loop over the array + # items in order to create distinct rules for each port in the array. + + new_resource.send(array_property).each do |array_item| + array_item = format_port(array_item) + + firewalld_rich_rule "#{new_resource.description} [#{array_item}/#{new_resource.protocol}]" do + if [:source_port].include?(array_property) + source_port array_item + port new_resource.port if new_resource.property_is_set?(:port) # Shorthand, same as :dest_port + port new_resource.dest_port if new_resource.property_is_set?(:dest_port) + elsif [:port, :dest_port].include?(array_property) + port array_item + source_port new_resource.source_port if new_resource.property_is_set?(:source_port) + end + + # The rest of the properties as usual + zone new_resource.zone if new_resource.property_is_set?(:zone) + source new_resource.source if new_resource.property_is_set?(:source) + protocol new_resource.protocol.to_s if protocol_required + destination new_resource.destination if new_resource.property_is_set?(:destination) + priority new_resource.position if new_resource.property_is_set?(:position) + rule_action rule_action_map[new_resource.command] if rule_action_map.keys.include?(new_resource.command) + log true if new_resource.command == :log + action :add + end + end + + return + end + + firewalld_rich_rule new_resource.description do + zone new_resource.zone if new_resource.property_is_set?(:zone) + source new_resource.source if new_resource.property_is_set?(:source) + source_port format_port(new_resource.source_port) if new_resource.property_is_set?(:source_port) + port format_port(new_resource.port) if new_resource.property_is_set?(:port) # Shorthand, same as :dest_port + port format_port(new_resource.dest_port) if new_resource.property_is_set?(:dest_port) + protocol new_resource.protocol.to_s if protocol_required + destination new_resource.destination if new_resource.property_is_set?(:destination) + priority new_resource.position if new_resource.property_is_set?(:position) + rule_action rule_action_map[new_resource.command] if rule_action_map.keys.include?(new_resource.command) + log true if new_resource.command == :log + action :add + end +end + +action_class do + def check_for_port_array_property(new_resource) + array_properties = [:source_port, :port, :dest_port].select do |property| + new_resource.property_is_set?(property) && new_resource.send(property).is_a?(Array) + end + + if array_properties.size > 1 + raise 'Only one of source_port, port, or dest_port can be an Array at a time.' + end + + array_properties.first # Return the property that is an Array + end + + def format_port(value) + # Convert the Ruby range to a firewalld port range, e.g. "1-100" + value.is_a?(Range) ? "#{value.min}-#{value.max}" : value + end +end diff --git a/resources/firewalld.rb b/resources/firewalld.rb index 88661730..12158802 100644 --- a/resources/firewalld.rb +++ b/resources/firewalld.rb @@ -3,11 +3,15 @@ provides :firewalld, os: 'linux' +# Custom options to pass to the package manager during install of firewalld package +property :package_options, String + action :install do chef_gem 'ruby-dbus' require 'dbus' package 'firewalld' do + options new_resource.package_options if new_resource.package_options action :install end diff --git a/test/fixtures/cookbooks/firewall-test/recipes/default.rb b/test/fixtures/cookbooks/firewall-test/recipes/default.rb index 23744770..28379073 100644 --- a/test/fixtures/cookbooks/firewall-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewall-test/recipes/default.rb @@ -1,3 +1,8 @@ + +firewalld = rhel? || amazon_linux? +iptables = !firewalld && node['firewall']['ubuntu_iptables'] +ufw = !firewalld && !iptables + include_recipe 'firewall' firewall_rule 'ssh22' do @@ -35,7 +40,7 @@ firewall_rule 'protocolnum' do protocol 112 command :allow - only_if { rhel? || amazon_linux? || node['firewall']['ubuntu_iptables'] } # debian ufw doesn't support protocol numbers + not_if { ufw } # debian ufw doesn't support protocol numbers end firewall_rule 'prepend' do @@ -59,52 +64,52 @@ firewall_rule 'range' do port 1000..1100 command :allow - - # centos 5 is broken for ipv6 ranges - # see https://github.com/chef-cookbooks/firewall/pull/111#issuecomment-163520156 - not_if { rhel? && node['platform_version'].to_f < 6.0 } end firewall_rule 'array' do port [1234, 5000..5100, '5678'] command :allow - - # centos 5 is broken for ipv6 ranges - # see https://github.com/chef-cookbooks/firewall/pull/111#issuecomment-163520156 - not_if { rhel? && node['platform_version'].to_f < 6.0 } -end - -# if using with iptables-restart, this produces an unreadable line; no problem, IF disabled -firewall_rule 'ufw raw test' do - raw 'allow from 192.168.1.1 to 192.168.2.1 port 25 proto tcp' - only_if { platform_family?('debian') && !node['firewall']['ubuntu_iptables'] } -end - -firewall_rule 'RPC Port Range In' do - port 5000..5100 - protocol :tcp - command :allow - direction :in - - # centos 5 is broken for ipv6 ranges - # see https://github.com/chef-cookbooks/firewall/pull/111#issuecomment-163520156 - not_if { rhel? && node['platform_version'].to_f < 6.0 } -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 +if firewalld + firewall_rule 'allow 5672 in internal zone' do + zone 'internal' + port 5672 + end +end + +if ufw + firewall_rule 'ufw raw test' do + raw 'allow from 192.168.1.1 to 192.168.2.1 port 25 proto tcp' + end +end + +if iptables + firewall_rule 'RPC Port Range In' do + port 5000..5100 + protocol :tcp + command :allow + direction :in + + # # centos 5 is broken for ipv6 ranges + # # see https://github.com/chef-cookbooks/firewall/pull/111#issuecomment-163520156 + # not_if { rhel? && node['platform_version'].to_f < 6.0 } + 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 end include_recipe 'firewall-test::windows' if windows? diff --git a/test/integration/default/inspec/default_spec.rb b/test/integration/default/inspec/default_spec.rb index aea10f2d..68ea1c83 100644 --- a/test/integration/default/inspec/default_spec.rb +++ b/test/integration/default/inspec/default_spec.rb @@ -18,7 +18,7 @@ describe command('ufw status 2>&1') do its(:stdout) { should match(/Status: active/) } end -end if os.debian? +end if ufw? describe command('netsh advfirewall show currentprofile firewallpolicy | findstr "Firewall Policy"') do its(:stdout) { should match('BlockInbound,AllowOutbound') } diff --git a/test/integration/firewalld/inspec/firewalld_spec.rb b/test/integration/firewalld/inspec/firewalld_spec.rb index a6121f09..9668b343 100644 --- a/test/integration/firewalld/inspec/firewalld_spec.rb +++ b/test/integration/firewalld/inspec/firewalld_spec.rb @@ -1,43 +1,24 @@ # these tests only for redhat with iptables expected_rules = [ - /ipv4 filter INPUT 50 -i lo -m comment --comment 'allow loopback' -j ACCEPT/, - /ipv4 filter INPUT 50 -p icmp -m comment --comment 'allow icmp' -j ACCEPT/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 22 -m comment --comment 'allow world to ssh' -j ACCEPT/, - /ipv4 filter INPUT 50 -m state --state RELATED,ESTABLISHED -m comment --comment established -j ACCEPT/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 22 -m comment --comment ssh22 -j ACCEPT/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 2200,2222 -m comment --comment ssh2222 -j ACCEPT/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1234 -m comment --comment temp1 -j DROP/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1235 -m comment --comment temp2 -j REJECT/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1236 -m comment --comment addremove -j ACCEPT/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1236 -m comment --comment addremove2 -j DROP/, - /ipv4 filter INPUT 50 -p 112 -m comment --comment protocolnum -j ACCEPT/, - %r{ipv4 filter INPUT 49 -s 192.168.99.99/32 -p tcp -m tcp -m comment --comment block-192.168.99.99 -j REJECT}, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1000:1100 -m comment --comment range -j ACCEPT/, - /ipv4 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1234,5000:5100,5678 -m comment --comment array -j ACCEPT/, + /rule port port="22" protocol="tcp" accept/, + /rule port port="2222" protocol="tcp" accept/, + /rule port port="2200" protocol="tcp" accept/, + /rule port port="1234" protocol="tcp" accept/, + /rule port port="1235" protocol="tcp" reject/, + /rule port port="1236" protocol="tcp" drop/, + /rule protocol value="112" accept/, + /rule priority="5" port port="7788" protocol="tcp" accept/, + /rule priority="49" family="ipv4" source address="192.168.99.99" reject/, + /rule port port="1000-1100" protocol="tcp" accept/, + /rule port port="5000-5100" protocol="tcp" accept/, + /rule port port="5678" protocol="tcp" accept/, + %r{rule family="ipv4" source address="0.0.0.0/0" port port="60000-61000" protocol="udp" accept}, + %r{rule family="ipv4" source address="0.0.0.0/0" port port="22" protocol="tcp" accept}, # ipv6 - /ipv6 filter INPUT 50 -i lo -m comment --comment 'allow loopback' -j ACCEPT/, - /ipv6 filter INPUT 50 -p icmp -m comment --comment 'allow icmp' -j ACCEPT/, - /ipv6 filter INPUT 50 -m state --state RELATED,ESTABLISHED -m comment --comment established -j ACCEPT/, - /ipv6 filter INPUT 50 -p ipv6-icmp -m comment --comment ipv6_icmp -j ACCEPT/, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 22 -m comment --comment ssh22 -j ACCEPT/, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 2200,2222 -m comment --comment ssh2222 -j ACCEPT/, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1234 -m comment --comment temp1 -j DROP/, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1235 -m comment --comment temp2 -j REJECT/, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1236 -m comment --comment addremove -j ACCEPT/, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1236 -m comment --comment addremove2 -j DROP/, - /ipv6 filter INPUT 50 -p 112 -m comment --comment protocolnum -j ACCEPT/, - %r{ipv6 filter INPUT 50 -s 2001:db8::ff00:42:8329/128 -p tcp -m tcp -m multiport --dports 80 -m comment --comment ipv6-source -j ACCEPT}, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1000:1100 -m comment --comment range -j ACCEPT/, - /ipv6 filter INPUT 50 -p tcp -m tcp -m multiport --dports 1234,5000:5100,5678 -m comment --comment array -j ACCEPT/, + /rule family="ipv6" source address="2001:db8::ff00:42:8329" port port="80" protocol="tcp" accept/, ] -describe command('firewall-cmd --permanent --direct --get-all-rules') do - expected_rules.each do |r| - its(:stdout) { should match(r) } - end -end - -describe command('firewall-cmd --direct --get-all-rules') do +describe command('firewall-cmd --list-rich-rules') do expected_rules.each do |r| its(:stdout) { should match(r) } end @@ -48,3 +29,8 @@ it { should be_enabled } it { should be_running } end + +# Check for rules not in the default zone +describe command('firewall-cmd --list-rich-rules --zone=internal') do + its(:stdout) { should match /rule port port="5672" protocol="tcp" accept/ } +end diff --git a/test/integration/helpers/spec_helper.rb b/test/integration/helpers/spec_helper.rb index 76b41684..bbf70d75 100644 --- a/test/integration/helpers/spec_helper.rb +++ b/test/integration/helpers/spec_helper.rb @@ -1,8 +1,12 @@ # helpers def firewalld? - (os.redhat? && os[:release].to_i == 7) || os.name == 'amazon' + %w(redhat suse).include?(os.family) +end + +def ufw? + os.debian? end def iptables? - (os.redhat? && (os[:release].to_i < 7 || os[:release].to_i >= 8)) && os.name != 'amazon' + !firewalld? && !ufw? end From 4bff6ac90f26e2eb7b502202ff3c6c3531401f37 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Mon, 30 Dec 2024 14:10:38 -0800 Subject: [PATCH 19/24] Fixed: ufw provider doesn't ensure ufw service is enabled Signed-off-by: Joseph Larionov --- CHANGELOG.md | 1 + libraries/provider_firewall_ufw.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48169dc5..eb315e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This file is used to list changes made in each version of the firewall cookbook. - Fixed: New zones created by `firewalld_zone` unexpectedly have forwarding enabled by default. - Fixed: `firewalld_*` resources ignore properties whose value is `false`. - Fixed: `firewalld_*` resources were not idempotent when using `ports`, `source_ports`, and `rich_rules` properties. +- Fixed: `ufw` provider doesn't ensure `ufw` service is enabled. ### Removed diff --git a/libraries/provider_firewall_ufw.rb b/libraries/provider_firewall_ufw.rb index 1c8ed50d..07a0017e 100644 --- a/libraries/provider_firewall_ufw.rb +++ b/libraries/provider_firewall_ufw.rb @@ -56,6 +56,10 @@ def whyrun_supported? ufw_file.run_action(:create) new_resource.updated_by_last_action(true) if ufw_file.updated_by_last_action? + + ufw_service = lookup_or_create_service('ufw') + ufw_service.run_action(:enable) + new_resource.updated_by_last_action(true) if ufw_service.updated_by_last_action? end action :restart do @@ -124,6 +128,17 @@ def whyrun_supported? new_resource.updated_by_last_action(true) if ufw_file.updated_by_last_action? end + def lookup_or_create_service(name) + begin + ufw_service = Chef.run_context.resource_collection.find(service: svc) + rescue + ufw_service = service name do + action :nothing + end + end + ufw_service + end + def lookup_or_create_rulesfile begin ufw_file = Chef.run_context.resource_collection.find(file: ufw_rules_filename) From 459bb2a6a8a72c99e3c3ae3c199e65aa3ecd6ff3 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Mon, 30 Dec 2024 17:00:29 -0800 Subject: [PATCH 20/24] Allow any compatible firewall solution on Linux platforms Signed-off-by: Joseph Larionov --- CHANGELOG.md | 2 + README.md | 100 +++++---- attributes/default.rb | 9 + attributes/iptables.rb | 2 - kitchen.dokken.yml | 21 +- kitchen.yml | 45 ++-- libraries/provider_firewall_iptables.rb | 4 +- .../provider_firewall_iptables_ubuntu.rb | 5 +- .../provider_firewall_iptables_ubuntu1404.rb | 200 ------------------ libraries/provider_firewall_ufw.rb | 4 +- recipes/default.rb | 2 +- resources/firewall_firewalld.rb | 4 +- resources/firewall_rule.rb | 4 +- resources/firewall_rule_firewalld.rb | 4 +- .../firewall-test/recipes/default.rb | 37 +++- .../firewalld-test/recipes/default.rb | 9 + .../firewalld/inspec/firewalld_spec.rb | 2 +- test/integration/helpers/spec_helper.rb | 2 +- .../iptables/inspec/iptables_spec.rb | 39 ++-- test/integration/ufw/inspec/ufw_spec.rb | 2 +- 20 files changed, 167 insertions(+), 330 deletions(-) delete mode 100644 libraries/provider_firewall_iptables_ubuntu1404.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index eb315e4d..fae0f526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This file is used to list changes made in each version of the firewall cookbook. - `priority`, `ingress_priority`, `egress_priority` properties added to `firewalld_zone`. - Added `firewalld_rich_rule` resource for adding/removing rich rules to/from firewalld zones. - Support for IPv6 rules on firewalld platforms. +- Support for using any compatible firewall solution on any platform. Defaults to the operating system's default firewall solution. ### Changed @@ -32,6 +33,7 @@ This file is used to list changes made in each version of the firewall cookbook. - Removed firewalld property `permanent` from `firewall_rule` resource. Firewalld rules are now always added permanently. - Removed properties `disabled_zone` and `enabled_zone` from `firewall` resource. Use the `firewalld_zone` resource to manage firewalld zone configuration. - Removed recipe `firewall::firewalld`. Its functionality has been merged into the `firewall::default` recipe. +- Removed attributes `default['firewall']['ubuntu_iptables']` and `default['firewall']['redhat7_iptables']`. Use the new `default['firewall']['solution']` attribute to set the desired firewall solution to use. ## 6.3.9 - *2024-12-05* diff --git a/README.md b/README.md index ad2ae9df..515304c1 100644 --- a/README.md +++ b/README.md @@ -22,44 +22,55 @@ This cookbook is maintained by the Sous Chefs. The Sous Chefs are a community of depends 'firewall' ``` -### Supported firewalls and platforms - -- UFW - Ubuntu, Debian (except 9) -- IPTables - Red Hat & CentOS, Ubuntu -- FirewallD - Red Hat & CentOS >= 7.0 -- Windows Advanced Firewall - 2012 R2 -- nftables - -Tested on: - -- Ubuntu 16.04 with iptables, ufw -- Debian 9 with iptables -- Debian 11 with nftables -- Debian 11 with new resources for firewalld -- CentOS 6 with iptables -- CentOS 7.1 with firewalld -- Oracle 8 with nftables -- Windows Server 2012r2 with Windows Advanced Firewall - -By default, Ubuntu chooses ufw. To switch to iptables, set this in an attribute file: +## Supported firewalls and platforms + +- [UFW (Uncomplicated Firewall)](https://help.ubuntu.com/community/UFW) +- [Firewalld](https://firewalld.org/) +- [IPTables](https://manpages.debian.org/stable/iptables/iptables.8.en.html) +- [Windows Firewall](https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/) +- [nftables](https://wiki.nftables.org/wiki-nftables/index.php/Main_Page) + +The default firewall solution used on Linux is based on the platform family: + +| Platform Family | Default Firewall Solution | +|-----------------|---------------------------| +| `amazon` | firewalld | +| `debian` | ufw | +| `fedora` | firewalld | +| `rhel` | firewalld | +| `suse` | firewalld | +| `ubuntu` | ufw | +| `windows` | windows | +| Other | iptables | + +If you'd like to use a firewall solution other than the platform's default, set the `default['firewall']['solution']` +attribute to the desired firewall: ```ruby -default['firewall']['ubuntu_iptables'] = true -``` +# firewalld +default['firewall']['solution'] = 'firewalld' -By default, Red Hat & CentOS >= 7.0 chooses firewalld. To switch to iptables, set this in an attribute file: +# iptables +default['firewall']['solution'] = 'iptables' -```ruby -default['firewall']['redhat7_iptables'] = true +# ufw +default['firewall']['solution'] = 'ufw' ``` +### nftables + In order to use nftables, just use the resource `nftables` and `nftables_rule`. These resources are written in more modern design styles and are not configurable by node attributes. +### Supported operating systems + +See the [kitchen.yml](https://github.com/sous-chefs/firewall/blob/main/kitchen.yml) for the full matrix of platforms +this cookbook is tested on. + ## Quickstart -To use this cookbook to open a port in the system's default firewall: +To simply open a port in the system's default firewall: ```ruby include_recipe 'firewall' @@ -132,7 +143,7 @@ firewall rules. On firewalld systems it adds rules to the default zone as firewa rules](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html). See the [`firewall_rule`](#firewall_rule) section for examples. -For more advanced firewalld configuration, see the documentation for the various [`firewalld` resources](documentation/README.md). +See the [`firewalld` resources](documentation/README.md) documentation for advanced firewalld configuration. ## Recipes @@ -146,13 +157,12 @@ Used to disable platform specific firewall. Many clouds have their own firewall ## Attributes +- `default['firewall']['solution'] = `, sets the firewall solution to use on Linux platforms. Defaults to the default firewall solution used by the platform family. See [Supported firewalls and platforms](#supported-firewalls-and-platforms) for more info. - `default['firewall']['allow_ssh'] = false`, set true to open port 22 for SSH when the default recipe runs - `default['firewall']['allow_mosh'] = false`, set to true to open UDP ports 60000 - 61000 for [Mosh][0] when the default recipe runs - `default['firewall']['allow_winrm'] = false`, set true to open port 5989 for WinRM when the default recipe runs - `default['firewall']['allow_loopback'] = false`, set to true to allow all traffic on the loopback interface - `default['firewall']['allow_icmp'] = false`, set true to allow icmp protocol on supported OSes (note: ufw and windows implementations don't support this) -- `default['firewall']['ubuntu_iptables'] = false`, set to true to use iptables on Ubuntu / Debian when using the default recipe -- `default['firewall']['redhat7_iptables'] = false`, set to true to use iptables on Red Hat / CentOS 7 when using the default recipe - `default['firewall']['ufw']['defaults']` hash for template `/etc/default/ufw` - `default['firewall']['iptables']['defaults']` hash for default policies for 'filter' table's chains` - `default['firewall']['windows']['defaults']` hash to define inbound / outbound firewall policy on Windows platform @@ -207,8 +217,6 @@ end #### Properties -The full syntax for all properties and which platforms they're available for: - ```ruby firewall_rule 'name' do firewall_name String # Default: 'default' @@ -222,23 +230,23 @@ firewall_rule 'name' do position Integer # Default: 50 description String # Default: 'name' unless specified - # Platform-specific properties - zone String # Platforms: firewalld - logging Symbol # platforms: ufw - redirect_port Integer # platforms: iptables, firewalld - dest_interface String # platforms: iptables, windows - interface String # platforms: iptables, ufw, windows - include_comment true, false # platforms: iptables, ufw. Default: true - stateful Symbol, Array # platforms: iptables, ufw - raw String # platforms: iptables, ufw - direction Symbol # Platforms: iptables, ufw, windows. Default: :in - notify_firewall true, false # platforms: iptables, ufw, windows. Default: true - program String # platforms: windows - service String # platforms: windows + # Firewall-specific properties + zone String # Firewall: firewalld + logging Symbol # Firewall: ufw + redirect_port Integer # Firewall: iptables, firewalld + dest_interface String # Firewall: iptables, windows + interface String # Firewall: iptables, ufw, windows + include_comment true, false # Firewall: iptables, ufw. Default: true + stateful Symbol, Array # Firewall: iptables, ufw + raw String # Firewall: iptables, ufw + direction Symbol # Firewall: iptables, ufw, windows. Default: :in + notify_firewall true, false # Firewall: iptables, ufw, windows. Default: true + program String # Firewall: windows + service String # Firewall: windows end ``` -Platform-agnostic properties that can be used with `firewall_rule` on any firewall system: +Firewall-agnostic properties that can be used with `firewall_rule` on any firewall system: - `firewall_name`: the matching firewall resource that this rule applies to. Default value: `default` - `description` (*default: same as rule name*): Used to provide a comment that will be included when adding the firewall rule. @@ -258,7 +266,7 @@ Platform-agnostic properties that can be used with `firewall_rule` on any firewa - `destination`: ip address or subnet to filter on packet destination, must be a valid IP - `position` (*default: 50*): **relative** position to insert rule at. Position may be any integer between 0 < n < 100 (exclusive), and more than one rule may specify the same position. -Additional properties for advanced firewall rules that tied to specific firewall solutions. **Note: These properties are _not_ platform-agnostic, so you must ensure they are used only on the appropriate platforms**: +Additional properties for advanced firewall rules that tied to specific firewall solutions. **Note: These properties are _not_ firewall-agnostic, so you must ensure they are used only on the appropriate firewall solutions**: - `zone`: (*firewalld*), a string, such as `public` that the rule will be applied. Defaults to the system's configured default zone. diff --git a/attributes/default.rb b/attributes/default.rb index 7f72dcf0..8cf81b4f 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -3,3 +3,12 @@ default['firewall']['allow_mosh'] = false default['firewall']['allow_loopback'] = false default['firewall']['allow_icmp'] = false + +default['firewall']['solution'] = { + 'debian' => 'ufw', + 'amazon' => 'firewalld', + 'fedora' => 'firewalld', + 'rhel' => 'firewalld', + 'suse' => 'firewalld', + 'windows' => 'windows', +}.fetch(node['platform_family'], 'iptables') diff --git a/attributes/iptables.rb b/attributes/iptables.rb index 551916a3..a4b36c68 100644 --- a/attributes/iptables.rb +++ b/attributes/iptables.rb @@ -11,7 +11,5 @@ 'COMMIT_FILTER' => 100, } -default['firewall']['ubuntu_iptables'] = false -default['firewall']['redhat7_iptables'] = false default['firewall']['allow_established'] = true default['firewall']['ipv6_enabled'] = true diff --git a/kitchen.dokken.yml b/kitchen.dokken.yml index cb194450..34ecb6b0 100644 --- a/kitchen.dokken.yml +++ b/kitchen.dokken.yml @@ -52,18 +52,15 @@ platforms: image: dokken/debian-12 pid_one_command: /bin/systemd - # TODO: Fix. Resources uses dnf when it should use yum - # - name: fedora-latest - # driver: - # image: dokken/fedora-latest - # pid_one_command: /usr/lib/systemd/systemd - # intermediate_instructions: *firewalld_fix - - # # TODO: Add support for openSUSE in resource - # - name: opensuse-leap-15 - # driver: - # image: dokken/opensuse-leap-15 - # pid_one_command: /usr/lib/systemd/systemd + - name: fedora-latest + driver: + image: dokken/fedora-latest + pid_one_command: /usr/lib/systemd/systemd + + - name: opensuse-leap-15 + driver: + image: dokken/opensuse-leap-15 + pid_one_command: /usr/lib/systemd/systemd - name: oracle-8 driver: diff --git a/kitchen.yml b/kitchen.yml index 6be0b33d..bef47c7a 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -45,56 +45,43 @@ platforms: suites: - name: default run_list: - - recipe[firewall::default] - recipe[firewall-test::default] - - name: firewalld + - name: firewalld-simple excludes: - - debian-11 - - debian-12 - - ubuntu-20.04 - - ubuntu-22.04 - - ubuntu-24.04 - windows-2016 - windows-2019 run_list: - - recipe[firewall::default] - recipe[firewall-test::default] + attributes: + firewall: + solution: firewalld - name: ufw excludes: - - almalinux-8 - - almalinux-9 - - almalinux-10 - - amazonlinux-2 - - amazonlinux-2023 - - centos-stream-9 - - centos-stream-10 - - fedora-latest - - opensuse-leap-15 - - oracle-8 - - oracle-9 - - rockylinux-8 - - rockylinux-9 + - almalinux-10 # UFW not available in EPEL 10 (yet?) + - amazonlinux-2023 # No EPEL for AL2023 + - centos-stream-10 # UFW not available in EPEL 10 (yet?) + - opensuse-leap-15 # openSUSE only supports firewalld - windows-2016 - windows-2019 run_list: - - recipe[firewall::default] - recipe[firewall-test::default] + attributes: + firewall: + solution: ufw - name: iptables excludes: - - oracle-9 # iptables fails to install from Oracle repo - - opensuse-leap-15 + - oracle-9 # iptables fails to install from Oracle repo + - opensuse-leap-15 # openSUSE only supports firewalld - windows-2016 - windows-2019 run_list: - - recipe[firewall::default] - recipe[firewall-test::default] attributes: firewall: - ubuntu_iptables: true - redhat7_iptables: true + solution: iptables - name: nftables includes: @@ -105,9 +92,9 @@ suites: run_list: - recipe[nftables-test] - - name: firewalld-dbus + - name: firewalld-advanced excludes: - - ubuntu-20.04 # firewalld version too old. + - ubuntu-20.04 # firewalld version too old for advanced usage. - windows-2016 - windows-2019 run_list: diff --git a/libraries/provider_firewall_iptables.rb b/libraries/provider_firewall_iptables.rb index 4bec8f8e..e689894b 100644 --- a/libraries/provider_firewall_iptables.rb +++ b/libraries/provider_firewall_iptables.rb @@ -22,8 +22,8 @@ class Provider::FirewallIptables < Chef::Provider::LWRPBase include FirewallCookbook::Helpers include FirewallCookbook::Helpers::Iptables - provides :firewall, os: 'linux', platform_family: %w(rhel fedora amazon) do |node| - (node['platform_version'].to_i < 7 && !amazon_linux?) || node['platform_version'].to_i >= 8 || node['firewall']['redhat7_iptables'] + provides :firewall, os: 'linux' do |node| + node['firewall']['solution'] == 'iptables' && node['platform_family'] != 'debian' end def whyrun_supported? diff --git a/libraries/provider_firewall_iptables_ubuntu.rb b/libraries/provider_firewall_iptables_ubuntu.rb index 78d1db73..3ec36c29 100644 --- a/libraries/provider_firewall_iptables_ubuntu.rb +++ b/libraries/provider_firewall_iptables_ubuntu.rb @@ -22,9 +22,8 @@ class Provider::FirewallIptablesUbuntu < Chef::Provider::LWRPBase include FirewallCookbook::Helpers include FirewallCookbook::Helpers::Iptables - provides :firewall, os: 'linux', platform_family: %w(debian) do |node| - node['firewall'] && node['firewall']['ubuntu_iptables'] && - node['platform_version'].to_f > (node['platform'] == 'ubuntu' ? 14.04 : 7) + provides :firewall, os: 'linux' do |node| + node['firewall']['solution'] == 'iptables' && node['platform_family'] == 'debian' end def whyrun_supported? diff --git a/libraries/provider_firewall_iptables_ubuntu1404.rb b/libraries/provider_firewall_iptables_ubuntu1404.rb deleted file mode 100644 index 87db6c2a..00000000 --- a/libraries/provider_firewall_iptables_ubuntu1404.rb +++ /dev/null @@ -1,200 +0,0 @@ -# -# Author:: Seth Chisamore () -# Cookbook:: firewall -# Resource:: default -# -# Copyright:: 2011-2019, Chef Software, Inc. -# -# 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. -# -class Chef - class Provider::FirewallIptablesUbuntu1404 < Chef::Provider::LWRPBase - include FirewallCookbook::Helpers - include FirewallCookbook::Helpers::Iptables - - provides :firewall, os: 'linux', platform_family: %w(debian) do |node| - node['firewall'] && node['firewall']['ubuntu_iptables'] && - node['platform_version'].to_f <= (node['platform'] == 'ubuntu' ? 14.04 : 7) - end - - def whyrun_supported? - false - end - - action :install do - return if disabled?(new_resource) - - # Ensure the package is installed - pkg = package 'iptables-persistent' do - action :nothing - end - pkg.run_action(:install) - new_resource.updated_by_last_action(true) if pkg.updated_by_last_action? - - rule_files = %w(rules.v4) - rule_files << 'rules.v6' if ipv6_enabled?(new_resource) - rule_files.each do |svc| - next if ::File.exist?("/etc/iptables/#{svc}") - - # must create empty file for service to start - f = lookup_or_create_rulesfile(svc) - f.content '# created by chef to allow service to start' - f.run_action(:create) - - new_resource.updated_by_last_action(true) if f.updated_by_last_action? - end - - iptables_service = lookup_or_create_service('iptables-persistent') - [:enable, :start].each do |act| - # iptables-persistent isn't a real service - iptables_service.status_command 'true' - - iptables_service.run_action(act) - new_resource.updated_by_last_action(true) if iptables_service.updated_by_last_action? - end - end - - action :restart do - return if disabled?(new_resource) - - # prints all the firewall rules - log_iptables(new_resource) - - # ensure it's initialized - new_resource.rules({}) unless new_resource.rules - ensure_default_rules_exist(node, new_resource) - - # this populates the hash of rules from firewall_rule resources - firewall_rules = Chef.run_context.resource_collection.select { |item| item.resource_name == :firewall_rule } - firewall_rules.each do |firewall_rule| - next unless firewall_rule.action.include?(:create) && !firewall_rule.should_skip?(:create) - - types = if ipv6_rule?(firewall_rule) # an ip4 specific rule - %w(ip6tables) - elsif ipv4_rule?(firewall_rule) # an ip6 specific rule - %w(iptables) - else # or not specific - %w(iptables ip6tables) - end - - types.each do |iptables_type| - # build rules to apply with weight - k = build_firewall_rule(node, firewall_rule, iptables_type == 'ip6tables') - v = firewall_rule.position - - # unless we're adding them for the first time.... bail out. - next if new_resource.rules[iptables_type].key?(k) && new_resource.rules[iptables_type][k] == v - new_resource.rules[iptables_type][k] = v - end - end - - restart_service = false - - rule_files = %w(iptables) - rule_files << 'ip6tables' if ipv6_enabled?(new_resource) - - rule_files.each do |iptables_type| - iptables_filename = if iptables_type == 'ip6tables' - '/etc/iptables/rules.v6' - else - '/etc/iptables/rules.v4' - end - - # ensure a file resource exists with the current iptables rules - begin - iptables_file = Chef.run_context.resource_collection.find(file: iptables_filename) - rescue - iptables_file = file iptables_filename do - action :nothing - end - end - iptables_file.content build_rule_file(new_resource.rules[iptables_type]) - iptables_file.run_action(:create) - - # if the file was changed, restart iptables - restart_service = true if iptables_file.updated_by_last_action? - end - - if restart_service - service_affected = service 'iptables-persistent' do - action :nothing - end - service_affected.run_action(:restart) - new_resource.updated_by_last_action(true) - end - end - - action :disable do - return if disabled?(new_resource) - - iptables_flush!(new_resource) - iptables_default_allow!(new_resource) - new_resource.updated_by_last_action(true) - - iptables_service = lookup_or_create_service('iptables-persistent') - [:disable, :stop].each do |act| - iptables_service.run_action(act) - new_resource.updated_by_last_action(true) if iptables_service.updated_by_last_action? - end - - %w(rules.v4 rules.v6).each do |svc| - # must create empty file for service to start - f = lookup_or_create_rulesfile(svc) - f.content '# created by chef to allow service to start' - f.run_action(:create) - - new_resource.updated_by_last_action(true) if f.updated_by_last_action? - end - end - - action :flush do - return if disabled?(new_resource) - - iptables_flush!(new_resource) - new_resource.updated_by_last_action(true) - - rule_files = %w(rules.v4) - rule_files << 'rules.v6' if ipv6_enabled?(new_resource) - rule_files.each do |svc| - # must create empty file for service to start - f = lookup_or_create_rulesfile(svc) - f.content '# created by chef to allow service to start' - f.run_action(:create) - - new_resource.updated_by_last_action(true) if f.updated_by_last_action? - end - end - - def lookup_or_create_service(name) - begin - iptables_service = Chef.run_context.resource_collection.find(service: svc) - rescue - iptables_service = service name do - action :nothing - end - end - iptables_service - end - - def lookup_or_create_rulesfile(name) - begin - iptables_file = Chef.run_context.resource_collection.find(file: name) - rescue - iptables_file = file "/etc/iptables/#{name}" do - action :nothing - end - end - iptables_file - end - end -end diff --git a/libraries/provider_firewall_ufw.rb b/libraries/provider_firewall_ufw.rb index 07a0017e..5b6fda5e 100644 --- a/libraries/provider_firewall_ufw.rb +++ b/libraries/provider_firewall_ufw.rb @@ -21,8 +21,8 @@ class Chef class Provider::FirewallUfw < Chef::Provider::LWRPBase include FirewallCookbook::Helpers::Ufw - provides :firewall, os: 'linux', platform_family: %w(debian) do |node| - !(node['firewall'] && node['firewall']['ubuntu_iptables']) + provides :firewall, os: 'linux' do |node| + node['firewall']['solution'] == 'ufw' end def whyrun_supported? diff --git a/recipes/default.rb b/recipes/default.rb index 5c393a5b..f02ce184 100644 --- a/recipes/default.rb +++ b/recipes/default.rb @@ -18,7 +18,7 @@ # # create a variable to use as a condition on some rules that follow -iptables_firewall = !platform_family?('rhel', 'amazon') && node['firewall']['ubuntu_iptables'] +iptables_firewall = node['firewall']['solution'] == 'iptables' firewall 'default' do ipv6_enabled node['firewall']['ipv6_enabled'] if iptables_firewall diff --git a/resources/firewall_firewalld.rb b/resources/firewall_firewalld.rb index 7f5b8e8d..41f41500 100644 --- a/resources/firewall_firewalld.rb +++ b/resources/firewall_firewalld.rb @@ -5,8 +5,8 @@ use '_partial/_firewall' # firewalld platforms only -provides :firewall, os: 'linux', platform_family: %w(rhel fedora amazon) do |node| - (node['platform_version'].to_i >= 7 && !node['firewall']['redhat7_iptables']) || (amazon_linux? && !node['firewall']['redhat7_iptables']) +provides :firewall, os: 'linux' do |node| + node['firewall']['solution'] == 'firewalld' end action :install do diff --git a/resources/firewall_rule.rb b/resources/firewall_rule.rb index 6a80620a..47d289f3 100644 --- a/resources/firewall_rule.rb +++ b/resources/firewall_rule.rb @@ -5,8 +5,8 @@ use '_partial/_firewall_rule' # non-firewalld platforms only -provides :firewall_rule do - !platform_family?('rhel', 'fedora', 'amazon') +provides :firewall_rule do |node| + node['firewall']['solution'] != 'firewalld' end property :direction, Symbol, equal_to: [:in, :out, :pre, :post], default: :in diff --git a/resources/firewall_rule_firewalld.rb b/resources/firewall_rule_firewalld.rb index d6d3aa0d..43360165 100644 --- a/resources/firewall_rule_firewalld.rb +++ b/resources/firewall_rule_firewalld.rb @@ -5,8 +5,8 @@ use '_partial/_firewall_rule' # firewalld platforms only -provides :firewall_rule, os: 'linux', platform_family: %w(rhel fedora amazon) do |node| - (node['platform_version'].to_i >= 7 && !node['firewall']['redhat7_iptables']) || (amazon_linux? && !node['firewall']['redhat7_iptables']) +provides :firewall_rule, os: 'linux' do |node| + node['firewall']['solution'] == 'firewalld' end # Additional firewalld-only properties diff --git a/test/fixtures/cookbooks/firewall-test/recipes/default.rb b/test/fixtures/cookbooks/firewall-test/recipes/default.rb index 28379073..be55a7c3 100644 --- a/test/fixtures/cookbooks/firewall-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewall-test/recipes/default.rb @@ -1,7 +1,29 @@ -firewalld = rhel? || amazon_linux? -iptables = !firewalld && node['firewall']['ubuntu_iptables'] -ufw = !firewalld && !iptables +firewalld = node['firewall']['solution'] == 'firewalld' +iptables = node['firewall']['solution'] == 'iptables' +ufw = node['firewall']['solution'] == 'ufw' + +# UFW provided by opt-in EPEL repository on RHEL platforms +package 'epel-release' do + only_if { platform_family?('rhel') } +end + +# The package resource on Fedora is broken until this is installed. +# Just a Test Kitchen issue? +execute 'install-python3-dnf' do + command 'dnf install -y python3-dnf' + not_if 'python3 -c "import dnf"' + only_if { platform_family?('fedora') } + action :run +end + +# Workaround for a bug when using firewalld: +# * Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1074789 +# * Ubuntu: https://bugs.launchpad.net/ubuntu/+source/policykit-1/+bug/2054716 +user 'polkitd' do + system true + only_if { firewalld && platform?('debian', 'ubuntu') } +end include_recipe 'firewall' @@ -29,7 +51,7 @@ firewall_rule 'addremove' do port 1236 command :allow - only_if { rhel? || amazon_linux? || node['firewall']['ubuntu_iptables'] } # don't do this on ufw, will reset ufw on every converge + not_if { ufw } # don't do this on ufw, will reset ufw on every converge end firewall_rule 'addremove2' do @@ -37,7 +59,7 @@ command :deny end -firewall_rule 'protocolnum' do +firewall_rule 'vrrp protocol by number' do protocol 112 command :allow not_if { ufw } # debian ufw doesn't support protocol numbers @@ -90,11 +112,8 @@ protocol :tcp command :allow direction :in - - # # centos 5 is broken for ipv6 ranges - # # see https://github.com/chef-cookbooks/firewall/pull/111#issuecomment-163520156 - # not_if { rhel? && node['platform_version'].to_f < 6.0 } end + firewall_rule 'HTTP HTTPS' do port [80, 443] protocol :tcp diff --git a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb index 85f5e09e..8bd53476 100644 --- a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb @@ -2,6 +2,15 @@ only_if { platform?('debian') } end +# The package resource on Fedora is broken until this is installed. +# Just a Test Kitchen issue? +execute 'install-python3-dnf' do + command 'dnf install -y python3-dnf' + not_if 'python3 -c "import dnf"' + only_if { platform_family?('fedora') } + action :run +end + # Workaround for a bug when using firewalld: # * Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1074789 # * Ubuntu: https://bugs.launchpad.net/ubuntu/+source/policykit-1/+bug/2054716 diff --git a/test/integration/firewalld/inspec/firewalld_spec.rb b/test/integration/firewalld/inspec/firewalld_spec.rb index 9668b343..ee9ffdab 100644 --- a/test/integration/firewalld/inspec/firewalld_spec.rb +++ b/test/integration/firewalld/inspec/firewalld_spec.rb @@ -1,4 +1,4 @@ -# these tests only for redhat with iptables + expected_rules = [ /rule port port="22" protocol="tcp" accept/, /rule port port="2222" protocol="tcp" accept/, diff --git a/test/integration/helpers/spec_helper.rb b/test/integration/helpers/spec_helper.rb index bbf70d75..b214fc5f 100644 --- a/test/integration/helpers/spec_helper.rb +++ b/test/integration/helpers/spec_helper.rb @@ -1,6 +1,6 @@ # helpers def firewalld? - %w(redhat suse).include?(os.family) + %w(fedora redhat suse).include?(os.family) end def ufw? diff --git a/test/integration/iptables/inspec/iptables_spec.rb b/test/integration/iptables/inspec/iptables_spec.rb index a3f25c7f..706aef38 100644 --- a/test/integration/iptables/inspec/iptables_spec.rb +++ b/test/integration/iptables/inspec/iptables_spec.rb @@ -1,33 +1,42 @@ -# these tests only for redhat with iptables + expected_rules = [ # we included the .*-j so that we don't bother testing comments /-A INPUT -i lo .*-j ACCEPT/, /-A INPUT -p icmp .*-j ACCEPT/, /-A INPUT -m state --state RELATED,ESTABLISHED .*-j ACCEPT/, /-A INPUT -p tcp -m tcp -m multiport --dports 22 .*-j ACCEPT/, - # /-A INPUT -p tcp -m tcp -m multiport --dports 22 .*-j ACCEPT/, - # /-A INPUT -p tcp -m tcp -m multiport --dports 2200,2222 .*-j ACCEPT/, - # /-A INPUT -p tcp -m tcp -m multiport --dports 1234 .*-j DROP/, - # /-A INPUT -p tcp -m tcp -m multiport --dports 1235 .*-j REJECT --reject-with icmp-port-unreachable/, - # /-A INPUT -p tcp -m tcp -m multiport --dports 1236 .*-j DROP/, - # /-A INPUT -p vrrp .*-j ACCEPT/, - # %r{-A INPUT -s 192.168.99.99(/32)? -p tcp -m tcp .*-j REJECT --reject-with icmp-port-unreachable}, + /-A INPUT -p tcp -m tcp -m multiport --dports 22 .*-j ACCEPT/, + /-A INPUT -p tcp -m tcp -m multiport --dports 2200,2222 .*-j ACCEPT/, + /-A INPUT -p tcp -m tcp -m multiport --dports 1234 .*-j DROP/, + /-A INPUT -p tcp -m tcp -m multiport --dports 1235 .*-j REJECT --reject-with icmp-port-unreachable/, + /-A INPUT -p tcp -m tcp -m multiport --dports 1236 .*-j DROP/, + %r{-A INPUT -s 192.168.99.99(/32)? -p tcp -m tcp .*-j REJECT --reject-with icmp-port-unreachable}, ] +vrrp_protocol = 'vrrp' +if %w(redhat suse).include?(os.family) && os.release == '10' + # On RHEL 10 iptables-save doesn't show the friendly protocol name, just the + # /etc/protocols number + vrrp_protocol = '112' +end + +expected_rules.push(/-A INPUT -p #{vrrp_protocol} .*-j ACCEPT/) + expected_ipv6_rules = [ /-A INPUT -i lo .*-j ACCEPT/, %r{-A INPUT( -s ::/0 -d ::/0)? -m state --state RELATED,ESTABLISHED .*-j ACCEPT}, /-A INPUT.* -p ipv6-icmp .*-j ACCEPT/, /-A INPUT -p tcp -m tcp -m multiport --dports 22 .*-j ACCEPT/, - # %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 22 .*-j ACCEPT}, - # %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 2200,2222 .*-j ACCEPT}, - # %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 1234 .*-j DROP}, - # %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 1235 .*-j REJECT --reject-with icmp6-port-unreachable}, - # %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 1236 .*-j DROP}, - # %r{-A INPUT( -s ::/0 -d ::/0)? -p vrrp .*-j ACCEPT}, - # %r{-A INPUT -s 2001:db8::ff00:42:8329/128( -d ::/0)? -p tcp -m tcp -m multiport --dports 80 .*-j ACCEPT}, + %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 22 .*-j ACCEPT}, + %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 2200,2222 .*-j ACCEPT}, + %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 1234 .*-j DROP}, + %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 1235 .*-j REJECT --reject-with icmp6-port-unreachable}, + %r{-A INPUT( -s ::/0 -d ::/0)? -p tcp -m tcp -m multiport --dports 1236 .*-j DROP}, + %r{-A INPUT -s 2001:db8::ff00:42:8329/128( -d ::/0)? -p tcp -m tcp -m multiport --dports 80 .*-j ACCEPT}, ] +expected_ipv6_rules.push(%r{-A INPUT( -s ::/0 -d ::/0)? -p #{vrrp_protocol} .*-j ACCEPT}) + describe command('iptables-save') do its(:stdout) { should match(/COMMIT/) } diff --git a/test/integration/ufw/inspec/ufw_spec.rb b/test/integration/ufw/inspec/ufw_spec.rb index 2a09a343..4a295b8f 100644 --- a/test/integration/ufw/inspec/ufw_spec.rb +++ b/test/integration/ufw/inspec/ufw_spec.rb @@ -1,4 +1,4 @@ -# these tests only for debian/ubuntu with ufw + expected_rules = [ %r{ 22/tcp + ALLOW IN +Anywhere}, %r{ 2200,2222/tcp + ALLOW IN +Anywhere}, From 848615a91436da021562cee44bf655b532aadd62 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Mon, 30 Dec 2024 17:01:10 -0800 Subject: [PATCH 21/24] Pin dokken to Chef 18.3 due to bug in latest Chef container Until https://github.com/chef/chef/issues/14760 is fixed. Signed-off-by: Joseph Larionov --- kitchen.dokken.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kitchen.dokken.yml b/kitchen.dokken.yml index 34ecb6b0..e50adfe8 100644 --- a/kitchen.dokken.yml +++ b/kitchen.dokken.yml @@ -1,7 +1,9 @@ driver: name: dokken privileged: true - chef_version: <%= ENV['CHEF_VERSION'] || 'current' %> + # Latest Chef container (18.6.2) is broken: https://github.com/chef/chef/issues/14760 + # chef_version: <%= ENV['CHEF_VERSION'] || 'current' %> + chef_version: '18.3' intermediate_instructions: - | # Need to set "IPv6_rpfilter=no" otherwise firewalld won't start inside Docker container RUN mkdir -p /etc/firewalld && \ From 4dbf1e723765d9ffe6fe15ff95a830d0878018e4 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Tue, 31 Dec 2024 11:57:40 -0800 Subject: [PATCH 22/24] Add upgrade instructions for this release Signed-off-by: Joseph Larionov --- CHANGELOG.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fae0f526..5b5b955b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,31 @@ This file is used to list changes made in each version of the firewall cookbook. ## Unreleased +### Summary + +Key changes in this release: + +- **Rich Rules on firewalld**: The `firewall_rule` resource now creates [rich rules](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html) on firewalld platforms instead of using the deprecated `--direct` interface. +- **Flexible firewall selection**: The cookbook now uses the `default['firewall']['solution']` attribute to determine the firewall solution to use instead of a hardcoded assignment for each platform. It defaults to the platform's native firewall (same as previous hardcoded values). +- **Firewalld 2.0.0**: Platforms using firewalld 2.0.0 and later, such as RHEL 10 and Ubuntu 24.04, are now supported. + +### Upgrade Instructions + +This release introduces breaking changes. To upgrade to this release: + +- Migrate usages of the `disabled` property on `firewall` resources to the `enabled` property instead. +- Migrate usages of `default['firewall']['firewalld']` attributes to `firewalld_zone` resources. +- Remove usages of the `:save` action from `firewall_rule` resources. Rules are now always saved permanently. +- Remove usages of the `permanent` property on `firewall_rule` resources. Rules are now always saved permanently. +- Remove usages of the `disabled_zone` and `enabled_zone` properties on `firewall` resources. Use the `firewalld_zone` resource to manage firewalld zone configuration. +- Replace usages of the `firewall::firewalld` recipe with `firewall::default`. +- Migrate usages of attributes `default['firewall']['ubuntu_iptables']` and `default['firewall']['redhat7_iptables']` with `default['firewall']['solution']`. + ### Added - Support for firewalld 2.0.0 and the platforms that use it; RHEL 10 and Ubuntu 24.04. - `priority`, `ingress_priority`, `egress_priority` properties added to `firewalld_zone`. -- Added `firewalld_rich_rule` resource for adding/removing rich rules to/from firewalld zones. +- Added `firewalld_rich_rule` resource for adding/removing [rich rules](https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html) to/from firewalld zones. - Support for IPv6 rules on firewalld platforms. - Support for using any compatible firewall solution on any platform. Defaults to the operating system's default firewall solution. From dff0975e70e6a3b91bf6f9f389dd907e3f926502 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Tue, 31 Dec 2024 12:30:24 -0800 Subject: [PATCH 23/24] Linting Signed-off-by: Joseph Larionov --- README.md | 8 ++++---- documentation/resources/firewalld_zone.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 515304c1..a95cc1a5 100644 --- a/README.md +++ b/README.md @@ -181,15 +181,15 @@ It's not recommended to use this resource directly. Instead simply `include_reci #### Actions - `:install` (*default action*): Install and Enable the firewall. This will ensure the appropriate packages are installed and that any services have been started. -- `:reload`: _firewalld only_. Reloads the runtime state to match the permanent configuration. All runtime-only rules are flushed out. +- `:reload`: *firewalld only*. Reloads the runtime state to match the permanent configuration. All runtime-only rules are flushed out. - `:disable`: Disable the firewall. Drop any rules and put the node in an unprotected state. Flush all current rules. Also erase any internal state used to detect when rules should be applied. -- `:flush`: _Except firewalld_. Flush all current rules. Also erase any internal state used to detect when rules should be applied. +- `:flush`: *Except firewalld*. Flush all current rules. Also erase any internal state used to detect when rules should be applied. #### Properties - `enabled` (default to `true`): If set to `false`, all actions will no-op on this resource. This is a way to prevent included cookbooks from configuring a firewall. -- `ipv6_enabled` (default to `true`): _Iptables only_. If set to false, firewall will not perform any ipv6 related work. +- `ipv6_enabled` (default to `true`): *Iptables only*. If set to false, firewall will not perform any ipv6 related work. - `log_level`: UFW only. Level of verbosity the firewall should log at. valid values are: :low, :medium, :high, :full, :off. default is :low. - `package_options`: Pass additional options to the package manager when installing the firewall. @@ -266,7 +266,7 @@ Firewall-agnostic properties that can be used with `firewall_rule` on any firewa - `destination`: ip address or subnet to filter on packet destination, must be a valid IP - `position` (*default: 50*): **relative** position to insert rule at. Position may be any integer between 0 < n < 100 (exclusive), and more than one rule may specify the same position. -Additional properties for advanced firewall rules that tied to specific firewall solutions. **Note: These properties are _not_ firewall-agnostic, so you must ensure they are used only on the appropriate firewall solutions**: +Additional properties for advanced firewall rules that tied to specific firewall solutions. **Note: These properties are *not* firewall-agnostic, so you must ensure they are used only on the appropriate firewall solutions**: - `zone`: (*firewalld*), a string, such as `public` that the rule will be applied. Defaults to the system's configured default zone. diff --git a/documentation/resources/firewalld_zone.md b/documentation/resources/firewalld_zone.md index e157048b..c8e99702 100644 --- a/documentation/resources/firewalld_zone.md +++ b/documentation/resources/firewalld_zone.md @@ -24,7 +24,7 @@ | `interfaces` | `[Array, String]` | | array of interfaces. See interface tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `masquerade` | `[true, false]` | | see masquerade tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `ports` | `[Array, String]` | | array of port and protocol pairs, in `["PORT/PROTOCOL"]` format. See port tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | -| `priority` | `Integer` | | set the zone priority for both ingress and egress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See https://firewalld.org/2023/04/zone-priorities for more information. | +| `priority` | `Integer` | | set the zone priority for both ingress and egress traffic. A lower priority value has higher precedence. Added in firewalld 2.0.0. See for more information. | | `protocols` | `[Array, String]` | | array of protocols, see protocol tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `rules_str` | `[Array, String]` | | array of rich-language rules. See rule tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | | `services` | `[Array, String]` | | array of service names, see service tag in [firewalld.zone(5)](https://firewalld.org/documentation/man-pages/firewalld.zone.html). | From 6f34a40eca20575d8bf2e81978629349292bc031 Mon Sep 17 00:00:00 2001 From: Joseph Larionov Date: Tue, 31 Dec 2024 12:12:11 -0800 Subject: [PATCH 24/24] Run tests with kitchen-dokken in GitHub Actions CI Signed-off-by: Joseph Larionov --- .github/workflows/ci.yml | 208 +++++++++++++++++++++++++++++++++------ 1 file changed, 179 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e98d62f..57f90395 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,46 +17,196 @@ jobs: integration: needs: lint-unit - runs-on: macos-latest + runs-on: ubuntu-24.04 strategy: + fail-fast: false matrix: include: - - os: amazonlinux-2 - suite: firewalld - - os: amazonlinux-2 - suite: iptables - - os: centos-7 - suite: firewalld - - os: centos-7 - suite: iptables - - os: centos-stream-8 - suite: iptables - - os: debian-9 - suite: ufw - - os: debian-10 - suite: ufw - - os: debian-11 - suite: firewalld-dbus - - os: debian-11 - suite: nftables - - os: ubuntu-1804 - suite: ufw - - os: ubuntu-2004 - suite: ufw - fail-fast: false + # Default suite + - suite: default + os: almalinux-8 + - suite: default + os: almalinux-9 + - suite: default + os: almalinux-10 + - suite: default + os: amazonlinux-2023 + - suite: default + os: centos-stream-9 + - suite: default + os: centos-stream-10 + - suite: default + os: debian-11 + - suite: default + os: debian-12 + - suite: default + os: fedora-latest + - suite: default + os: opensuse-leap-15 + - suite: default + os: oracle-8 + - suite: default + os: oracle-9 + - suite: default + os: rockylinux-8 + - suite: default + os: rockylinux-9 + - suite: default + os: ubuntu-2204 + - suite: default + os: ubuntu-2404 + + # Firewalld simple suite + - suite: firewalld-simple + os: almalinux-8 + - suite: firewalld-simple + os: almalinux-9 + - suite: firewalld-simple + os: almalinux-10 + - suite: firewalld-simple + os: amazonlinux-2023 + - suite: firewalld-simple + os: centos-stream-9 + - suite: firewalld-simple + os: centos-stream-10 + - suite: firewalld-simple + os: debian-11 + - suite: firewalld-simple + os: debian-12 + - suite: firewalld-simple + os: fedora-latest + - suite: firewalld-simple + os: opensuse-leap-15 + - suite: firewalld-simple + os: oracle-8 + - suite: firewalld-simple + os: oracle-9 + - suite: firewalld-simple + os: rockylinux-8 + - suite: firewalld-simple + os: rockylinux-9 + - suite: firewalld-simple + os: ubuntu-2004 + - suite: firewalld-simple + os: ubuntu-2204 + - suite: firewalld-simple + os: ubuntu-2404 + + # UFW suite + - suite: ufw + os: almalinux-8 + - suite: ufw + os: almalinux-9 + - suite: ufw + os: centos-stream-9 + - suite: ufw + os: debian-11 + - suite: ufw + os: debian-12 + # - suite: ufw # Fails on GitHub Actions with: Module ip6_tables not found in directory /lib/modules/6.8.0-1017-azure + # os: fedora-latest + - suite: ufw + os: oracle-8 + - suite: ufw + os: oracle-9 + - suite: ufw + os: rockylinux-8 + - suite: ufw + os: rockylinux-9 + - suite: ufw + os: ubuntu-2204 + - suite: ufw + os: ubuntu-2404 + + # Iptables suite + - suite: iptables + os: almalinux-8 + - suite: iptables + os: almalinux-9 + - suite: iptables + os: almalinux-10 + - suite: iptables + os: amazonlinux-2023 + - suite: iptables + os: centos-stream-9 + - suite: iptables + os: centos-stream-10 + - suite: iptables + os: debian-11 + - suite: iptables + os: debian-12 + # - suite: iptables # Fails on GitHub Actions with: ip6tables.service failed + # os: fedora-latest + - suite: iptables + os: oracle-8 + - suite: iptables + os: rockylinux-8 + - suite: iptables + os: rockylinux-9 + - suite: iptables + os: ubuntu-2204 + - suite: iptables + os: ubuntu-2404 + + # NFTables suite + - suite: nftables + os: debian-11 + - suite: nftables + os: debian-12 + - suite: nftables + os: oracle-8 + - suite: nftables + os: oracle-9 + + # Firewalld advanced suite + - suite: firewalld-advanced + os: almalinux-8 + - suite: firewalld-advanced + os: almalinux-9 + - suite: firewalld-advanced + os: almalinux-10 + - suite: firewalld-advanced + os: amazonlinux-2023 + - suite: firewalld-advanced + os: centos-stream-9 + - suite: firewalld-advanced + os: centos-stream-10 + - suite: firewalld-advanced + os: debian-11 + - suite: firewalld-advanced + os: debian-12 + - suite: firewalld-advanced + os: fedora-latest + - suite: firewalld-advanced + os: opensuse-leap-15 + - suite: firewalld-advanced + os: oracle-8 + - suite: firewalld-advanced + os: oracle-9 + - suite: firewalld-advanced + os: rockylinux-8 + - suite: firewalld-advanced + os: rockylinux-9 + - suite: firewalld-advanced + os: ubuntu-2204 + - suite: firewalld-advanced + os: ubuntu-2404 + + # TODO: Windows suite + # - suite: windows + # os: windows-2016 + # - suite: windows + # os: windows-2019 steps: - name: Checkout code uses: actions/checkout@v4 - - name: Install VirtualBox - run: | - brew update - brew upgrade virtualbox - name: Install Chef uses: actionshub/chef-install@3.0.1 - - name: kitchen + - name: Dokken uses: actionshub/test-kitchen@3.0.0 env: CHEF_LICENSE: accept-no-persist + KITCHEN_LOCAL_YAML: kitchen.dokken.yml with: suite: ${{ matrix.suite }} os: ${{ matrix.os }}