asbestos 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +10 -0
  5. data/Guardfile +9 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +461 -0
  8. data/Rakefile +1 -0
  9. data/asbestos.gemspec +26 -0
  10. data/bin/asbestos +112 -0
  11. data/examples/0_simple.rb +5 -0
  12. data/examples/10_kitchen_sink.rb +72 -0
  13. data/examples/1_two_hosts.rb +18 -0
  14. data/examples/2_accept_from_many.rb +19 -0
  15. data/examples/3_groups.rb +39 -0
  16. data/examples/4_host_templates.rb +29 -0
  17. data/examples/5_static_addresses.rb +7 -0
  18. data/examples/6_interface_addresses.rb +19 -0
  19. data/examples/7_services.rb +9 -0
  20. data/examples/8_rule_sets.rb +37 -0
  21. data/examples/9_literal_commands.rb +8 -0
  22. data/lib/asbestos.rb +108 -0
  23. data/lib/asbestos/address.rb +8 -0
  24. data/lib/asbestos/dsl.rb +40 -0
  25. data/lib/asbestos/firewalls/iptables.rb +127 -0
  26. data/lib/asbestos/host.rb +244 -0
  27. data/lib/asbestos/host_template.rb +15 -0
  28. data/lib/asbestos/metadata.rb +4 -0
  29. data/lib/asbestos/rule_set.rb +131 -0
  30. data/lib/asbestos/rule_sets/accept_from_self.rb +19 -0
  31. data/lib/asbestos/rule_sets/allow_related_established.rb +5 -0
  32. data/lib/asbestos/rule_sets/icmp_protection.rb +28 -0
  33. data/lib/asbestos/rule_sets/sanity_check.rb +41 -0
  34. data/lib/asbestos/service.rb +86 -0
  35. data/lib/asbestos/services/chef.rb +4 -0
  36. data/lib/asbestos/services/cube.rb +14 -0
  37. data/lib/asbestos/services/http.rb +8 -0
  38. data/lib/asbestos/services/memcached.rb +4 -0
  39. data/lib/asbestos/services/mongodb.rb +28 -0
  40. data/lib/asbestos/services/monit.rb +4 -0
  41. data/lib/asbestos/services/mysql.rb +4 -0
  42. data/lib/asbestos/services/nfs.rb +5 -0
  43. data/lib/asbestos/services/redis.rb +4 -0
  44. data/lib/asbestos/services/ssh.rb +4 -0
  45. data/spec/asbestos/address_spec.rb +25 -0
  46. data/spec/asbestos/firewalls/iptables_spec.rb +179 -0
  47. data/spec/asbestos/host_spec.rb +173 -0
  48. data/spec/asbestos/host_template_spec.rb +32 -0
  49. data/spec/asbestos/rule_set_spec.rb +55 -0
  50. data/spec/asbestos/service_spec.rb +60 -0
  51. data/spec/spec_helper.rb +20 -0
  52. metadata +159 -0
