Chef and iptables

4 minute read

Like many DevOps professionals before me, I needed a way to manage iptables programmatically through Chef. Before I realized that Opscode had written a cookbook for just this, I had already spent some time building my own. The cookbook written by Opscode is very clever and allows you to apply a set of iptables rules with a single line in another recipe. The only problem is that it is predicated on you creating 'templates' of iptables policies you want to apply in advance, and you can only apply those templates. You can't just supply a port number or part of a rule. Additionally, their cookbook wasn't compatible with Vagrant because of the ruby binary wasn't in the paths used in the cookbook.

Short version - I wrote my own iptables cookbook and augmented the Opscode iptables cookbook, and I wanted to share.

Changes to the Opscode Cookbook

Basically, the cookbook functionality is unchanged save for two parts. First, in addition to the previous method for invoking iptables, you can now invoke it by setting the ['iptables']['roles'] attribute to one or more roles (comma delimited). There is a new 'default.rb' attribute file and the default recipe has been modified slighly to support this. Second, the default recipe has been modified to detect the path to the ruby binary so as to be compatible with Vagrant boxes instead of being hardcoded.

You can check out all of the code on Github.

My iptables Cookbook

My requirements were a little less generic, and so my iptables cookbook is a little more isolated than the Opscode version. Specifically, I didn't want to affect any existing iptables rules or chains, so my cookbook injects new chains that should be specific for the servers application (hence App-INPUT and App-OUTPUT chains). Unfortunately, today my cookbook only supports ports to be specified via attributes and only works on Enterprise Linux OS's. Having seen and played with Opscode version, I'll have to refactor mine to support Debian Linux versions as well as non-attribute driven usage.

To see the full cookbook or to clone, check out the repo on Github. Otherwise the bulk of the logic is in the default recipe, which you can check out here:

# Cookbook Name:  inline-iptables
# Recipe:         default
# Author:         Alex D Glover (alex@alexdglover.com)
# Description:    A recipe to manage iptables without impacting existing
#   iptables rules or chains, and allowing individual
#   inbound/outbound ports as well as port ranges to be
#   managed with programmatic ease.
# Dependencies:   attributes/default.rb
# Usage:          Set the ["inline-iptables"]["listen_ports"] and/or
#   ["inline-iptables"]["outbound_ports"] attributes at the
#   node or role level


# Grab the comma separated list of ports that need to be open.
# Keep in mind these attribute could be set by a role, recipe, or node override
listen_ports      = node["inline-iptables"]["listen_ports"]
outbound_ports    = node["inline-iptables"]["outbound_ports"]

# Boolean variable to track if we a change has been made
iptables_modified = false

# Debug friendly logging
Chef::Log.info <<-EOS

Entering ondemand_base::iptables_manager {
  listen_ports        = #{listen_ports}
  outbound_ports      = #{outbound_ports}
}
EOS

# Read the current iptables data
iptables_content = File.read("/etc/sysconfig/iptables")

# If the list of ports is the empty string, do nothing
unless listen_ports == ""

  # We are creating a new iptables chain called App-INPUT; if it already
  # exists, don't create it again
  unless iptables_content.include?(":App-INPUT - [")
    execute "create new INPUT chain" do
      command "iptables -N App-INPUT;"
    end
    iptables_modified = true
  end

  # Break our port list into an array
  listen_ports_array = listen_ports.split(',')

  # For each port that needs to be opened, insert the corresponding rule
  # into our chain, but only if doesn't exist already
  listen_ports_array.each do |port|
    unless iptables_content.include?("-A App-INPUT -p tcp -m tcp --dport #{port} -j ACCEPT")
      execute "add input rule to iptables for port #{port}" do
        command "iptables -I App-INPUT -p tcp --dport #{port} -j ACCEPT"
      end
      iptables_modified = true
    end
  end

  # Connect our App-INPUT chain to the generic INPUT chain
  unless iptables_content.include?("-A INPUT -j App-INPUT")
    execute "connect App-INPUT to INPUT chain" do
      command "iptables -I INPUT -j App-INPUT"
    end
    iptables_modified = true
  end

  # Connect our App-INPUT chain to the generic FORWARD chain
  unless iptables_content.include?("-A FORWARD -j App-INPUT")
    execute "connect App-INPUT to FORWARD chain" do
      command "iptables -I FORWARD -j App-INPUT"
    end
    iptables_modified = true
  end

else
  Chef::Log.info "No listen ports specified to be opened"
end

# If the list of ports is the empty string, do nothing
unless outbound_ports == ""

  # We are creating a new iptables chain called App-OUTPUT; if it already
  # exists, don't create it again
  unless iptables_content.include?(":App-OUTPUT - [")
    execute "create new OUTPUT chain" do
      command "iptables -N App-OUTPUT;"
    end
    iptables_modified = true
  end

  # Break our port list into an array
  outbound_ports_array = outbound_ports.split(',')

  # For each port that needs to be opened, insert the corresponding rule
  # into our chain, but only if doesn't exist already
  outbound_ports_array.each do |port|
    execute "add input rule to iptables for port #{port}" do
      command "iptables -I App-OUTPUT -p tcp --dport #{port} -j ACCEPT"
      only_if {!iptables_content.include?("-A App-OUTPUT -p tcp -m tcp --dport #{port} -j ACCEPT")}
    end
    iptables_modified = true
  end

  # Connect our App-OUTPUT chain to the generic OUTPUT chain
  unless iptables_content.include?("-A OUTPUT -j App-OUTPUT")
    execute "connect App-OUTPUT to OUTPUT chain" do
      command "iptables -I OUTPUT -j App-OUTPUT"
    end
    iptables_modified = true
  end

else
Chef::Log.info "No outbound ports specified to be opened"
end

if iptables_modified

  execute "save updated iptables" do
    command "/etc/init.d/iptables save"
  end

  service "iptables" do
    action :restart
    ignore_failure true
  end
else
  Chef::Log.info "No changes made, not restarting iptables"
end

Would greatly appreciate any feedback/criticism from the Chef community. Thanks for reading.

Leave a Comment