Search code examples
chef-infrafirewallchef-recipecookbooktest-kitchen

Chef firewall cookbook not working with custom JSON objects


I am playing around with the firewall cookbook: cookbook 'firewall', '~> 2.7.0'

Not sure if it's just me but the instructions are not very clear but I gave it the old college try...

What I am trying to do is build a recipe that allows me to use data_bags and environments to specify rules. Most of the nodes are going to be CentOS 7 but I have a few Ubuntu 16 too (maybe different versions pending third-party software). Most nodes will have 2 NICs, 1 with zone public and another with trusted. Due to this, I would prefer to try to stick to the firewall cookbook if possible.

During development, I used rspec and everything worked as expected. When I went to Test Kitchen, well, that's when I found out nothing was working. So I shelled into the node and realized no rules were being applied.

Ideally, I first want the default zone to be :public. Then I wanted to apply rules to each zone from the json objects. When testing with rspec, it all looked good. When I used Test Kitchen, nothing is even happening.

Hopefully I am just doing something stupid. I am hoping for a push in the right direction here... It should be noted I started this a while back and stopped due to another project. Now I am back on this and I am trying to identify the issues.

Thanks for your time.

An example of an environment (dev):

{
  "name": "dev",
  "description": "DEV Environment for Nodes",
  "chef_type": "environment",
  "json_class": "Chef::Environment",
  "default_attributes": {
    "oly": {
      "environment": "dev",
      "type" : "node",
      "firewall": {
        "status": "enabled",
        "zones": {
          "public": {
            "22": {
              "private_ip_1": "10.0.0.0/8",
              "private_ip_2": "172.16.0.0/12",
              "private_ip_3": "192.168.0.0/16",
              "private_ip_4": "169.254.0.0/16",
              "private_ip_5": "100.64.0.0/10"
            }
          }
        }
      }
    }
  },
  "cookbook_versions": {
    "oly-client": "= 4.0.0"
  }
}

The above environment has a firewall zone config that opens up port 22 for all private IP addresses.

An example of a data_bag (firewall:global) is:

{
  "id": "global",
  "zones": {
    "public": {
      "22": {
          "office_1": "1.1.1.1/32",
          "office_2": "2.2.2.2/32",
          "office_3": "3.3.3.3/32",
          "office_4": "4.4.4.4/32",
          "office_5": "5.5.5.5/32"
      }
    }
  }
}

Ideally, this allows global rules to be applied to the recipe.

The cookbook I am working on:

#
# Cookbook:: oly-client
# Recipe:: firewall
# 
# TODO: Create a method to optimize code (code repetition is real here)

# Fetch firewall settings
_firewallSettings = node['oly']['firewall']

# Make sure we have firewall settings and that they are enabled
if (!_firewallSettings.to_a.empty? && _firewallSettings.key?("status") && 'enabled' == _firewallSettings['status'].downcase)
  # include the base firewall recipe
  include_recipe "firewall::default"

  # Enable platform default firewall and set default zone
  firewall "default" do
    action [:install]
    enabled_zone :public
  end

  # START global firewall rules
  _globalFirewallRules = data_bag_item('firewall', 'global')
  if (_globalFirewallRules && _globalFirewallRules.key?("zones"))

    # Loop over each firewall zone and build rules from data
    _globalFirewallRules['zones'].each do |_zone, _zoneData|

      # Ensure we have zone data
      if (_zoneData)

        # Ensure the firewall is installed for the zone
        firewall "#{_zone}" do
          enabled_zone "#{_zone}".to_sym
          action [:install]
        end

        # Process rules for firewall
        _zoneData.each do |_port, _portRules|
          # Verify rules exist
          if (_portRules)
            # Build rules
            _portRules.each do |_ipComment, _ipAddress|

              # Define rule
              firewall_rule "#{_zone} - #{_port}: #{_ipComment} - #{_ipAddress}" do
                firewall_name "#{_zone}"
                port _port.to_i
                source _ipAddress
                direction :in
                command :allow
              end

            end
          end
        end

        # Save the firewall settings
        firewall "#{_zone}" do
          # action :save
          action [:save]
        end

      end

    end

  end
  # END global firewall rules

  # Check if environment has any zones configured
  if (_firewallSettings.key?("zones"))

    # Loop over each firewall zone and build rules from data
    _firewallSettings['zones'].each do |_zone, _zoneData|

      # Ensure we have zone data
      if (_zoneData)

        # Ensure the firewall is installed for the zone (in case global zones does not include)
        firewall "#{_zone}" do
          enabled_zone "#{_zone}".to_sym
          # action :install
          action [:install]
        end

        # Process rules for firewall
        _zoneData.each do |_port, _portRules|
          # Verify rules exist
          if (_portRules)
            # Build rules
            _portRules.each do |_ipComment, _ipAddress|

              # Define rule
              firewall_rule "#{_zone} - #{_port}: #{_ipComment} - #{_ipAddress}" do
                firewall_name "#{_zone}"
                port _port.to_i
                source _ipAddress
                direction :in
                command :allow
              end

            end
          end
        end

        # Save the firewall settings
        firewall "#{_zone}" do
          # action :save
          action [:save]
        end

      end

    end


  end
  # END environment firewall rules

  # TODO Add logic for custom rules (with search capabilites, like users - Did not do yet as this is edge case if needed at all)

  # Save the firewall settings
  firewall "default" do
    # action :save
    action [:save]
  end