@@ -0,0 +1,8 @@
1
+
2
+ module Asbestos
3
+ class Address
4
+ include Asbestos::ClassCollection
5
+
6
+ class_collection :all
7
+ end
8
+ end
@@ -0,0 +1,40 @@
1
+
2
+ def host_template(name, &block)
3
+ name = name.to_sym
4
+ Asbestos::HostTemplate.new(name, block).tap do |host_template|
5
+
6
+ #
7
+ # Calling define_method wont let you define block parameters,
8
+ # but doing it this way will
9
+ #
10
+ Object.send(:define_method, name) do |host_name, &block|
11
+ host(host_name, &host_template.template).tap do |h|
12
+ h.instance_eval &block if block
13
+ h.template = name
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+
20
+ def host(name, &block)
21
+ Asbestos::Host.new(name.to_sym).tap do |h|
22
+ h.instance_eval &block if block_given?
23
+ end
24
+ end
25
+
26
+ def rule_set(name, &template)
27
+ Asbestos::RuleSet[name.to_sym] = template
28
+ end
29
+
30
+ def service(name, &template)
31
+ Asbestos::Service[name.to_sym] = template
32
+ end
33
+
34
+ def address(name, address)
35
+ Asbestos::Address[name] = [*address]
36
+ end
37
+
38
+
39
+ # For referencing lazy hosts in the dsl without prepending "Asbestos::"
40
+ Host = Asbestos::Host
@@ -0,0 +1,127 @@
1
+ module Asbestos::Firewall
2
+ module IPTables
3
+
4
+ def self.preamble(host)
5
+ [ "# Generated by Asbestos at #{Time.now.utc} for #{host.name}",
6
+ "# #{Asbestos::HOMEPAGE}",
7
+ "*filter"
8
+ ] +
9
+ host.chains.collect do |name, default_action|
10
+ chain name, default_action
11
+ end
12
+ end
13
+
14
+
15
+ def self.chain(name, default_action)
16
+ default_action = '-' if default_action == :none
17
+
18
+ ":#{name.upcase} #{default_action.upcase} [0:0]"
19
+ end
20
+
21
+
22
+ def self.open_port(interfaces, port, protocol, comment, remote_address = nil)
23
+ if interfaces
24
+ interfaces.collect do |interface|
25
+ accept :state => :new,
26
+ :protocol => protocol,
27
+ :port => port,
28
+ :comment => comment,
29
+ :interface => interface,
30
+ :remote_address => remote_address
31
+ end
32
+ else
33
+ accept :state => :new,
34
+ :protocol => protocol,
35
+ :port => port,
36
+ :comment => comment,
37
+ :remote_address => remote_address
38
+ end
39
+ end
40
+
41
+ #
42
+ # TODO: Use iptables' long options here for clarity?
43
+ #
44
+ def self.rule(args)
45
+ Array.new.tap do |r|
46
+ chain = \
47
+ if args[:chain]
48
+ args[:chain].to_s.upcase
49
+ else
50
+ 'INPUT'
51
+ end
52
+ r << "-A #{chain}"
53
+
54
+ r << "-j #{args[:action].upcase}" if args[:action]
55
+
56
+ if args[:interface]
57
+ direction = \
58
+ if args[:direction]
59
+ case args[:direction]
60
+ when :incoming
61
+ 'i'
62
+ when :outgoing
63
+ 'o'
64
+ else
65
+ raise "you must provide a :direction flag of either :incoming or :outgoing"
66
+ end
67
+ elsif %w{INPUT PREROUTING}.include? chain
68
+ 'i'
69
+ elsif %w{OUTPUT POSTROUTING}.include? chain
70
+ 'o'
71
+ else
72
+ raise "you must provide a :direction flag of either :incoming or :outgoing to use :interface with chain #{chain}"
73
+ end
74
+ r << "-#{direction} #{args[:interface]}"
75
+ end
76
+
77
+ r << "-p #{args[:protocol]}" if args[:protocol]
78
+ r << "-d #{args[:local_address]}" if args[:local_address]
79
+ r << "-s #{args[:remote_address]}" if args[:remote_address]
80
+
81
+ r << "-m state --state #{args[:state].upcase}" if args[:state]
82
+ #r << "-m #{args[:protocol]} --dport #{args[:port]}" if args[:protocol] && args[:port]
83
+ r << "--dport #{args[:port]}" if args[:port]
84
+ r << "-m limit --limit #{args[:limit]}" if args[:limit]
85
+
86
+ r << %{--log-prefix "#{args[:log_prefix]}"} if args[:log_prefix]
87
+ r << "--log-level #{args[:log_level]}" if args[:log_level]
88
+
89
+ r << "--icmp-type #{args[:icmp_type]}" if args[:icmp_type]
90
+
91
+ if args[:comment]
92
+ if args[:interface]
93
+ r << %{-m comment --comment "#{args[:comment]} on #{args[:interface]}"}
94
+ else
95
+ r << %{-m comment --comment "#{args[:comment]}"}
96
+ end
97
+ end
98
+ end.join(' ')
99
+ end
100
+
101
+ class << self
102
+ [:accept, :reject, :drop, :log].each do |action|
103
+ define_method(action) do |args|
104
+ self.rule(args.merge(:action => action))
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+
111
+ def self.postamble(host)
112
+ Array.new.tap do |rules|
113
+ rules << log(:limit => '5/min',
114
+ :log_level => 7,
115
+ :log_prefix => "iptables dropped: ",
116
+ :comment => "log dropped packets") if host.log_denials?
117
+
118
+ rules << drop(:chain => :input,
119
+ :comment => "drop packets that haven't been explicitly accepted") if host.chains[:input].upcase == :ACCEPT
120
+
121
+ rules << 'COMMIT'
122
+ rules << "# Asbestos completed at #{Time.now.utc}"
123
+ end
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,244 @@
1
+
2
+ class Asbestos::Host
3
+ include Asbestos::ClassCollection
4
+
5
+ class_collection :all
6
+ class_collection :groups
7
+
8
+ class << self
9
+ # returns a lazily evaluated block, to allow hosts to be
10
+ # defined in the DSL without a lot of hoopla
11
+ def [](name)
12
+ lambda { @all[name] }
13
+ end
14
+ end
15
+
16
+
17
+ attr_reader :name
18
+ attr_reader :groups
19
+ attr_reader :interfaces
20
+ attr_reader :addresses
21
+ attr_reader :rulesets
22
+ attr_reader :chains
23
+
24
+ # the HostTemplate that built this host
25
+ attr_accessor :template
26
+
27
+
28
+ def initialize(name)
29
+ @name = name
30
+ @groups = []
31
+ @rulesets = []
32
+
33
+ @chains = {}
34
+
35
+ @interfaces = {} # maps interface's tag to /dev name
36
+ @addresses = {} # maps interface's /dev name to an ip address
37
+
38
+
39
+ Asbestos.with_indifferent_access! @chains
40
+ Asbestos.with_indifferent_access! @interfaces
41
+ Asbestos.with_indifferent_access! @addresses
42
+
43
+ if Asbestos.hostname.to_sym == @name
44
+ Asbestos.interfaces.each do |if_name, info|
45
+ @addresses[if_name] = info[:inet_addr]
46
+ end
47
+ end
48
+
49
+ #
50
+ # Define the necessary chains
51
+ # # FIXME do we need :forward too?
52
+ #
53
+ [:input, :output].each do |name|
54
+ chain(name, :accept)
55
+ end
56
+
57
+ self.class.all[name] = self
58
+ end
59
+
60
+
61
+ def debug
62
+ [
63
+ "Hostname: #{@name}",
64
+ (@template ? " Template: #{@template}" : nil),
65
+ " Interfaces: #{@interfaces}",
66
+ " Addresses: #{@addresses}",
67
+ ].tap do |a|
68
+ a << " Groups: #{@groups.sort.join(', ')}" unless @groups.empty?
69
+ unless @rulesets.empty?
70
+ a << " RuleSets/Services:"
71
+ @rulesets.each { |s| a << " #{s.inspect}" }
72
+ end
73
+ end.join("\n")
74
+ end
75
+
76
+ def inspect
77
+ "#<Host name:#{name}>"
78
+ end
79
+
80
+ alias_method :to_s, :inspect
81
+
82
+
83
+ #
84
+ # DSL ------------------------------------------------------------------------------
85
+ #
86
+
87
+ #
88
+ # Places this host in a named group
89
+ #
90
+ # host 'dax' do
91
+ # group :developers
92
+ # end
93
+ #
94
+ def group(name = nil)
95
+ if name
96
+ @groups << name
97
+ self.class.groups[name] ||= []
98
+ self.class.groups[name] << self
99
+ else
100
+ @groups
101
+ end
102
+ end
103
+
104
+ #
105
+ # Defines an interface on this host with a given "tag". The interface's address can
106
+ # be defined explicitly, or at runtime via a block.
107
+ #
108
+ # host 'dax' do
109
+ # group :developers
110
+ #
111
+ # interface :external, :eth0 #=> address is "dax_external"
112
+ # interface :dmz, [:eth1, :eth2] #=> addresses are "dax_dmz_eth1" and "dax_dmz_eth2"
113
+ #
114
+ # interface :internal, :eth3 do |host|
115
+ # [host.group, host.name, 'foo'].join('_')
116
+ # end #=> address is "developers_dax_foo"
117
+ #
118
+ # interface :internal, :eth4, 'bar' #=> address is "bar"
119
+ # end
120
+ #
121
+ def interface(tag, if_names, address = nil, &block)
122
+ if_names = [*if_names]
123
+ raise "single address, #{address}, given for multiple interfaces, #{if_names}, on host #{name}" if if_names.length > 1 && address
124
+
125
+ @interfaces[tag] = if_names
126
+
127
+ # determine the address for each interface
128
+ if_names.each do |if_name|
129
+ new_address = \
130
+ if !address
131
+ if block_given?
132
+ yield(self, if_name)
133
+ else
134
+ if if_names.length > 1
135
+ "#{name}_#{tag}_#{if_name}"
136
+ else
137
+ "#{name}_#{tag}"
138
+ end
139
+ end
140
+ else
141
+ address
142
+ end
143
+ @addresses[if_name] ||= new_address
144
+ end
145
+
146
+ end
147
+
148
+ #
149
+ # Indicates that this host should log denied firewall packets.
150
+ #
151
+ # host 'dax' do
152
+ # log_denials
153
+ # end
154
+ def log_denials
155
+ @log_denials = true
156
+ end
157
+
158
+ def log_denials?
159
+ !!@log_denials
160
+ end
161
+
162
+
163
+ #
164
+ # Defines a firewall chain on this host, this may be an IPTables-only concept.
165
+ #
166
+ # The default_action here is also called the chain's "policy" in IPTables parlance
167
+ #
168
+ def chain(name, default_action = :none)
169
+ @chains[name.downcase.to_sym] = default_action
170
+ end
171
+
172
+ #
173
+ # Indicates that this host should have rules to allow the corresponding
174
+ # service to run on it. The arguments provided after the service name
175
+ # should be valid DSL calls supported by the service. Certain DSL calls
176
+ # come standard with all services, see the Service class for more info.
177
+ #
178
+ # host 'dax' do
179
+ # runs :nginx, :on => :external
180
+ # runs :ssh, :on => :internal, :port => 22022
181
+ # runs :riak, :on => :internal, :from => {:riak_cluster => :internal}
182
+ # end
183
+ #
184
+ def runs(service_name, args = {})
185
+ template = Asbestos::Service[service_name]
186
+ raise "Service not defined: #{service_name}" unless template
187
+
188
+ @rulesets <<
189
+ Asbestos::Service.new(service_name, self).tap do |s|
190
+ s.instance_eval &template
191
+ # override template defaults with provided options
192
+ args.each do |k, v|
193
+ s.send k, v
194
+ end
195
+ end
196
+ end
197
+
198
+
199
+ #
200
+ # Determine this host's firewall rules, according to the firewall type.
201
+ #
202
+ def rules
203
+ #
204
+ # This is called first in case any preable needs to be declared (chains, specifically)
205
+ #
206
+ _ruleset_rules = ruleset_rules
207
+
208
+ [
209
+ Asbestos.firewall.preamble(self),
210
+ _ruleset_rules,
211
+ Asbestos.firewall.postamble(self)
212
+ ].flatten
213
+ end
214
+
215
+ #
216
+ # Ask each ruleset/service to generate its rules.
217
+ #
218
+ def ruleset_rules
219
+ @rulesets.collect do |r|
220
+ ["# Begin [#{r.name}]",
221
+ r.firewall_rules,
222
+ "# End [#{r.name}]",
223
+ ""]
224
+ end
225
+ end
226
+
227
+ #
228
+ # Missing methods should be the name of RuleSets, if not, raise an error
229
+ #
230
+ # This is similar to the "runs" method above, but for RuleSets, rather than services.
231
+ #
232
+ def method_missing(rule_set_name, args = {})
233
+ template = Asbestos::RuleSet[rule_set_name]
234
+ raise %{Unknown host DSL call : "#{rule_set_name}" for host "#{name}"} unless template
235
+
236
+ @rulesets << \
237
+ Asbestos::RuleSet.new(rule_set_name, self, template).tap do |rs|
238
+ # override template defaults with provided options
239
+ args.each do |k, v|
240
+ rs.send k, v
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,15 @@
1
+ class Asbestos::HostTemplate
2
+ include Asbestos::ClassCollection
3
+
4
+ class_collection :all
5
+
6
+ attr_reader :template
7
+
8
+ def initialize(name, template)
9
+ @name = name
10
+ @template = template
11
+
12
+ self.class[name] = self
13
+ end
14
+
15
+ end
@@ -0,0 +1,4 @@
1
+ module Asbestos
2
+ VERSION = "0.0.1"
3
+ HOMEPAGE = "http://www.github.com/koudelka/asbestos"
4
+ end
@@ -0,0 +1,131 @@
1
+ require 'forwardable'
2
+
3
+ class Asbestos::RuleSet
4
+ extend ::Forwardable
5
+ include Asbestos::ClassCollection
6
+
7
+ class_collection :all
8
+
9
+ attr_reader :name
10
+ attr_reader :host
11
+ attr_reader :attributes
12
+ attr_reader :commands
13
+
14
+ def initialize(name, host, template)
15
+ @name = name
16
+ @host = host
17
+ @attributes = {}
18
+ @commands = []
19
+ @template = template
20
+ end
21
+
22
+ def inspect
23
+ "#{name}:#{@attributes.inspect}"
24
+ end
25
+
26
+
27
+ #
28
+ # Asks this RuleSet to generate its firewall rules
29
+ #
30
+ def firewall_rules
31
+ instance_eval &@template
32
+ @commands
33
+ end
34
+
35
+ #
36
+ # DSL ------------------------------------------------------------------------------
37
+ #
38
+
39
+ #
40
+ # These host functions are useful when building rules.
41
+ #
42
+ def_delegators :@host, :chain, :interfaces, :command, :addresses
43
+
44
+ #
45
+ # Records a literal firewall command for this host, ignoring firewall type (iptables, ipfw, etc)
46
+ #
47
+ def command(str)
48
+ @commands << str
49
+ end
50
+
51
+ #
52
+ # Requests a rule from this platform's firewall, with the given args.
53
+ #
54
+ [:rule, :accept, :reject, :drop, :log].each do |action|
55
+ define_method(action) do |args|
56
+ @commands << Asbestos.firewall.send(action, args)
57
+ end
58
+ end
59
+
60
+ #
61
+ # Given a list of "from" objects, resolve a list of hosts or addresses
62
+ #
63
+ def from_each(froms = @attributes[:from], &block)
64
+ case froms
65
+ when Array # a list of any of the other types
66
+ froms.each do |from|
67
+ from_each from, &block
68
+ end
69
+ when Hash # either a group or a specific host paired with an interface
70
+ froms.each do |host_or_group, their_interface_tag|
71
+ if [Symbol, String].include? host_or_group.class # it's a group name
72
+ Host.groups[host_or_group].uniq.each do |group_host|
73
+ next if group_host == @host
74
+ yield group_host, their_interface_tag
75
+ end
76
+ else # it's a Host or a lazly defined Host in a proc
77
+ host = host_or_group.is_a?(Proc) ? host_or_group.call : host_or_group
78
+ yield host, their_interface_tag
79
+ end
80
+ end
81
+ when String, Symbol # some kind of address(es)
82
+ if Asbestos::Address[froms]
83
+ Asbestos::Address[froms].each do |address|
84
+ yield address
85
+ end
86
+ else
87
+ yield froms
88
+ end
89
+ when nil # from everyone
90
+ yield nil
91
+ when Host, Proc
92
+ raise "#{@host.name}/#{name}: you specified a 'from' Host but no remote interface"
93
+ else
94
+ raise "#{@host.name}/#{name}: invalid 'from' object"
95
+ end
96
+ end
97
+
98
+ #
99
+ # Resolves a set of "from" objects into addresses
100
+ #
101
+ def from_each_address(froms = @attributes[:from])
102
+ from_each(froms) do |host_or_address, remote_interface_tag|
103
+ case host_or_address
104
+ when Host # specific host, specific remote interface
105
+ host_or_address.interfaces[remote_interface_tag].each do |remote_interface|
106
+ yield host_or_address.addresses[remote_interface]
107
+ end
108
+ else
109
+ yield host_or_address
110
+ end
111
+ end
112
+ end
113
+
114
+ #
115
+ # Responsible for storing and retrieving unspecified DSL calls as service attributes.
116
+ #
117
+ def method_missing(attribute, *args)
118
+ if args.empty?
119
+ @attributes[attribute]
120
+ else
121
+ #
122
+ # Certain DSL properties should be stored as arrays
123
+ #
124
+ if [:ports, :protocols, :groups].include? attribute
125
+ @attributes[attribute] = [*args]
126
+ else
127
+ @attributes[attribute] = args.first
128
+ end
129
+ end
130
+ end
131
+ end