### Overview
-This type provides the capability to manage firewall rules within
-puppet.
+This module provides the resource 'firewall' which provides the capability to
+manage firewall rules within puppet.
Current support includes:
* iptables
* ip6tables
+With the resource 'firewallchain' we also provide a mechanism to manage chains
+for:
+
+* iptables
+* ip6tables
+* ebtables
+
### Disclaimer
Warning! While this software is written in the best interest of quality it has
table => 'nat',
}
+Creating a new rule that forwards to a chain, then adding a rule to this chain:
+
+ firewall { '100 forward to MY_CHAIN':
+ chain => 'INPUT',
+ jump => 'MY_CHAIN',
+ require => Firewallchain["filter:MY_CHAIN:IPv4"],
+ }
+ firewallchain { 'filter:MY_CHAIN:IPv4':
+ ensure => present,
+ }
+ firewall { '100 my rule':
+ chain => 'MY_CHAIN',
+ action => 'accept',
+ proto => 'tcp',
+ dport => 5000,
+ }
+
You can make firewall rules persistent with the following iptables example:
exec { "persist-firewall":
Firewall {
notify => Exec["persist-firewall"]
}
+ Firewallchain {
+ notify => Exec["persist-firewall"]
+ }
If you wish to ensure any reject rules are executed last, try using stages.
The following example shows the creation of a class which is where your
* iptables
* ip6tables
+* ebtables (chains only)
But plans are to support lots of other firewall implementations:
-
Puppet::Type.type(:firewallchain).provide :iptables_chain do
@doc = "Iptables chain provider"
has_feature :iptables_chain
has_feature :policy
- commands :iptables => '/sbin/iptables'
- commands :iptables_save => '/sbin/iptables-save'
-
- commands :ip6tables => '/sbin/ip6tables'
- commands :ip6tables_save => '/sbin/ip6tables-save'
-
- optional_commands( { :ebtables => '/sbin/ebtables',
- :ebtables_save => '/sbin/ebtables-save'
- } )
+ optional_commands({
+ :iptables => '/sbin/iptables',
+ :iptables_save => '/sbin/iptables-save',
+ :ip6tables => '/sbin/ip6tables',
+ :ip6tables_save => '/sbin/ip6tables-save',
+ :ebtables => '/sbin/ebtables',
+ :ebtables_save => '/sbin/ebtables-save',
+ })
defaultfor :kernel => :linux
# chain name is greedy so we anchor from the end.
# [\d+:\d+] doesn't exist on ebtables
- Mapping = { :IPv4 => { :tables => method( :iptables ), :save => method( :iptables_save), :re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/ },
- :IPv6 => { :tables => method( :ip6tables ), :save => method( :ip6tables_save ), :re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/ },
- :ethernet => { :tables => method( :ebtables ), :save => method( :ebtables_save ), :re => /^:(.+)\s(\S+)$/ }
- }
+ Mapping = {
+ :IPv4 => {
+ :tables => method(:iptables),
+ :save => method(:iptables_save),
+ :re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/,
+ },
+ :IPv6 => {
+ :tables => method(:ip6tables),
+ :save => method(:ip6tables_save),
+ :re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/,
+ },
+ :ethernet => {
+ :tables => method(:ebtables),
+ :save => method(:ebtables_save),
+ :re => /^:(.+)\s(\S+)$/,
+ }
+ }
InternalChains = /^(PREROUTING|POSTROUTING|BROUTING|INPUT|FORWARD|OUTPUT)$/
- Tables = 'NAT|MANGLE|FILTER|RAW|RAWPOST|BROUTE|'
- Nameformat = /^(#{Tables}):(.+):(IP(v[46])?|ethernet|)$/
+ Tables = 'nat|mangle|filter|raw|rawpost|broute'
+ Nameformat = /^(#{Tables}):(.+):(IP(v[46])?|ethernet)$/
def create
# can't create internal chains
else
debug "Inserting chain #{chain} on table #{table} (#{protocol}) using #{t}"
t.call ['-t',table,'-N',chain]
- if @resource[:policy] != :empty
- t.call ['-t',table,'-P',chain,@resource[:policy].to_s.upcase]
+ unless @resource[:policy].nil?
+ t.call ['-t',table,'-P',chain,@resource[:policy].to_s.upcase]
end
end
end
end
allvalidchains do |t, table, chain|
debug "Deleting chain #{chain} on table #{table}"
- t.call ['-t',table,'-X',chain]
+ t.call ['-t',table,'-X',chain]
end
end
def exists?
- # we want puppet to call create on 1/2 completed rules (i.e. :ensure => :IPv4/6)
properties[:ensure] == :present
end
def policy=(value)
return if value == :empty
allvalidchains do |t, table, chain|
- p = ['-t',table,'-P',chain,value.to_s.upcase]
+ p = ['-t',table,'-P',chain,value.to_s.upcase]
debug "[set policy] #{t} #{p}"
t.call p
end
end
end
+ def flush
+ debug("[flush]")
+ # Clear the property hash so we re-initialize with updated values
+ @property_hash.clear
+ end
+
# Look up the current status. This allows us to conventiently look up
# existing status with properties[:foo].
def properties
if @property_hash.empty?
@property_hash = query || {:ensure => :absent}
- #@property_hash[:ensure] = :absent if @property_hash.empty?
end
@property_hash.dup
end
debug "[instances]"
table = nil
chains = []
- hash = {}
Mapping.each { |p, c|
begin
- c[:save].call.split("\n").each do |line|
+ c[:save].call.each_line do |line|
if line =~ c[:re] then
- name = (table == 'filter' ? '' : table.upcase) + ':' + $1
- policy = $2 == '-' ? :empty : $2.downcase.to_sym
- if ( p == :IPv4 or p == :IPv6 ) && table != 'nat'
- if hash[name]
- # duplicate so create a {table}:{chain}:IP instance
- ippolicy = hash[name][:policy] == policy ? policy : :inconsistent
- hash.delete(name)
- chains << new({:name => name + ':', :policy => ippolicy, :ensure => :present })
- debug "[dup] '#{name}:' #{ippolicy}"
- else
- hash[name] = { :policy => policy, :protocol => p }
- end
- end
- name += ':' + p.to_s
- chains << new({:name => name, :policy => policy, :ensure => :present })
+ name = (table == 'filter' ? 'filter' : table) + ':' + $1 + ':' + p.to_s
+ policy = $2 == '-' ? nil : $2.downcase.to_sym
+
+ chains << new({
+ :name => name,
+ :policy => policy,
+ :ensure => :present,
+ })
+
debug "[instance] '#{name}' #{policy}"
elsif line =~ /^\*(\S+)/
table = $1
- elsif line =~ /^($|-A|COMMIT|#)/
- # other stuff we don't care about
else
- debug "unrecognised line: #{line}"
+ next
end
end
rescue Puppet::Error
# ignore command not found for ebtables or anything that doesn't exist
end
}
- # put all the chain names that exist in one IP stack into a 1/2 completed (:ensure) state
- # The create method will check this and complete only what's required
- hash.each { |key, value|
- x = {:name => key + ':', :ensure => value[:protocol], :policy => :empty}
- debug "halfstate #{x.inspect}"
- chains << new(x)
- }
+
chains
end
def allvalidchains
@resource[:name].match(Nameformat)
- table = ($1=='') ? 'filter' : $1.downcase
+ table = $1
chain = $2
protocol = $3
- if protocol == 'IP' || protocol == ''
- yield Mapping[:IPv4][:tables],table,chain,:IPv4
- yield Mapping[:IPv6][:tables],table,chain,:IPv6
- else
- yield Mapping[protocol][:tables],table,chain,protocol.to_sym
- end
+ yield Mapping[protocol.to_sym][:tables],table,chain,protocol.to_sym
end
-
+
end
-
Puppet::Type.newtype(:firewallchain) do
@doc = <<-EOS
- This type provides the capability to manage iptables chains and policies on
- internal chains within puppet.
- EOS
+ This type provides the capability to manage rule chains for firewalls.
- #InternalChains = /^(PREROUTING|POSTROUTING|BROUTING|INPUT|FORWARD|OUTPUT)$/
- #Tables = 'NAT|MANGLE|FILTER|RAW|RAWPOST|BROUTE|'
- ## Technically colons (':') are allowed in table names however it requires
- ## ruby-1.9 to do a regex to allow a backslash escaping of the colon.
- ## ruby-1.9 regex: Nameformat = /^(<table>#{Tables}):(<chain>([^:]*(?<!\\))+):(<protocol>IP(v[46])?|EB)?$/
- #Nameformat = /^(#{Tables}):([^:]+):(IP(v[46])?|ethernet:)$/
+ Currently this supports only iptables, ip6tables and ebtables on Linux. And
+ provides support for setting the default policy on chains and tables that
+ allow it.
+ EOS
feature :iptables_chain, "The provider provides iptables chain features."
feature :policy, "Default policy (inbuilt chains only)"
- #autorequire(:firewallchain) do
- # if @parameters[:name] =~ /:$/
- # [ @parameters[:name] + 'IPv4', @parameters[:name] + 'IPv6']
- # end
- #end
-
ensurable do
defaultvalues
defaultto :present
newparam(:name) do
desc <<-EOS
The canonical name of the chain.
+
+ For iptables the format must be {chain}:{table}:{protocol}.
EOS
isnamevar
validate do |value|
if value !~ Nameformat then
- raise ArgumentError, "Inbuilt chains must be in the form {chain}:{table}:{protocol} where {table} is one of FILTER, NAT, MANGLE, RAW, RAWPOST, BROUTE or empty (alias for filter), chain can be anything without colons or one of PREROUTING, POSTROUTING, BROUTING, INPUT, FORWARD, OUTPUT for the inbuilt chains, and {protocol} being empty or IP (both meaning IPv4 and IPv6), IPv4, IPv6, ethernet (ethernet bridging) got '#{value}' table:'#{$1}' chain:'#{$2}' protocol:'#{$3}'"
- else
+ raise ArgumentError, "Inbuilt chains must be in the form {table}:{chain}:{protocol} where {table} is one of FILTER, NAT, MANGLE, RAW, RAWPOST, BROUTE or empty (alias for filter), chain can be anything without colons or one of PREROUTING, POSTROUTING, BROUTING, INPUT, FORWARD, OUTPUT for the inbuilt chains, and {protocol} being IPv4, IPv6, ethernet (ethernet bridging) got '#{value}' table:'#{$1}' chain:'#{$2}' protocol:'#{$3}'"
+ else
table = $1
chain = $2
protocol = $3
case table
- when /^(FILTER|)$/
+ when 'filter'
if chain =~ /^(PREROUTING|POSTROUTING|BROUTING)$/
raise ArgumentError, "INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table 'filter'"
end
- when 'MANGLE'
+ when 'mangle'
if chain =~ InternalChains && chain == 'BROUTING'
raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table 'mangle'"
end
- when 'NAT'
+ when 'nat'
if chain =~ /^(BROUTING|INPUT|FORWARD)$/
raise ArgumentError, "PREROUTING, POSTROUTING and OUTPUT are the only inbuilt chains that can be used in table 'nat'"
end
if protocol =~/^(IP(v6)?)?$/
- raise ArgumentError, "table nat isn't valid in IPv6 (or the default IP which is IPv4 and IPv6). You must specify ':IPv4' as the name suffix"
+ raise ArgumentError, "table nat isn't valid in IPv6. You must specify ':IPv4' as the name suffix"
end
- when 'RAW'
+ when 'raw'
if chain =~ /^(POSTROUTING|BROUTING|INPUT|FORWARD)$/
raise ArgumentError,'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\''
end
- when 'BROUTE'
+ when 'broute'
if protocol != 'ethernet'
raise ArgumentError,'BROUTE is only valid with protocol \'ethernet\''
end
if chain =~ /^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$/
- raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'BROUTE\''
+ raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'broute\''
end
- end
- if chain == 'BROUTING' && ( protocol != 'ethernet' || table!='BROUTE')
- raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'BROUTE\' with protocol \'ethernet\' i.e. \'BROUTE:BROUTING:enternet\''
+ end
+ if chain == 'BROUTING' && ( protocol != 'ethernet' || table!='broute')
+ raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'BROUTE\' with protocol \'ethernet\' i.e. \'broute:BROUTING:enternet\''
end
end
end
newproperty(:policy) do
desc <<-EOS
This is the action to when the end of the chain is reached.
- It can only be set on inbuilt chains ( INPUT, FORWARD, OUTPUT,
+ It can only be set on inbuilt chains (INPUT, FORWARD, OUTPUT,
PREROUTING, POSTROUTING) and can be one of:
* accept - the packet is accepted
* return - the packet is returned to calling (jump) queue
or the default of inbuilt chains
EOS
- newvalues(:accept, :drop, :queue, :return, :empty)
+ newvalues(:accept, :drop, :queue, :return)
defaultto do
- # ethernet chain have an ACCEPT default while other haven't got an allowed value
+ # ethernet chain have an ACCEPT default while other haven't got an
+ # allowed value
if @resource[:name] =~ /:ethernet$/
:accept
else
- :empty
+ nil
end
end
end
protocol = $3
# Check that we're not removing an internal chain
- if chain =~ InternalChains && value(:ensure).to_s == 'absent'
+ if chain =~ InternalChains && value(:ensure) == :absent
self.fail "Cannot remove in-built chains"
end
- if value(:policy) == :empty && protocol == 'ethernet'
+ if value(:policy).nil? && protocol == 'ethernet'
self.fail "you must set a non-empty policy on all ethernet table chains"
end
# Check that we're not setting a policy on a user chain
- if chain !~ InternalChains && value(:policy).to_s != 'empty' && protocol != 'ethernet'
- self.fail "policy can only be set on in-built chains (with the exceptionn of ethernet chains) (table:#{table} chain:#{chain} protocol:#{protocol})"
+ if chain !~ InternalChains &&
+ !value(:policy).nil? &&
+ protocol != 'ethernet'
+
+ self.fail "policy can only be set on in-built chains (with the exception of ethernet chains) (table:#{table} chain:#{chain} protocol:#{protocol})"
end
-
+
# no DROP policy on nat table
if table == 'nat' &&
- value(:policy).to_s == 'DROP'
+ value(:policy) == :drop
+
self.fail 'The "nat" table is not intended for filtering, the use of DROP is therefore inhibited'
end
end
end
-
# Create a resource instance and make sure the provider is iptables
resource = Puppet::Type.type(:firewallchain).new({
- :name => ':test:',
+ :name => 'filter:test:IPv4',
})
resource.provider.class.to_s.should == "Puppet::Type::Firewallchain::ProviderIptables_chain"
end
-
- it "should raise a default provider error when there are no commands" do
- # Stub all commands lookups so they return nothing
- exists.any_instance.stubs(:which).returns false
-
- # Instantiate a resource instance and make sure it raises an exception
- lambda { resource = Puppet::Type.type(:firewallchain).new({
- :name => ':test:' }) }.should raise_error(Puppet::DevError,
- "Could not find a default provider for firewallchain")
- end
end
describe 'iptables chain provider' do
provider.stubs(:command).with(:iptables_save).returns "/sbin/iptables-save"
provider.stubs(:command).with(:ip6tables_save).returns "/sbin/ip6tables-save"
end
-
+
it 'should be able to get a list of existing rules' do
# Pretend to return nil from iptables
provider.expects(:execute).with(['/sbin/ebtables-save']).returns("")
'NAT:OUTPUT:ethernet',
'NAT:POSTROUTING:ethernet',
]
- provider.expects(:execute).with(['/sbin/ebtables-save']).returns('
+ provider.stubs(:execute).with(['/sbin/ebtables-save']).returns('
*broute
:BROUTING ACCEPT
:broute ACCEPT
'NAT:mangle:IPv4',
':$5()*&%\'"^$): :IPv4',
]
- provider.expects(:execute).with(['/sbin/iptables-save']).returns('
+ provider.stubs(:execute).with(['/sbin/iptables-save']).returns('
# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012
*raw
:PREROUTING ACCEPT [12:1780]
':OUTPUT:IPv6',
':test:IPv6',
]
- provider.expects(:execute).with(['/sbin/ip6tables-save']).returns('
+ provider.stubs(:execute).with(['/sbin/ip6tables-save']).returns('
# Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012
*raw
:PREROUTING ACCEPT [2173:489241]
ip6tables.each { |name| @all += [ name[0..-3], name[0..-5] ] }
end
- it 'should have all in parsed resources' do
+ it 'should have all in parsed resources' do
provider.instances.each do |resource|
@all.include?(resource.name)
end
firewallchain = Puppet::Type.type(:firewallchain)
describe firewallchain do
- before :each do
- @class = firewallchain
- @provider = stub 'provider'
- @provider.stubs(:name).returns(:iptables_chain)
- Puppet::Type::Firewallchain.stubs(:defaultprovider).returns @provider
- @resource = @class.new({:name => ':INPUT:', :policy => :accept })
- end
+ let(:klass) { firewallchain }
+ let(:provider) {
+ prov = stub 'provider'
+ prov.stubs(:name).returns(:iptables_chain)
+ prov
+ }
+ let(:resource) {
+ Puppet::Type::Firewallchain.stubs(:defaultprovider).returns provider
+ klass.new({:name => 'filter:INPUT:IPv4', :policy => :accept })
+ }
it 'should have :name be its namevar' do
- @class.key_attributes.should == [:name]
+ klass.key_attributes.should == [:name]
end
describe ':name' do
- {'' => ['INPUT','OUTPUT','FORWARD'],
- 'NAT' => ['PREROUTING', 'POSTROUTING', 'OUTPUT'],
- 'MANGLE' => [ 'PREROUTING', 'POSTROUTING', 'INPUT', 'FORWARD', 'OUTPUT' ],
- 'FILTER' => ['INPUT','OUTPUT','FORWARD'],
- 'RAW' => [ 'PREROUTING', 'OUTPUT'],
- 'BROUTE' => ['BROUTING']
+ {'nat' => ['PREROUTING', 'POSTROUTING', 'OUTPUT'],
+ 'mangle' => [ 'PREROUTING', 'POSTROUTING', 'INPUT', 'FORWARD', 'OUTPUT' ],
+ 'filter' => ['INPUT','OUTPUT','FORWARD'],
+ 'raw' => [ 'PREROUTING', 'OUTPUT'],
+ 'broute' => ['BROUTING']
}.each_pair do |table, allowedinternalchains|
- ['', 'IPv4', 'IPv6', 'IP', 'ethernet'].each do |protocol|
+ ['IPv4', 'IPv6', 'ethernet'].each do |protocol|
[ 'test', '$5()*&%\'"^$09):' ].each do |chainname|
name = "#{table}:#{chainname}:#{protocol}"
- if table == 'NAT' && ['IPv6','','IP'].include?(protocol)
+ if table == 'nat' && protocol == 'IPv6'
it "should fail #{name}" do
- lambda { @resource[:name] = name }.should raise_error(Puppet::Error)
+ expect { resource[:name] = name }.to raise_error(Puppet::Error)
end
- elsif protocol != 'ethernet' && table == 'BROUTE'
+ elsif protocol != 'ethernet' && table == 'broute'
it "should fail #{name}" do
- lambda { @resource[:name] = name }.should raise_error(Puppet::Error)
+ expect { resource[:name] = name }.to raise_error(Puppet::Error)
end
else
it "should accept name #{name}" do
- @resource[:name] = name
- @resource[:name].should == name
+ resource[:name] = name
+ resource[:name].should == name
end
end
end # chainname
name = table + ':' + internalchain + ':'
if internalchain == 'BROUTING'
name += 'ethernet'
- elsif table == 'NAT'
+ elsif table == 'nat'
+ name += 'IPv4'
+ else
name += 'IPv4'
end
if allowedinternalchains.include? internalchain
it "should allow #{name}" do
- @resource[:name] = name
- @resource[:name].should == name
+ resource[:name] = name
+ resource[:name].should == name
end
else
it "should fail #{name}" do
- lambda { @resource[:name] = name }.should raise_error(Puppet::Error)
+ expect { resource[:name] = name }.to raise_error(Puppet::Error)
end
end
end # internalchain
end # table, allowedinternalchainnames
it 'should fail with invalid table names' do
- lambda { @resource[:name] = 'wrongtablename:test:' }.should raise_error(Puppet::Error)
+ expect { resource[:name] = 'wrongtablename:test:' }.to raise_error(Puppet::Error)
end
it 'should fail with invalid protocols names' do
- lambda { @resource[:name] = ':test:IPv5' }.should raise_error(Puppet::Error)
+ expect { resource[:name] = ':test:IPv5' }.to raise_error(Puppet::Error)
end
end
describe ':policy' do
-
+
[:accept, :drop, :queue, :return].each do |policy|
it "should accept policy #{policy}" do
- @resource[:policy] = policy
- @resource[:policy].should == policy
+ resource[:policy] = policy
+ resource[:policy].should == policy
end
end
it 'should fail when value is not recognized' do
- lambda { @resource[:policy] = 'not valid' }.should raise_error(Puppet::Error)
+ expect { resource[:policy] = 'not valid' }.to raise_error(Puppet::Error)
end
[:accept, :drop, :queue, :return].each do |policy|
it "non-inbuilt chains should not accept policy #{policy}" do
- lambda { @class.new({:name => ':testchain:', :policy => policy }) }.should raise_error(Puppet::Error)
+ expect { klass.new({:name => 'filter:testchain:IPv4', :policy => policy }) }.to raise_error(Puppet::Error)
end
it "non-inbuilt chains can accept policies on protocol = ethernet (policy #{policy})" do
- @class.new({:name => ':testchain:ethernet', :policy => policy }).should be_instance_of(@provider)
+ klass.new({:name => 'filter:testchain:ethernet', :policy => policy }).should be_instance_of(@provider)
end
end