else
  # Firewall is disabled unless explicitly enabled
  include_recipe 'firewall::disable_firewall'
end

My rspec test (reaplaced IPs but should work the same):

#
# Cookbook:: oly-client
# Spec:: default
#
# Copyright:: 2017, The Authors, All Rights Reserved.

require 'spec_helper'

describe 'oly-client::firewall' do

  context 'on CentOS 7 Latest' do

    let(:chef_run) do
      ChefSpec::SoloRunner.new(platform: 'centos', version: '7') do |node|

        # Build node attributes for tests
        node.normal['oly']['firewall']['status'] = "enabled"
        node.normal['oly']['firewall']['zones'] = {
          "public": {
            "22": {
              "private_ip_1": "10.0.0.0/8",
              "private_ip_2": "172.16.0.0/12",
              "private_ip_3": "192.168.0.0/16",
              "private_ip_4": "169.254.0.0/16"
            }
          },
          "trusted": {
            "22": {
              "private_ip_5": "100.64.0.0/10"
            }
          }
        }

        # Firewall rules
        node.normal['firewall']['allow_icmp'] = true
        node.normal['firewall']['allow_ssh'] = true
        node.normal['firewall']['allow_winrm'] = false
        node.normal['firewall']['allow_mosh'] = false

      end.converge(described_recipe)
    end

    # Stub databags
    before do
      stub_data_bag('firewall').and_return(['global'])
      stub_data_bag_item('firewall', 'global').and_return({
        "id": "global",
        "zones": {
          "public": {
            "22": {
                  "office_1": "1.1.1.1/32",
                  "office_2": "2.2.2.2/32",
                  "office_3": "3.3.3.3/32"
            }
          },
          "trusted": {
            "22": {
              "office_1": "1.1.1.1/32",
              "office_3": "3.3.3.3/32",
              "office_4": "4.4.4.4/32",
              "office_5": "5.5.5.5/32"
            }
          }
        }
      })
    end

    it 'include the recipe to enable firewall' do
      expect(chef_run).to include_recipe('firewall::default')
    end

    it 'enables the firewall' do
      expect(chef_run).to install_firewall('public')
      expect(chef_run).to install_firewall('trusted')
    end

    it 'creates some rules' do
      _rules = [
        "allow loopback", 
        "allow icmp", 
        "allow world to ssh", 
        "established",
        "ipv6_icmp",
        "public - 22: private_ip_1 - 10.0.0.0/8",
        "public - 22: private_ip_2 - 172.16.0.0/12",
        "public - 22: private_ip_3 - 192.168.0.0/16",
        "public - 22: private_ip_4 - 169.254.0.0/16",
        "trusted - 22: private_ip_5 - 100.64.0.0/10",
        "public - 22: office_1 - 1.1.1.1/32",
        "public - 22: office_2 - 2.2.2.2/32",
        "public - 22: office_3 - 3.3.3.3/32",
        "trusted - 22: office_1 - 1.1.1.1/32",
        "trusted - 22: office_3 - 3.3.3.3/32",
        "trusted - 22: office_4 - 4.4.4.4/32",
        "trusted - 22: office_5 - 5.5.5.5/32"
      ]

      _rules.each do |r|
        expect(chef_run).to create_firewall_rule(r)
      end
    end


    it 'not to creates some rules' do
      _rules = [
        "allow world to winrm", 
        "allow world to mosh",
        "public - 22: office_4 - 4.4.4.4/32",
        "public - 22: office_5 - 5.5.5.5/32",
        "trusted - 22: office_2 - 2.2.2.2/32"
      ]

      _rules.each do |r|
        expect(chef_run).to_not create_firewall_rule(r)
      end
    end

  end

end


Solution

  • Zones are not currently supported in the cookbook. I submitted a PR to add the support. While the documentation is not fully clear for the cookbook, the issues I posted here are due to a missing feature for firewalld.