asbestos 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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