Skip to content

Commit

Permalink
Draft: Add support for policy objects
Browse files Browse the repository at this point in the history
XXX: Needs more checking of default_*_zone manifest parameters...

Closes: voxpupuli#316
  • Loading branch information
qha committed Apr 5, 2022
1 parent 07a6dfa commit 3c83a22
Show file tree
Hide file tree
Showing 15 changed files with 1,008 additions and 30 deletions.
81 changes: 77 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
This module manages firewalld, the userland interface that replaces
iptables and ships with RHEL7+. The module manages firewalld itself as
well as providing types and providers for managing firewalld zones,
ports, and rich rules.
policies, ports, and rich rules.

## Compatibility

Expand Down Expand Up @@ -48,6 +48,7 @@ class { 'firewalld': }
* `log_denied`: Optional, (firewalld-0.4.3.2-8+) Log denied packets, can be one
of `off`, `all`, `multicast`, `unicast`, `broadcast` (default: undef)
* `zones`: A hash of [firewalld zones](#firewalld-zones) to configure
* `policies`: A hash of [firewalld policies](#firewalld-policies) to configure
* `ports`: A hash of [firewalld ports](#firewalld-ports) to configure
* `services`: A hash of [firewalld services](#firewalld-service) to configure
* `rich_rules`: A hash of [firewalld rich rules](#firewalld-rich-rules) to configure
Expand Down Expand Up @@ -78,6 +79,7 @@ changes.
This module supports a number of resource types

* [firewalld_zone](#firewalld-zones)
* [firewalld_policy](#firewalld-policies)
* [firewalld_port](#firewalld-ports)
* [firewalld_service](#firewalld-service)
* [firewalld_ipset](#firewalld-ipsets)
Expand Down Expand Up @@ -146,14 +148,71 @@ firewalld::zones:
includes the default ssh port. If you fail to specify the
appropriate port, rich rule, or service, you will lock yourself out.

### Firewalld policies

Firewalld policies can be managed with the `firewalld_policy` resource type.

_Example in Class_:

```puppet
firewalld_policy { 'anytorestricted':
ensure => present,
target => '%%REJECT%%',
ingress_zones => ['ANY'],
egress_zones => ['restricted'],
purge_rich_rules => true,
purge_services => true,
purge_ports => true,
}
```

_Example in Hiera_:

```yaml
firewalld::policies:
anytorestricted:
ensure: present
target: '%%REJECT%%'
ingress_zones:
- 'ANY'
egress_zones:
- 'restricted'
purge_rich_rules: true
purge_services: true
purge_ports: true
```

#### Parameters (Firewalld policies)

* `target`: Specify the target of the policy.
* `ingress_zones`: An array of ingress zones for this policy.
* `egress_zones`: An array of egress zones for this policy.
* `priority`: A non zero integer specifying the priority of this
policy, policies with negative priorities apply before rules in
zones, policies with positive priorities, after. Defaults to -1.
* `icmp_blocks`: An array of ICMP blocks for the policy
* `masquerade`: If set to `true` or `false` specifies whether or not
to add masquerading to the policy
* `purge_rich_rules`: Optional, and defaulted to false. When true any
configured rich rules found in the policy that do not match what is in
the Puppet catalog will be purged.
* `purge_services`: Optional, and defaulted to false. When true any
configured services found in the policy that do not match what is in
the Puppet catalog will be purged.
* `purge_ports`: Optional, and defaulted to false. When true any
configured ports found in the policy that do not match what is in the
Puppet catalog will be purged.

### Firewalld Rich Rules

Firewalld rich rules are managed using the `firewalld_rich_rule`
resource type

Exactly one of the `zone` or `policy` parameters must be given

firewalld_rich_rules will `autorequire` the firewalld_zone specified
in the `zone` parameter so there is no need to add dependencies for
this
in the `zone` parameter or the firewalld_policy specified in the
`policy` parameter so there is no need to add dependencies for this

_Example in Class_:

Expand Down Expand Up @@ -181,7 +240,9 @@ firewalld::rich_rules:

#### Parameters (Firewalld Rich Rules)

* `zone`: Name of the zone this rich rule belongs to
* `zone`: (Optional) Name of the zone this rich rule belongs to

* `policy`: (Optional) Name of the policy this rich rule belongs to

* `family`: Protocol family, defaults to `ipv4`

Expand Down Expand Up @@ -403,6 +464,8 @@ will produce:
The `firewalld_service` type is used to add or remove both built in
and custom services from zones.

Exactly one of the `zone` or `policy` parameters must be given.

firewalld_service will `autorequire` the firewalld_zone specified in
the `zone` parameter and the firewalld::custom_service specified in
the `service` parameter, so there is no need to add dependencies for
Expand Down Expand Up @@ -445,6 +508,10 @@ firewalld::services:
defaults to parameter `default_service_zone` of class `firewalld` if
specified.

* `policy`: Name of the policy in which you want to manage the
service. Make sure to set `zone` to `Unset` if you use this and have
specified `default_service_zone` for class `firewalld`.

* `ensure`: Whether to add (`present`) or remove the service
(`absent`), defaults to `present`.

Expand Down Expand Up @@ -488,6 +555,8 @@ options of an ipset you must delete the existing ipset first.
Firewalld ports can be managed with the `firewalld_port` resource
type.

Exactly one of the `zone` or `policy` parameters must be given.

firewalld_port will `autorequire` the firewalld_zone specified in the
`zone` parameter so there is no need to add dependencies for this

Expand Down Expand Up @@ -518,6 +587,10 @@ firewalld::ports:
* `zone`: Name of the zone this port belongs to, defaults to parameter
`default_port_zone` of class `firewalld` if specified.

* `policy`: Name of the policy this port belongs to. Make sure to set
`zone` to `Unset` if you use this and have specified
`default_port_zone` for class `firewalld`.

* `port`: The port to manage, defaults to the resource name.

* `protocol`: The protocol this port uses, e.g. `tcp` or `udp`,
Expand Down
9 changes: 7 additions & 2 deletions lib/puppet/provider/firewalld.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def self.check_running_state
end

# v3.0.0
def self.execute_firewall_cmd(args, zone = nil, perm = true, failonfail = true, check_online = true)
def self.execute_firewall_cmd(args, zone = nil, policy = nil, perm = true, failonfail = true, check_online = true)
if check_online && !online?
shell_cmd = 'firewall-offline-cmd'
perm = false
Expand All @@ -53,6 +53,7 @@ def self.execute_firewall_cmd(args, zone = nil, perm = true, failonfail = true,
cmd_args = []
cmd_args << '--permanent' if perm
cmd_args << ['--zone', zone] unless zone.nil?
cmd_args << ['--policy', policy] unless policy.nil?

# Add the arguments to our command string, removing any quotes, the command
# provider will sort the quotes out.
Expand All @@ -74,7 +75,11 @@ def self.execute_firewall_cmd(args, zone = nil, perm = true, failonfail = true,
end

def execute_firewall_cmd(args, zone = @resource[:zone], perm = true, failonfail = true)
self.class.execute_firewall_cmd(args, zone, perm, failonfail)
self.class.execute_firewall_cmd(args, zone, nil, perm, failonfail)
end

def execute_firewall_cmd_policy(args, policy = @resource[:policy], perm = true, failonfail = true)
self.class.execute_firewall_cmd(args, nil, policy, perm, failonfail)
end

# Arguments should be parsed as separate array entities, but quoted arg
Expand Down
213 changes: 213 additions & 0 deletions lib/puppet/provider/firewalld_policy/firewall_cmd.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
require 'puppet'
require 'puppet/type'
require File.join(File.dirname(__FILE__), '..', 'firewalld.rb')

Puppet::Type.type(:firewalld_policy).provide(
:firewall_cmd,
parent: Puppet::Provider::Firewalld
) do
desc 'Interact with firewall-cmd'

def exists?
@resource[:policy] = @resource[:name]
execute_firewall_cmd_policy(['--get-policies'], nil).split(' ').include?(@resource[:name])
end

def create
debug("Creating new policy #{@resource[:name]} with target: '#{@resource[:target]}'")
execute_firewall_cmd_policy(['--new-policy', @resource[:name]], nil)

self.target = (@resource[:target]) if @resource[:target]
self.ingress_zones = @resource[:ingress_zones]
self.egress_zones = @resource[:egress_zones]
self.priority = @resource[:priority] if @resource[:priority]
self.icmp_blocks = (@resource[:icmp_blocks]) if @resource[:icmp_blocks]
self.description = (@resource[:description]) if @resource[:description]
self.short = (@resource[:short]) if @resource[:short]
end

def destroy
debug("Deleting policy #{@resource[:name]}")
execute_firewall_cmd_policy(['--delete-policy', @resource[:name]], nil)
end

def target
policy_target = execute_firewall_cmd_policy(['--get-target']).chomp
# The firewall-cmd may or may not return the target surrounded by
# %% depending on the version. See:
# https://github.com/crayfishx/puppet-firewalld/issues/111
return @resource[:target] if @resource[:target].delete('%') == policy_target
policy_target
end

def target=(_t)
debug("Setting target for policy #{@resource[:name]} to #{@resource[:target]}")
execute_firewall_cmd_policy(['--set-target', @resource[:target]])
end

def ingress_zones
execute_firewall_cmd_policy(['--list-ingress-zones']).chomp.split(' ') || []
end

def ingress_zones=(new_ingress_zones)
new_ingress_zones ||= []
cur_ingress_zones = ingress_zones
(new_ingress_zones - cur_ingress_zones).each do |i|
debug("Adding ingress zone '#{i}' to policy #{@resource[:name]}")
execute_firewall_cmd_policy(['--add-ingress-zone', i])
end
(cur_ingress_zones - new_ingress_zones).each do |i|
debug("Removing ingress zone '#{i}' from policy #{@resource[:name]}")
execute_firewall_cmd_policy(['--remove-ingress-zone', i])
end
end

def egress_zones
execute_firewall_cmd_policy(['--list-egress-zones']).chomp.split(' ') || []
end

def egress_zones=(new_egress_zones)
new_egress_zones ||= []
cur_egress_zones = egress_zones
(new_egress_zones - cur_egress_zones).each do |i|
debug("Adding egress zone '#{i}' to policy #{@resource[:name]}")
execute_firewall_cmd_policy(['--add-egress-zone', i])
end
(cur_egress_zones - new_egress_zones).each do |i|
debug("Removing egress zone '#{i}' from policy #{@resource[:name]}")
execute_firewall_cmd_policy(['--remove-egress-zone', i])
end
end

def priority
execute_firewall_cmd_policy(['--get-priority']).chomp
end

def priority=(new_priority)
execute_firewall_cmd_policy(['--set-priority', new_priority])
end

def masquerade
if execute_firewall_cmd_policy(['--query-masquerade'], @resource[:name], true, false).chomp == 'yes'
:true
else
:false
end
end

def masquerade=(bool)
case bool
when :true
execute_firewall_cmd_policy(['--add-masquerade'])
when :false
execute_firewall_cmd_policy(['--remove-masquerade'])
end
end

def icmp_blocks
get_icmp_blocks
end

def icmp_blocks=(i)
set_blocks = []
remove_blocks = []

case i
when Array then
get_icmp_blocks.each do |remove_block|
unless i.include?(remove_block)
debug("removing block #{remove_block} from policy #{@resource[:name]}")
remove_blocks.push(remove_block)
end
end

i.each do |block|
raise Puppet::Error, 'parameter icmp_blocks must be a string or array of strings!' unless block.is_a?(String)
if get_icmp_types.include?(block)
debug("adding block #{block} to policy #{@resource[:name]}")
set_blocks.push(block)
else
valid_types = get_icmp_types.join(', ')
raise Puppet::Error, "#{block} is not a valid icmp type on this system! Valid types are: #{valid_types}"
end
end
when String then
get_icmp_blocks.reject { |x| x == i }.each do |remove_block|
debug("removing block #{remove_block} from policy #{@resource[:name]}")
remove_blocks.push(remove_block)
end
if get_icmp_types.include?(i)
debug("adding block #{i} to policy #{@resource[:name]}")
set_blocks.push(i)
else
valid_types = get_icmp_types.join(', ')
raise Puppet::Error, "#{i} is not a valid icmp type on this system! Valid types are: #{valid_types}"
end
else
raise Puppet::Error, 'parameter icmp_blocks must be a string or array of strings!'
end
unless remove_blocks.empty?
remove_blocks.each do |block|
execute_firewall_cmd_policy(['--remove-icmp-block', block])
end
end
unless set_blocks.empty? # rubocop:disable Style/GuardClause
set_blocks.each do |block|
execute_firewall_cmd_policy(['--add-icmp-block', block])
end
end
end

# rubocop:disable Style/AccessorMethodName
def get_rules
perm = execute_firewall_cmd_policy(['--list-rich-rules']).split(%r{\n})
curr = execute_firewall_cmd_policy(['--list-rich-rules'], @resource[:name], false).split(%r{\n})
[perm, curr].flatten.uniq
end

def get_services
perm = execute_firewall_cmd_policy(['--list-services']).split(' ')
curr = execute_firewall_cmd_policy(['--list-services'], @resource[:name], false).split(' ')
[perm, curr].flatten.uniq
end

def get_ports
perm = execute_firewall_cmd_policy(['--list-ports']).split(' ')
curr = execute_firewall_cmd_policy(['--list-ports'], @resource[:name], false).split(' ')

[perm, curr].flatten.uniq.map do |entry|
port, protocol = entry.split(%r{/})
debug("get_ports() Found port #{port} protocol #{protocol}")
{ 'port' => port, 'protocol' => protocol }
end
end

def get_icmp_blocks
execute_firewall_cmd_policy(['--list-icmp-blocks']).split(' ').sort
end

def get_icmp_types
execute_firewall_cmd_policy(['--get-icmptypes'], nil).split(' ')
end
# rubocop:enable Style/AccessorMethodName

def description
execute_firewall_cmd_policy(['--get-description'], @resource[:name], true, false)
end

def description=(new_description)
execute_firewall_cmd_policy(['--set-description', new_description], @resource[:name], true, false)
end

def short
execute_firewall_cmd_policy(['--get-short'], @resource[:name], true, false)
end

def short=(new_short)
execute_firewall_cmd_policy(['--set-short', new_short], @resource[:name], true, false)
end

def flush
reload_firewall
end
end
Loading

0 comments on commit 3c83a22

Please sign in to comment.