From 203699d4ebf933be779b789f7ede9f769c088ffb Mon Sep 17 00:00:00 2001 From: Massimiliano Adamo <maxadamo@gmail.com> Date: Wed, 21 Dec 2022 17:01:51 +0100 Subject: [PATCH] initial commit --- README.md | 76 +++++++++++- functions/fw_builder.pp | 186 +++++++++++++++++++++++++++++ functions/fw_builder_public_ips.pp | 87 ++++++++++++++ functions/parser.pp | 57 +++++++++ types/iplist.pp | 5 + types/list.pp | 5 + types/puppet_environment.pp | 17 +++ 7 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 functions/fw_builder.pp create mode 100644 functions/fw_builder_public_ips.pp create mode 100644 functions/parser.pp create mode 100644 types/iplist.pp create mode 100644 types/list.pp create mode 100644 types/puppet_environment.pp diff --git a/README.md b/README.md index 4a1b64f..2ba8268 100644 --- a/README.md +++ b/README.md @@ -1 +1,75 @@ -## module for app fw_builder +# Module for Firewall Builder + +## Table of Contents + +1. [Firewall builder](#firewall-builder) +2. [Fail2ban allow list](#fail2ban-allow-list) + +## Firewall builder <a name="firewall-builder"></a> + +This is a configuration example in Hiera: + +```yaml +firewall: + # in this section you can group IPs + custom_ipset: + authz: + # specify a list of IPs, Networks, FQDNs under the name "authz" + # FQDN elements must have at least A or AAAA record + list: + - '150.254.166.19' + - '150.254.208.67' + - '2001:718:ff05:206::155' + - '2001:718:ff05:206::166' + - "www.geant.org" + - "www.google.uz" + rackspace: + # specify a list of IPs or Networks under the name "rackspace" + list: + - '134.213.42.227' + - '2a00:1a48:15e1:4:33d7:40d8:d11f:abb5' + - '134.213.42.228' + - '2a00:1a48:15e1:4:31d5:3397:4fd6:7ad3' + - '10.100.4.0/24' + - "2001:630:280:20::/64" + # run a hieradata lookup against an array of IPs, FQDNs existing in + # hiera and group them under the name "rackspace" + hieradata: + - "haproxy_servers" + - "db_servers" + # run a query on the puppetDB, produces a list of IPs + # and group them under the name "rackspace". + # An empty match will cause a puppet fail + puppetdb: + - { 'name': 'myserver\d+\.geant\.org' } # by default it matches the same environment of the host + - { 'name': 'otherserver\d+\.geant\.org', 'env': 'uat' } + - { 'name': 'moreservers\d+\.geant\.org', 'env': ['uat', 'test'] } + # this section contains addresses NOT belonging to the internal network + # if you do not specify an "ipset", the connection will be open world-wide + # ipsets from the public section will be added to "fail2ban" ignoreip list + public: + # open http/https to everyone + web: + port: [80, 443] + # open ldap/ldaps to ipset groups "authz" and "rackspace" + ldap_ports: + port: [389, 636] + proto: ['tcp'] + ipset: ["authz", "rackspace"] + # this section contains addresses belonging to the internal network + # if you do not specify an "ipset" it will open to everyone in the internal network + trust: + # open the specified ports to all internal networks + trusted_ports: + port: [389, 443, 636, 3000, 3001, 3268, 6379, 6380, 8000] + proto: ['tcp'] + # open DB ports to "haproxy_servers" + db_ports: + port: [3306] + proto: ['tcp'] + ipset: "haproxy_servers" +``` + +## Fail2ban allow list <a name="fail2ban-allow-list"></a> + +ipsets listed in the "public" section of the firewall builder will be added to Fail2ban allow-list. diff --git a/functions/fw_builder.pp b/functions/fw_builder.pp new file mode 100644 index 0000000..68f283b --- /dev/null +++ b/functions/fw_builder.pp @@ -0,0 +1,186 @@ +# == Function: fw_builder::fw_builder +# +# tries to build the ipset and firewall rule set comming from hieradata +#Â this function only affects the host firewall not the VMware crap +# +# == Example +# +# Please check the main README: ../README.md +# +function fw_builder::fw_builder() { + + # time to retrieve hieradata for the firewall + # + $fw_conf = lookup('firewall', Hash, 'deep') + $fw_conf_hash = { fw_conf => $fw_conf } + + file { + ['/etc/facter/', '/etc/facter/facts.d/']: + ensure => 'directory'; + '/etc/facter/facts.d/fw_conf.yaml': + content => to_yaml($fw_conf_hash), + } + + # first define if we need to generate all the custom ipset lists that have been + # define, for latter use + # + if ($fw_conf['custom_ipset']) { + + $ipsets = $fw_conf['custom_ipset'].keys().map |$name| { + + # check if key names are valid + # + $ipset_keys = keys($fw_conf['custom_ipset'][$name]) + $ipset_keys.each |$key| { + unless $key in ['list', 'hieradata', 'puppetdb'] { + fail("${key} is not a valid key. Valid keys are: 'list', 'hieradata', and 'puppetdb'") + } + } + + # ipset name is limited to 26 characters: 7 characters are surrounding the string as follows: "fwb_${name}_v4" + if $name.length() > 19 { fail("ipset name ${name} cannot exceed 19 characters") } + + # getting list of IPs, Networks, and/or FQDNs + # + if ($fw_conf['custom_ipset'][$name]['list']) { + $_list = $fw_conf['custom_ipset'][$name]['list'] + if $_list !~ Fw_builder::List { fail("${_list} types are not IPs, Networks or FQDNs") } + $list = fw_builder::parser($_list) + } + + # getting IPs or FQDNs from 'hieradata' lookup, if 'hieradata' is defined + # + if ($fw_conf['custom_ipset'][$name]['hieradata']) { + $_hieradata = flatten($fw_conf['custom_ipset'][$name]['hieradata'].map |$hash_name| { + lookup($hash_name, Array, 'deep') + }) + if $_hieradata !~ Fw_builder::List { fail("${_hieradata} types are not IPs, Networks or FQDNs") } + $hieradata = fw_builder::parser($_hieradata) + } + + # querying 'puppetDB', if 'puppetdb' is defined + # + if ($fw_conf['custom_ipset'][$name]['puppetdb']) { + # check if "env" was defined and it contains proper values + $pdb_filter = join( + $fw_conf['custom_ipset'][$name]['puppetdb'].map |$hash| { + if $hash['env'] { + # wrong setting for "env" creates an empty fact and breaks puppet on the host + if $hash['env'] !~ Fw_builder::Puppet_environment { + fail("${hash['env']} is an unacceptable value for 'env'. Valid values are 'test', 'uat', or 'production'") + } else { + if $hash['env'] =~ String { + $env_string = "= '${hash['env']}'" + } elsif $hash['env'] =~ Array { + # if we use something like [%{::environment}, 'test'] we need unique + $_env_string = join(unique($hash['env']), '|') + $env_string = "~ '(${_env_string})'" + } + } + } else { + # we use the same environment of the agent + $env_string = "= '${::environment}'" + } + "facts.fqdn ~ '${hash[name]}' and facts.agent_specified_environment ${env_string}" + }, + ') or (' + ) + # $pdb_filter example: + # facts.fqdn ~ 'nomad\d+\.geant\.org' and facts.agent_specified_environment = 'test') or (facts.fqdn ~ ... + $query = "inventory[facts.hostname, facts.ipaddress, facts.ipaddress6, facts.fqdn] { (${pdb_filter}) order by certname }" + $full_list = puppetdb_query($query) + $searchlist = $full_list.map |$hash| { $hash['facts.ipaddress']} + $full_list.map |$hash| { $hash['facts.ipaddress6'] } + # an empty list creates an empty fact, it means that the regex is not working + # and the firewall setting is ineffective. We better fail here + if $searchlist !~ Fw_builder::Iplist { + fail('PuppetDB query for Firewall Builder did not match any host. You may want to review you regex') + } + } + + # create a list with all the ip's + # + $full_ip_list = flatten([$list, $fqdnlist, $searchlist, $hieradata]).filter |$val| { $val =~ NotUndef } + + # if we have a non zero list then let's create / update it + # + if $full_ip_list.length() > 0 { + $full_ip_list_sorted = sort($full_ip_list) + # time to create the ipset with all the data/ip's .... + ipset::set { + default: + type => 'hash:net', + ensure => 'present'; + "fwb_${name}_v4": + set => $full_ip_list_sorted.filter |$ip| { $ip =~ Stdlib::IP::Address::V4 }; + "fwb_${name}_v6": + set => $full_ip_list_sorted.filter |$ip| { $ip =~ Stdlib::IP::Address::V6 }, + options => { + 'family' => 'inet6' + } + }; + { $name => $full_ip_list } + } + } + } else { + $ipsets = [] + } + + file { '/etc/facter/facts.d/fw_ipsets.yaml': + content => to_yaml({fw_ipsets => $ipsets}); + } + + # this section will setup / create all the fwb rules + # + ['public', 'trust'].each() |$zone| { + if $fw_conf[$zone] { + $fw_conf[$zone].each |$name , $conf| { + $ports_spaces = $conf['port'] ? { + Array => join($conf['port'], ' '), + String => $conf['port'], + Integer => $conf['port'], + default => fail("'port' can only be Array, String or Integer") + } + + if $conf['ipset'] { + #Â this part will generate all the rule that are restricted with ipset + $ipset_array = $conf['ipset'] ? { + String => [$conf['ipset']], + Array => $conf['ipset'], + default => fail("'ipset' can only be Array, or String") + } + $ipset_array.each |$ipset_element| { + firewall_multi { + default: + chain => "INPUT_${zone}", + proto => $conf[proto], + dport => $conf[port], + action => accept; + "150 fwb INPUT_${zone} Allow inbound ${name} port(s): ${ports_spaces} ipset:fwb_${ipset_element}_v4": + ipset => "fwb_${ipset_element}_v4 src", + provider => 'iptables'; + "150 fwb INPUT_${zone} Allow inbound ${name} port(s): ${ports_spaces} ipset:fwb_${ipset_element}_v6": + ipset => "fwb_${ipset_element}_v6 src", + provider => 'ip6tables'; + } + } + } else { + firewall_multi { + default: + chain => "INPUT_${zone}", + proto => $conf[proto], + dport => $conf[port], + action => accept; + "150 fwb INPUT_${zone} Allow inbound ${name} port(s): ${ports_spaces} v4": + provider => 'iptables'; + "150 fwb INPUT_${zone} Allow inbound ${name} port(s): ${ports_spaces} v6": + provider => 'ip6tables'; + } + } + } + } else { + echo { "FW Builder zone ${zone}": message => "No work to do in ${zone} !! (this could be normal)" } + } + } + + [$fw_conf, $ipsets] +} diff --git a/functions/fw_builder_public_ips.pp b/functions/fw_builder_public_ips.pp new file mode 100644 index 0000000..c41b988 --- /dev/null +++ b/functions/fw_builder_public_ips.pp @@ -0,0 +1,87 @@ +# == Function: fw_builder::fw_builder_public_ips +# +# create an array of IPs listed in the public section +# of the firewall builder. +# +# === Parameters +# +# [*facts_fw_conf*] +# custom fact: fw builder configuration, including the public IPs +# +# [*facts_ipsets*] +# custom fact: ipsets pushed by fw builder +# +# === Variables +# +# [*public_ips*] +# IPs without subnet +# +# [*public_cidr*] +# IPs with subnet +# +function fw_builder::fw_builder_public_ips( + Variant[String, Hash, Undef] $facts_fw_conf, + Optional[Array] $facts_ipsets +) >> Array { + + if $facts_fw_conf =~ Undef or $facts_ipsets =~ Undef { + # when puppet runs for the first time these facts are not available + $public_ipsets = [] + } elsif $facts_fw_conf['public'] =~ String { + # if public is empty it's seen as empty string + $public_ipsets = [] + } else { + if 'public' in $facts_fw_conf { + # this check is not needed, but it will be necessary if the + # code of fw_builder changes and "public" can be absent + + $facts_fw_conf_public = $facts_fw_conf['public'] + + # create a list of lists with all the ipsets in public + $unflattened_public_ipsets = $facts_fw_conf_public.map |$app_key, $app_value| { + if 'ipset' in keys($facts_fw_conf_public[$app_key]) { + $facts_fw_conf_public[$app_key]['ipset'] + } + } + + # flatten the list of list into a list with unique elements, and remove any Undef + $public_ipsets_with_undef = unique(flatten($unflattened_public_ipsets)) + $public_ipsets = $public_ipsets_with_undef.filter |$item| { $item !~ Undef } + } else { + $public_ipsets = [] + } + } + + # if we got ipsets in public, we parse them, we collect the corresponding IPs + # and we add them to "public_cidr" list + # + if $public_ipsets.length > 0 { + # create a list of lists with all the IPs associated with the ipsets in public + $unflattened_public_ips = $facts_ipsets.map |$index, $value| { + if keys($facts_ipsets[$index])[0] in $public_ipsets { + $key_name = keys($facts_ipsets[$index])[0] + $facts_ipsets[$index][$key_name] + } + } + + # flatten the list of list into a list with unique elements, and remove any Undef + $public_ips_with_undef = unique(flatten($unflattened_public_ips)) + $public_ips = $public_ips_with_undef.filter | $item | { $item !~ Undef } + + # add /32 to IPv4, add /128 to IPv6, add nothing to CIDR + $public_cidr = $public_ips.map |$ip| { + if $ip =~ Stdlib::IP::Address::V4::Nosubnet { + "${ip}/32" + } elsif $ip =~ Stdlib::IP::Address::V6::Nosubnet { + "${ip}/128" + } elsif $ip =~ Stdlib::IP::Address::V4::CIDR or $ip =~ Stdlib::IP::Address::V6::CIDR { + $ip + } + } + } else { + # there are no ipsets in public: we don't need to change fail2ban + $public_cidr = [] + } + + $public_cidr +} diff --git a/functions/parser.pp b/functions/parser.pp new file mode 100644 index 0000000..72d5be0 --- /dev/null +++ b/functions/parser.pp @@ -0,0 +1,57 @@ +# == Function: fw_builder::parser +# +# parse elements and add subnet if necessary +# it does not work quite well with IPv4 and ipset but it doesn't cause any issue +# +# === Parameters +# +# [*facts_fw_conf*] +# custom fact: fw builder configuration, including the public IPs +# +# [*facts_ipsets*] +# custom fact: ipsets pushed by fw builder +# +# === Variables +# +# [*public_ips*] +# IPs without subnet +# +# [*public_cidr*] +# IPs with subnet +# +function fw_builder::parser(Array $ip_array) >> Array { + + if $ip_array.length > 0 { + $unflattened_cidr_array = $ip_array.map |$ip| { + if $ip =~ Stdlib::IP::Address::V4::Nosubnet { + "${ip}/32" + } elsif $ip =~ Stdlib::IP::Address::V6::Nosubnet { + "${ip}/128" + } elsif $ip =~ Stdlib::IP::Address::V4::CIDR or $ip =~ Stdlib::IP::Address::V6::CIDR { + $ip + } elsif $ip =~ Stdlib::Fqdn { + $ipv4 = dns_a($ip)[0] + $ipv6 = dns_aaaa($ip)[0] + if ($ipv4) { + $ipv4_subnetted = "${ipv4}/32" + } else { + $ipv4_subnetted = undef + } + if ($ipv6) { + $ipv6_subnetted = downcase("${ipv6}/128") + } else { + $ipv6_subnetted = undef + } + # if we cannot resolve either ipv4 and ipv6 we fail here + if $ipv4 == undef and $ipv6 == undef { fail("${ip} does not have a DNS entry. Please amend the configuration") } + [$ipv4_subnetted, $ipv6_subnetted] + } + } + $cidr_array_with_undef = unique(flatten($unflattened_cidr_array)) + $cidr_array = $cidr_array_with_undef.filter | $item | { $item !~ Undef } + } else { + $cidr_array = [] + } + + $cidr_array +} diff --git a/types/iplist.pp b/types/iplist.pp new file mode 100644 index 0000000..53509af --- /dev/null +++ b/types/iplist.pp @@ -0,0 +1,5 @@ +# +# @summary Allowed types in Iplist +# +# +type Fw_builder::Iplist = Array[Stdlib::IP::Address] diff --git a/types/list.pp b/types/list.pp new file mode 100644 index 0000000..54cb647 --- /dev/null +++ b/types/list.pp @@ -0,0 +1,5 @@ +# +# @summary Allowed types in List +# +# +type Fw_builder::List = Array[Variant[Stdlib::IP::Address, Stdlib::Fqdn]] diff --git a/types/puppet_environment.pp b/types/puppet_environment.pp new file mode 100644 index 0000000..9d70bff --- /dev/null +++ b/types/puppet_environment.pp @@ -0,0 +1,17 @@ +# +# @summary Allowed Puppet Environments +# +# +type Fw_builder::Puppet_environment = Variant[ + Enum[ + 'test', + 'uat', + 'production' + ], + Array[Enum[ + 'test', + 'uat', + 'production' + ] + ] +] -- GitLab