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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 044df83d..5b5b955b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,57 @@ 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](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. + +### 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 + +- 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. +- Fixed: `ufw` provider doesn't ensure `ufw` service is enabled. + +### 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. +- 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* ## 6.3.8 - *2024-11-18* diff --git a/README.md b/README.md index cb2eb017..a95cc1a5 100644 --- a/README.md +++ b/README.md @@ -22,50 +22,78 @@ 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 (IPv4 only support, [needs contributions/testing](https://github.com/chef-cookbooks/firewall/issues/86)) -- 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. -## Considerations that apply to all firewall providers and resources +### 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 simply open a port in the system's default firewall: + +```ruby +include_recipe 'firewall' + +firewall_rule 'ssh' do + port 22 +end +``` + +## How it works -This cookbook comes with two resources, firewall and firewall rule. The typical usage scenario is as follows: +The most basic use involves two resources, `firewall` and `firewall_rule`. The typical usage scenario is as follows: -- 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. +- 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 `: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. +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,65 +136,62 @@ 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 +See the [`firewalld` resources](documentation/README.md) documentation for advanced firewalld configuration. -Used to disable platform specific firewall. Many clouds have their own firewall configured outside of the OS instance such as AWS Security Groups. +## Recipes + +### `firewall::default` + +The default recipe creates a firewall resource with action install. -### firewalld +### `firewall::disable_firewall` -A firewalld specific recipe creates a firewall resource with action install with the default zone (default: `drop`) +Used to disable platform specific firewall. Many clouds have their own firewall configured outside of the OS instance such as AWS Security Groups. ## 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 - `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 -- `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. +- `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 +209,85 @@ 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). -#### Parameters +#### Properties + +```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 + + # 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 +``` + +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` -- `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* 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. +- `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 +302,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 +324,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/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/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/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/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_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_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/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 05e93846..c8e99702 100644 --- a/documentation/resources/firewalld_zone.md +++ b/documentation/resources/firewalld_zone.md @@ -15,18 +15,21 @@ | 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). | +| `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 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.dokken.yml b/kitchen.dokken.yml index 998bb20c..e50adfe8 100644 --- a/kitchen.dokken.yml +++ b/kitchen.dokken.yml @@ -1,7 +1,14 @@ 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 && \ + echo "DefaultZone=public" > /etc/firewalld/firewalld.conf && \ + echo "IPv6_rpfilter=no" >> /etc/firewalld/firewalld.conf transport: { name: dokken } provisioner: { name: dokken } @@ -57,12 +64,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 4791d8d8..bef47c7a 100644 --- a/kitchen.yml +++ b/kitchen.yml @@ -14,28 +14,27 @@ provisioner: allow_mosh: true allow_loopback: true allow_icmp: true - firewalld: - permanent: true verifier: name: inspec 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: freebsd-11 - - name: freebsd-12 - 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 - name: windows-2016 driver_config: box: tas50/windows_2016 @@ -46,71 +45,63 @@ platforms: suites: - name: default run_list: - - recipe[firewall::default] - recipe[firewall-test::default] - - name: firewalld + - name: firewalld-simple excludes: - - almalinux-8 - - centos-8 - - debian-9 - - debian-10 - - oracle-8 - - ubuntu-18.04 - - ubuntu-20.04 - windows-2016 - windows-2019 run_list: - - recipe[firewall::default] - recipe[firewall-test::default] + attributes: + firewall: + solution: firewalld - name: ufw excludes: - - almalinux-8 - - amazonlinux-2 - - centos-7 - - centos-8 - - fedora-latest - - freebsd-11 - - freebsd-12 - - opensuse-leap-15 - - oracle-8 + - 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: - - debian-9 - - debian-10 - - oracle-8 - - ubuntu-18.04 - - ubuntu-20.04 + - 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: - debian-11 + - debian-12 - oracle-8 + - oracle-9 run_list: - recipe[nftables-test] - - name: firewalld-dbus - includes: - - debian-11 - - fedora-34 + - name: firewalld-advanced + excludes: + - ubuntu-20.04 # firewalld version too old for advanced usage. + - windows-2016 + - windows-2019 run_list: - recipe[firewalld-test] + provisioner: + enforce_idempotency: true + multiple_converge: 2 - name: windows includes: 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/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/helpers_firewalld_dbus.rb b/libraries/helpers_firewalld_dbus.rb index c78b9049..4c18bb60 100644 --- a/libraries/helpers_firewalld_dbus.rb +++ b/libraries/helpers_firewalld_dbus.rb @@ -21,6 +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/libraries/provider_firewall_firewalld.rb b/libraries/provider_firewall_firewalld.rb deleted file mode 100644 index e75fdfff..00000000 --- a/libraries/provider_firewall_firewalld.rb +++ /dev/null @@ -1,179 +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.is_a?(Chef::Resource::FirewallRule) } - 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| - # build rules to apply with weight - k = "firewall-cmd --zone=#{firewall_rule.zone} --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 --zone=#{firewall_rule.zone} --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/provider_firewall_iptables.rb b/libraries/provider_firewall_iptables.rb index 1878861b..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? @@ -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..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? @@ -75,7 +74,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 deleted file mode 100644 index b8b277a8..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.is_a?(Chef::Resource::FirewallRule) } - 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_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..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? @@ -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 @@ -66,7 +70,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) @@ -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) 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.rb b/libraries/resource_firewall.rb index 3920f8ff..d59c2b7a 100644 --- a/libraries/resource_firewall.rb +++ b/libraries/resource_firewall.rb @@ -2,21 +2,14 @@ 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) - # 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) 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/libraries/resource_firewall_rule.rb b/libraries/resource_firewall_rule.rb deleted file mode 100644 index c85deda2..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, default: 'drop') - - # 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/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' diff --git a/recipes/default.rb b/recipes/default.rb index f9927946..f02ce184 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 = node['firewall']['solution'] == '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..41f41500 --- /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' do |node| + node['firewall']['solution'] == 'firewalld' +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 new file mode 100644 index 00000000..47d289f3 --- /dev/null +++ b/resources/firewall_rule.rb @@ -0,0 +1,38 @@ + +unified_mode true + +# Common properties defined in a resource partial +use '_partial/_firewall_rule' + +# non-firewalld platforms only +provides :firewall_rule do |node| + node['firewall']['solution'] != 'firewalld' +end + +property :direction, Symbol, equal_to: [:in, :out, :pre, :post], default: :in +property :logging, Symbol, equal_to: [:connections, :packets] +property :interface, String +property :dest_interface, String +property :stateful, [Symbol, Array] +property :include_comment, [true, false], default: true + +# 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 diff --git a/resources/firewall_rule_firewalld.rb b/resources/firewall_rule_firewalld.rb new file mode 100644 index 00000000..43360165 --- /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' do |node| + node['firewall']['solution'] == 'firewalld' +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 c805ea79..12158802 100644 --- a/resources/firewalld.rb +++ b/resources/firewalld.rb @@ -3,10 +3,21 @@ 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' + + package 'firewalld' do + options new_resource.package_options if new_resource.package_options + action :install + end + + service 'firewalld' do + action [:enable, :start] + end end action :reload do 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_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_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/resources/firewalld_service.rb b/resources/firewalld_service.rb index 44791259..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 @@ -71,8 +73,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 d4841721..7b52ea60 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).', @@ -29,8 +35,11 @@ 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], + 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).', @@ -49,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], @@ -72,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 @@ -85,16 +96,38 @@ 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) + 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) - next unless new_value + + 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/firewall-test/recipes/default.rb b/test/fixtures/cookbooks/firewall-test/recipes/default.rb index f6183f5a..be55a7c3 100644 --- a/test/fixtures/cookbooks/firewall-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewall-test/recipes/default.rb @@ -1,3 +1,30 @@ + +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' firewall_rule 'ssh22' do @@ -24,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 @@ -32,10 +59,10 @@ command :deny end -firewall_rule 'protocolnum' do +firewall_rule 'vrrp protocol by number' 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 +86,49 @@ 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 'limit 23/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 + 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/fixtures/cookbooks/firewalld-test/recipes/default.rb b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb index aeadcebf..8bd53476 100644 --- a/test/fixtures/cookbooks/firewalld-test/recipes/default.rb +++ b/test/fixtures/cookbooks/firewalld-test/recipes/default.rb @@ -2,6 +2,23 @@ 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 +user 'polkitd' do + system true + only_if { platform?('debian', 'ubuntu') } +end + firewalld 'default' firewalld_config 'set some values' do @@ -19,33 +36,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' @@ -59,11 +66,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 @@ -90,11 +92,6 @@ firewalld_policy 'pminimal' do egress_zones 'external' ingress_zones 'internal' - masquerade true -end - -firewalld_policy 'change-pminimal' do - short 'pminimal' masquerade false end @@ -110,10 +107,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' @@ -147,3 +140,20 @@ 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 + +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/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-dbus/inspec/firewalld-dbus_spec.rb b/test/integration/firewalld-dbus/inspec/firewalld-dbus_spec.rb index 01cfd24e..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 @@ -77,9 +94,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 +124,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,14 +171,18 @@ 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 - ptest_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:#{' '} - services: dhcpv6-client mdns samba-client ssh + services: #{cockpit}dhcpv6-client mdns samba-client ssh ports:#{' '} protocols:#{' '} forward: no @@ -164,14 +191,17 @@ source-ports:#{' '} icmp-blocks:#{' '} rich rules:#{' '} + \trule port port="443" protocol="tcp" audit accept EOF - its(:stdout) { should cmp ptest_config } + its(:stdout) { should cmp expected_config } 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 @@ -191,9 +221,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 @@ -210,6 +242,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 } diff --git a/test/integration/firewalld/inspec/firewalld_spec.rb b/test/integration/firewalld/inspec/firewalld_spec.rb index a6121f09..ee9ffdab 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..b214fc5f 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(fedora 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 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 ef5e847a..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}, @@ -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/, ]