diff --git a/README.md b/README.md
index 4a1b64f66696b2b820af7cd566f5645574f0535e..2ba8268ef1bd9d5077dd5bd23b3cbc1605c93506 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 0000000000000000000000000000000000000000..68f283bc1c6576f9dc68de100c7af7a72c53b6c9
--- /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 0000000000000000000000000000000000000000..c41b988d603d6f5531f1b06fa84d1c0f7d653b3c
--- /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 0000000000000000000000000000000000000000..72d5be02e40ad656b4714db813f0756dc00b77f6
--- /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 0000000000000000000000000000000000000000..53509af5641c18771d24ce438d8120098024f3ec
--- /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 0000000000000000000000000000000000000000..54cb6475085b6c31cbc1776c60fd375af752210c
--- /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 0000000000000000000000000000000000000000..9d70bff1de0754d80e8645ab02461204c289a3ba
--- /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'
+    ]
+  ]
+]