reyes 0.0.6 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/bin/reyes +65 -30
- data/lib/reyes/aws_manager.rb +135 -0
- data/lib/reyes/fake_aws.rb +71 -0
- data/lib/reyes/group_manager.rb +55 -41
- data/lib/reyes/iptables.rb +17 -2
- data/lib/reyes/run_generation.rb +16 -1
- data/lib/reyes/run_manager.rb +52 -16
- data/lib/reyes/version.rb +1 -1
- data/lib/reyes.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NGE0MzE5OTBmNzQ2YWI0YmMxYmJlZGMxMDgxMGEwNDJiOTgzNTg0Mg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ZTY4MWFiMmNmMzg5MzEwNDBlYTc4MWUwZGVmYWZhMWYyOGMyZjI3YQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZTczYjRhNGIzZjZjMDViZjRlZDg4ODc2ZTNhYTQ5YjA4MjBiMDFmMjY5NWIw
|
10
|
+
NDE5M2ZmYzZkNDgzMDlmNjVlZTcwYjIyMGI2ODdiNzQ0MDg0ZWJmN2M3Y2Qy
|
11
|
+
MWJhYzljYTM5YzljNDYyMDI2MmQyYTZjODExZDUwZWM1YTFjNjg=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
OWRhNjJjMWI4YTFjN2UwYmNkNWQxZjllZTRkMDM5ZGM5MWM0YzA0MzQyMDVm
|
14
|
+
ZDMzZDE2ZTc0ZjE2NzEyOWM1NzhmZjYyNzlmNmYwZWZkMzYzZWMyMjE4OWM5
|
15
|
+
M2MzM2FiYTRiMDZmZjUwZmQyYjk4MjQyOGFkOThjZGVmYjYwZTk=
|
data/bin/reyes
CHANGED
@@ -2,47 +2,64 @@
|
|
2
2
|
require 'optparse'
|
3
3
|
require_relative '../lib/reyes'
|
4
4
|
|
5
|
-
def
|
6
|
-
|
5
|
+
def command_dump(output_file, options)
|
6
|
+
aws = Reyes::AwsManager.new(options[:config])
|
7
|
+
aws.dump_fake_data(output_file)
|
8
|
+
end
|
9
|
+
|
10
|
+
def command_install(json_file, instance_id, options)
|
7
11
|
region = options.fetch(:region)
|
8
12
|
|
9
13
|
if options[:splay]
|
10
14
|
Reyes::Utils.sleep_random(options[:splay])
|
11
15
|
end
|
12
16
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
r = Reyes::RunManager.new(g)
|
17
|
+
fake = Reyes::FakeAws.new(File.open(json_file, 'r'))
|
18
|
+
g = Reyes::GroupManager.new(fake, region, instance_id)
|
19
|
+
r = Reyes::RunManager.new(g)
|
17
20
|
|
18
|
-
|
21
|
+
data = r.generate_data!(options.fetch(:gen_options))
|
22
|
+
r.apply_data!(data, options.fetch(:apply_options))
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
if options[:prune]
|
23
|
-
r.prune_ipsets
|
24
|
-
# Pruning IPTables rules doesn't make any sense, they are atomically replaced
|
25
|
-
end
|
24
|
+
if options[:prune]
|
25
|
+
r.prune_ipsets
|
26
26
|
end
|
27
|
-
|
28
27
|
end
|
29
28
|
|
30
|
-
|
31
29
|
def parse_args
|
32
30
|
options = {
|
33
|
-
:region => 'us-west-1',
|
34
|
-
:
|
31
|
+
:region => 'us-west-1', # TODO: make required
|
32
|
+
:gen_options => {},
|
33
|
+
:apply_options => {
|
34
|
+
:log_accept => true, # TODO remove this default
|
35
|
+
},
|
35
36
|
}
|
36
37
|
|
37
38
|
optparse = OptionParser.new do |opts|
|
38
39
|
opts.banner = <<-EOM
|
39
|
-
usage: #{File.basename($0)} [options]
|
40
|
+
usage: #{File.basename($0)} [options] COMMAND ARGS...
|
40
41
|
|
41
42
|
Manipulate IPTables rules based on EC2 security groups.
|
42
43
|
|
44
|
+
Commands:
|
45
|
+
|
46
|
+
install: load AWS data from JSON_FILE and install rules for INSTANCE_ID
|
47
|
+
|
48
|
+
#{File.basename($0)} [options] install JSON_FILE INSTANCE_ID
|
49
|
+
|
50
|
+
dump: generate JSON from AWS and serialize data to JSON_FILE
|
51
|
+
|
52
|
+
#{File.basename($0)} [options] dump JSON_FILE
|
53
|
+
|
54
|
+
|
55
|
+
Defaults:
|
56
|
+
region: #{options.fetch(:region)}
|
57
|
+
|
58
|
+
|
43
59
|
For example:
|
44
60
|
|
45
|
-
#{File.basename($0)} --prune
|
61
|
+
#{File.basename($0)} --prune install data.json $(facter -p ec2_instance_id)
|
62
|
+
|
46
63
|
|
47
64
|
Options:
|
48
65
|
EOM
|
@@ -55,32 +72,39 @@ Options:
|
|
55
72
|
options[:region] = region
|
56
73
|
end
|
57
74
|
|
58
|
-
opts.on('--
|
59
|
-
options[:
|
75
|
+
opts.on('--env ENV', 'Set SRFC4 env for dump') do |env|
|
76
|
+
options[:tag_filters] = {'env' => env}
|
60
77
|
end
|
61
78
|
|
62
|
-
opts.on('--
|
63
|
-
options[:
|
64
|
-
options[:instance_id] = id
|
79
|
+
opts.on('--prune', 'Prune old generations after creating rules') do
|
80
|
+
options[:prune] = true
|
65
81
|
end
|
66
82
|
|
67
83
|
# TODO: known bug: --dry-run does not prevent run generation increment
|
68
84
|
opts.on('-n', '--dry-run', 'Print diff without making changes') do
|
69
|
-
options[:
|
85
|
+
options[:apply_options][:dry_run] = true
|
70
86
|
end
|
71
87
|
|
72
88
|
opts.on('-i', '--interactive', 'Interactively confirm changes') do
|
73
|
-
options[:
|
89
|
+
options[:apply_options][:interactive] = true
|
74
90
|
end
|
75
91
|
|
76
92
|
opts.on('--empty', 'Generate empty (default DROP) rules') do
|
77
|
-
options[:
|
93
|
+
options[:gen_options][:empty] = true
|
78
94
|
end
|
79
95
|
|
80
96
|
opts.on('--splay SECS', 'Delay up to SECS before execution') do |arg|
|
81
97
|
options[:splay] = Integer(arg)
|
82
98
|
end
|
83
99
|
|
100
|
+
opts.on('--[no-]log-accept', 'Log packets in ACCEPT') do |arg|
|
101
|
+
options[:apply_options][:log_accept] = arg
|
102
|
+
end
|
103
|
+
|
104
|
+
opts.on('--[no-]log-drop', 'Log packets in DROP') do |arg|
|
105
|
+
options[:apply_options][:log_drop] = arg
|
106
|
+
end
|
107
|
+
|
84
108
|
opts.on('-h', '--help', 'Display this help message') do
|
85
109
|
STDERR.puts opts
|
86
110
|
exit 0
|
@@ -89,9 +113,20 @@ Options:
|
|
89
113
|
|
90
114
|
optparse.parse!
|
91
115
|
|
92
|
-
|
93
|
-
|
94
|
-
|
116
|
+
command = ARGV.shift
|
117
|
+
case command
|
118
|
+
when 'dump'
|
119
|
+
unless ARGV.length == 1
|
120
|
+
STDERR.puts optparse
|
121
|
+
exit 2
|
122
|
+
end
|
123
|
+
command_dump(ARGV.fetch(0), options)
|
124
|
+
when 'install'
|
125
|
+
unless ARGV.length == 2
|
126
|
+
STDERR.puts optparse
|
127
|
+
exit 2
|
128
|
+
end
|
129
|
+
command_install(ARGV.fetch(0), ARGV.fetch(1), options)
|
95
130
|
else
|
96
131
|
STDERR.puts optparse
|
97
132
|
STDERR.puts "\nError: Must provide a command"
|
data/lib/reyes/aws_manager.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'aws-sdk'
|
2
|
+
require 'json'
|
3
|
+
require 'socket'
|
2
4
|
|
3
5
|
module Reyes
|
4
6
|
class AwsManager
|
@@ -105,8 +107,141 @@ module Reyes
|
|
105
107
|
end
|
106
108
|
end
|
107
109
|
|
110
|
+
def generate_fake_data
|
111
|
+
log.info('Generating AWS data for serialization')
|
112
|
+
start = Time.now.utc
|
113
|
+
data = {
|
114
|
+
'metadata' => {
|
115
|
+
'generated' => start,
|
116
|
+
'generated_stamp' => start.to_i,
|
117
|
+
'hostname' => Socket.gethostname,
|
118
|
+
'pid' => Process.pid,
|
119
|
+
},
|
120
|
+
'vpcs' => vpcs.map(&:id),
|
121
|
+
'security_groups_by_name' => {},
|
122
|
+
'regions' => {},
|
123
|
+
}
|
124
|
+
|
125
|
+
regions.each do |region|
|
126
|
+
data['regions'][region] = fake_data_for_region(region)
|
127
|
+
end
|
128
|
+
|
129
|
+
# populate security_groups_by_name
|
130
|
+
data['regions'].each_pair do |region, rdata|
|
131
|
+
rdata.fetch('security_groups').each_pair do |sg_id, sg_data|
|
132
|
+
data['security_groups_by_name'][sg_data.fetch('name')] ||= []
|
133
|
+
data['security_groups_by_name'][sg_data.fetch('name')] << {
|
134
|
+
'region' => region,
|
135
|
+
'group_id' => sg_id,
|
136
|
+
'vpc' => sg_data.fetch('vpc_id'),
|
137
|
+
}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
data['metadata']['generated_in'] = Time.now - start
|
142
|
+
|
143
|
+
log.info("Generated AWS data in #{(Time.now - start).round(1)}s")
|
144
|
+
|
145
|
+
data
|
146
|
+
end
|
147
|
+
|
148
|
+
def dump_fake_data(filename)
|
149
|
+
log.info("Dumping AWS data to #{filename.inspect}")
|
150
|
+
data = generate_fake_data
|
151
|
+
|
152
|
+
File.open(filename, File::WRONLY | File::CREAT | File::EXCL) do |fh|
|
153
|
+
fh.write(JSON.pretty_generate(data))
|
154
|
+
end
|
155
|
+
|
156
|
+
log.info('Wrote data to file')
|
157
|
+
end
|
158
|
+
|
108
159
|
private
|
109
160
|
|
161
|
+
def fake_data_for_region(region)
|
162
|
+
log.info("Generating data for #{region}")
|
163
|
+
|
164
|
+
data = {
|
165
|
+
'instances' => {},
|
166
|
+
'security_groups' => {},
|
167
|
+
}
|
168
|
+
|
169
|
+
AWS.memoize do
|
170
|
+
conn = ec2(region)
|
171
|
+
|
172
|
+
sg_members = {}
|
173
|
+
|
174
|
+
conn.instances.sort_by(&:instance_id).each do |i|
|
175
|
+
groups = i.security_groups.to_a.sort_by(&:name)
|
176
|
+
|
177
|
+
data['instances'][i.instance_id] = {
|
178
|
+
'tags' => i.tags.to_h.to_h,
|
179
|
+
'region' => region,
|
180
|
+
'availability_zone' => i.availability_zone,
|
181
|
+
'private_ip_address' => i.private_ip_address,
|
182
|
+
'public_ip_address' => i.public_ip_address,
|
183
|
+
'security_groups' => groups.map(&:group_id),
|
184
|
+
'security_group_names' => groups.map(&:name),
|
185
|
+
}
|
186
|
+
|
187
|
+
groups.map(&:id).each do |sg_id|
|
188
|
+
sg_members[sg_id] ||= []
|
189
|
+
sg_members[sg_id] << i.instance_id
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
conn.security_groups.sort_by(&:group_id).each do |g|
|
194
|
+
data['security_groups'][g.group_id] = {
|
195
|
+
'name' => g.name,
|
196
|
+
'description' => g.description,
|
197
|
+
'vpc_id' => g.vpc_id,
|
198
|
+
'region' => region,
|
199
|
+
'ipset_suffix' => ipset_suffix_for_group(region, g),
|
200
|
+
'ingress_ip_permissions' => g.ingress_ip_permissions.map {|perm|
|
201
|
+
|
202
|
+
if perm.port_range
|
203
|
+
port_start = perm.port_range.first
|
204
|
+
port_end = perm.port_range.last
|
205
|
+
else
|
206
|
+
port_start = nil
|
207
|
+
port_end = nil
|
208
|
+
if perm.protocol != :any
|
209
|
+
log.error("Invalid looking IpPermission object")
|
210
|
+
log.error(GroupTools.pretty_perm(perm))
|
211
|
+
raise Error.new("Missing port_range for perm")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
{
|
216
|
+
'protocol' => perm.protocol,
|
217
|
+
'port_start' => port_start,
|
218
|
+
'port_end' => port_end,
|
219
|
+
'ip_ranges' => perm.ip_ranges.sort,
|
220
|
+
'group_names' => perm.groups.map(&:name),
|
221
|
+
}
|
222
|
+
},
|
223
|
+
# avoid g.instances to prevent O(n) behavior
|
224
|
+
'instances' => sg_members.fetch(g.group_id, []).sort,
|
225
|
+
}
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
data
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
# @param group [AWS::EC2::SecurityGroup]
|
234
|
+
#
|
235
|
+
# @return [String] A string title, at most 31 characters long
|
236
|
+
#
|
237
|
+
def ipset_suffix_for_group(region, group)
|
238
|
+
[
|
239
|
+
Reyes::GroupManager::RegionShortNames.fetch(region),
|
240
|
+
group.group_id,
|
241
|
+
group.name,
|
242
|
+
].join(':')[0...31]
|
243
|
+
end
|
244
|
+
|
110
245
|
def aws_config
|
111
246
|
@config.aws_config
|
112
247
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Reyes
|
5
|
+
class FakeAws
|
6
|
+
include Chalk::Log
|
7
|
+
|
8
|
+
# @param json_source
|
9
|
+
def initialize(json_source)
|
10
|
+
log.info('Loading JSON data')
|
11
|
+
@data = JSON.load(json_source)
|
12
|
+
log.info("Loaded JSON with metadata: #{metadata.inspect}")
|
13
|
+
end
|
14
|
+
|
15
|
+
def region_data(region)
|
16
|
+
@data.fetch('regions').fetch(region)
|
17
|
+
end
|
18
|
+
|
19
|
+
def instance(region, instance_id)
|
20
|
+
region_data(region).fetch('instances').fetch(instance_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
def security_group(region, security_group_id)
|
24
|
+
region_data(region).fetch('security_groups').fetch(security_group_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
def security_group_ids_for_instance(region, instance_id)
|
28
|
+
instance(region, instance_id).fetch('security_groups')
|
29
|
+
end
|
30
|
+
|
31
|
+
def security_groups_for_instance(region, instance_id)
|
32
|
+
data = {}
|
33
|
+
|
34
|
+
security_group_ids_for_instance(region, instance_id).each do |sg_id|
|
35
|
+
data[sg_id] = security_group(region, sg_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
data
|
39
|
+
end
|
40
|
+
|
41
|
+
def addresses_for_security_group(region, security_group_id)
|
42
|
+
security_group_members(region, security_group_id).map do |instance_id|
|
43
|
+
instance(region, instance_id).fetch('private_ip_address')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def security_group_members(region, security_group_id)
|
48
|
+
security_group(region, security_group_id).fetch('instances')
|
49
|
+
end
|
50
|
+
|
51
|
+
def foreign_groups_by_name(group_name)
|
52
|
+
vpc_set = vpc_ids.to_set
|
53
|
+
groups = {}
|
54
|
+
@data.fetch('security_groups_by_name').fetch(group_name).each do |g|
|
55
|
+
next unless vpc_set.include?(g.fetch('vpc'))
|
56
|
+
groups[g.fetch('group_id')] = security_group(g.fetch('region'),
|
57
|
+
g.fetch('group_id'))
|
58
|
+
end
|
59
|
+
|
60
|
+
groups
|
61
|
+
end
|
62
|
+
|
63
|
+
def vpc_ids
|
64
|
+
@data.fetch('vpcs')
|
65
|
+
end
|
66
|
+
|
67
|
+
def metadata
|
68
|
+
@data.fetch('metadata')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/reyes/group_manager.rb
CHANGED
@@ -21,26 +21,31 @@ module Reyes
|
|
21
21
|
|
22
22
|
ReyesInputChain = 'reyes-ipsec-input'
|
23
23
|
|
24
|
-
attr_reader :
|
24
|
+
attr_reader :fake_aws, :instance_id
|
25
25
|
|
26
|
-
|
26
|
+
# @param fake_aws [Reyes::FakeAws]
|
27
|
+
# @param region [String]
|
28
|
+
# @param instance_id [String] A String instance ID of the target instance.
|
29
|
+
#
|
30
|
+
def initialize(fake_aws, region, instance_id, generation=nil)
|
27
31
|
log.info("Initializing #{self.class.name} for #{region} #{instance_id}")
|
28
|
-
log.info("This is reyes #{Reyes::VERSION}")
|
29
32
|
|
30
|
-
@
|
33
|
+
@fake_aws = fake_aws
|
31
34
|
@generation = generation || RunGeneration.new
|
35
|
+
@region = region
|
32
36
|
@instance_id = instance_id
|
33
|
-
@instance = @aws.ec2(region).instances[instance_id]
|
34
37
|
end
|
35
38
|
|
39
|
+
# @return [Hash]
|
36
40
|
def our_groups
|
37
|
-
@
|
38
|
-
@instance.security_groups
|
39
|
-
}
|
41
|
+
fake_aws.security_groups_for_instance(@region, @instance_id)
|
40
42
|
end
|
41
43
|
|
42
44
|
def generate_rules_empty
|
43
|
-
{
|
45
|
+
{
|
46
|
+
:groups => {},
|
47
|
+
:ipsets => {},
|
48
|
+
}
|
44
49
|
end
|
45
50
|
|
46
51
|
# Given our instance ID and security group rules, generate IPTables rules
|
@@ -53,52 +58,48 @@ module Reyes
|
|
53
58
|
# Also create a list of IPTables rules that will reference these ipsets.
|
54
59
|
#
|
55
60
|
# This method generates the data and returns it as a hash without making
|
56
|
-
# any changes to the system
|
57
|
-
# for ipset garbage collection).
|
61
|
+
# any changes to the system.
|
58
62
|
#
|
59
|
-
def generate_rules
|
60
|
-
run_generation_increment!
|
63
|
+
def generate_rules
|
61
64
|
log.info("Generating rules for generation #{run_generation}")
|
62
65
|
|
63
|
-
@aws.warm_sg_cache
|
64
|
-
|
65
66
|
data = generate_rules_empty
|
66
67
|
|
67
68
|
needed_groups = {}
|
68
69
|
|
69
|
-
our_groups.
|
70
|
-
group_key = "#{
|
70
|
+
our_groups.each_pair do |group_id, group|
|
71
|
+
group_key = "#{group_id}:#{group.fetch('name')}"
|
71
72
|
data[:groups][group_key] = []
|
72
73
|
|
73
74
|
# we only work with ingress permissions
|
74
|
-
group.ingress_ip_permissions.each do |perm|
|
75
|
+
group.fetch('ingress_ip_permissions').each do |perm|
|
75
76
|
perm_hash = {
|
76
|
-
protocol: perm.protocol,
|
77
|
-
port: perm.
|
77
|
+
protocol: perm.fetch('protocol'),
|
78
|
+
port: [perm.fetch('port_start'), perm.fetch('port_end')],
|
78
79
|
remote_addrs: [],
|
79
80
|
remote_sets: [],
|
80
81
|
}
|
81
82
|
|
82
|
-
case perm.protocol
|
83
|
-
when
|
83
|
+
case perm.fetch('protocol').to_s
|
84
|
+
when 'icmp'
|
84
85
|
log.info("Skipping ICMP rule")
|
85
86
|
next
|
86
|
-
when
|
87
|
+
when 'tcp', 'udp'
|
87
88
|
|
88
89
|
# append IP ranges
|
89
|
-
perm_hash[:remote_addrs] += perm.ip_ranges
|
90
|
+
perm_hash[:remote_addrs] += perm.fetch('ip_ranges')
|
90
91
|
|
91
92
|
# list the security groups we'll need to look up
|
92
|
-
perm.
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
perm_hash[:remote_sets] << ipset_name_for_group(fg)
|
93
|
+
perm.fetch('group_names').each do |g_name|
|
94
|
+
foreign_groups_by_name(g_name).each_pair do |fg_id, fg_data|
|
95
|
+
needed_groups[fg_id] = fg_data
|
96
|
+
perm_hash[:remote_sets] << ipset_name_for_group_hash(fg_data)
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
100
100
|
else
|
101
|
-
raise
|
101
|
+
raise NotImplementedError.new(
|
102
|
+
"Unexpected protocol #{perm['protocol'].inspect} in #{group_id}")
|
102
103
|
end
|
103
104
|
|
104
105
|
data[:groups][group_key] << perm_hash
|
@@ -106,9 +107,9 @@ module Reyes
|
|
106
107
|
end
|
107
108
|
|
108
109
|
data[:ipsets] = {}
|
109
|
-
needed_groups.
|
110
|
-
data[:ipsets][
|
111
|
-
addresses_for_group(
|
110
|
+
needed_groups.each_pair do |group_id, group_data|
|
111
|
+
data[:ipsets][ipset_name_for_group_hash(group_data)] = \
|
112
|
+
addresses_for_group(group_data.fetch('region'), group_id)
|
112
113
|
end
|
113
114
|
|
114
115
|
data
|
@@ -151,7 +152,11 @@ module Reyes
|
|
151
152
|
@generation.increment!
|
152
153
|
end
|
153
154
|
|
155
|
+
# TODO: delurk
|
156
|
+
# @param group [AWS::EC2::SecurityGroup]
|
157
|
+
#
|
154
158
|
# @return [String] A string title, at most 31 characters long
|
159
|
+
#
|
155
160
|
def ipset_name_for_group(group)
|
156
161
|
[
|
157
162
|
run_generation.to_s,
|
@@ -161,6 +166,17 @@ module Reyes
|
|
161
166
|
].join(':')[0...31]
|
162
167
|
end
|
163
168
|
|
169
|
+
# @param group_hash [Hash]
|
170
|
+
#
|
171
|
+
# @return [String] A string title, at most 31 characters long
|
172
|
+
#
|
173
|
+
def ipset_name_for_group_hash(group_hash)
|
174
|
+
[
|
175
|
+
run_generation.to_s,
|
176
|
+
group_hash.fetch('ipset_suffix'),
|
177
|
+
].join(':')[0...31]
|
178
|
+
end
|
179
|
+
|
164
180
|
# @return [Hash] the parsed names, or nil in case of error
|
165
181
|
def self.parse_ipset_name(name)
|
166
182
|
parts = name.split(":")
|
@@ -177,25 +193,23 @@ module Reyes
|
|
177
193
|
|
178
194
|
# Look up addresses for given group in remote VPCs.
|
179
195
|
#
|
180
|
-
# @param
|
196
|
+
# @param region [String]
|
197
|
+
# @param group_id [String]
|
181
198
|
#
|
182
199
|
# @return [Array<String>] A list of private instance IP addresses
|
183
200
|
#
|
184
|
-
def addresses_for_group(
|
185
|
-
|
186
|
-
groups.map { |g|
|
187
|
-
@aws.instances_in_security_group(g).map(&:private_ip_address)
|
188
|
-
}.flatten
|
201
|
+
def addresses_for_group(region, group_id)
|
202
|
+
fake_aws.addresses_for_security_group(region, group_id)
|
189
203
|
end
|
190
204
|
|
191
205
|
# Look up remote VPC security groups by name.
|
192
206
|
#
|
193
207
|
# @param name [String]
|
194
208
|
#
|
195
|
-
# @return [
|
209
|
+
# @return [Hash]
|
196
210
|
#
|
197
211
|
def foreign_groups_by_name(name)
|
198
|
-
|
212
|
+
fake_aws.foreign_groups_by_name(name)
|
199
213
|
end
|
200
214
|
|
201
215
|
# @param [Hash] data
|
data/lib/reyes/iptables.rb
CHANGED
@@ -6,7 +6,7 @@ module Reyes
|
|
6
6
|
# series of arguments appropriate for passing to IPTables.
|
7
7
|
#
|
8
8
|
# @param [Symbol] protocol
|
9
|
-
# @param [Range] dport_range
|
9
|
+
# @param [Range, Array(Integer, Integer)] dport_range
|
10
10
|
# @param [Array<String>] remote_addrs
|
11
11
|
# @param [Array<String>] remote_sets
|
12
12
|
# @param [String] input_chain
|
@@ -16,7 +16,10 @@ module Reyes
|
|
16
16
|
#
|
17
17
|
def self.generate_rules(protocol, dport_range, remote_addrs, remote_sets,
|
18
18
|
input_chain, accept_chain)
|
19
|
-
|
19
|
+
|
20
|
+
unless ['tcp', 'udp'].include?(protocol.to_s)
|
21
|
+
raise ArgumentError.new("Unsupported protocol #{protocol.inspect}")
|
22
|
+
end
|
20
23
|
|
21
24
|
unless block_given?
|
22
25
|
return enum_for(__method__, protocol, dport_range, remote_addrs,
|
@@ -25,6 +28,18 @@ module Reyes
|
|
25
28
|
|
26
29
|
cmd = ['-A', input_chain, '-p', protocol.to_s]
|
27
30
|
|
31
|
+
# dport_range may be a Range or two element Array
|
32
|
+
case dport_range
|
33
|
+
when Range
|
34
|
+
# OK
|
35
|
+
when Array
|
36
|
+
if dport_range.length != 2
|
37
|
+
raise ArgumentError.new("bad dport_range: #{dport_range.inspect}")
|
38
|
+
end
|
39
|
+
else
|
40
|
+
raise ArgumentError.new("invalid dport_range: #{dport_range.inspect}")
|
41
|
+
end
|
42
|
+
|
28
43
|
if dport_range.first == dport_range.last
|
29
44
|
# single port match
|
30
45
|
cmd += ['--dport', dport_range.first.to_s]
|
data/lib/reyes/run_generation.rb
CHANGED
@@ -31,6 +31,20 @@ module Reyes
|
|
31
31
|
@value
|
32
32
|
end
|
33
33
|
|
34
|
+
# Set the generation value and save it to disk.
|
35
|
+
#
|
36
|
+
# @param value [Integer]
|
37
|
+
#
|
38
|
+
def set!(value)
|
39
|
+
unless value.is_a?(Integer)
|
40
|
+
raise ArgumentError.new("Not an integer: #{value.inspect}")
|
41
|
+
end
|
42
|
+
@value = value
|
43
|
+
log.debug("Setting generation to #{@value}")
|
44
|
+
save(true)
|
45
|
+
@value
|
46
|
+
end
|
47
|
+
|
34
48
|
private
|
35
49
|
|
36
50
|
def reload
|
@@ -66,8 +80,9 @@ module Reyes
|
|
66
80
|
end
|
67
81
|
end
|
68
82
|
|
69
|
-
def save
|
83
|
+
def save(truncate=false)
|
70
84
|
@fh.rewind
|
85
|
+
@fh.truncate(0) if truncate
|
71
86
|
@fh.puts(@value.to_s)
|
72
87
|
@fh.flush
|
73
88
|
end
|
data/lib/reyes/run_manager.rb
CHANGED
@@ -8,34 +8,62 @@ module Reyes
|
|
8
8
|
|
9
9
|
# @param [Hash] options
|
10
10
|
#
|
11
|
-
# @option options :dry_run [Boolean] (false) Don't actually apply changes
|
12
11
|
# @option options :empty [Boolean] (false) Generate an empty (default DROP)
|
13
12
|
# rule sets without actually looking up security groups
|
14
|
-
# @option options :interactive [Boolean] (false) Whether to prompt for
|
15
|
-
# confirmation before applying rules
|
16
|
-
# @option options :log_accept [Boolean] (false) Whether to log packets on
|
17
|
-
# ACCEPT
|
18
13
|
#
|
19
|
-
def
|
14
|
+
def generate_data!(options={})
|
20
15
|
options = {
|
21
|
-
dry_run: false,
|
22
16
|
empty: false,
|
23
|
-
|
24
|
-
log_accept: false,
|
17
|
+
increment_generation: true,
|
25
18
|
}.merge(options)
|
26
19
|
|
27
|
-
|
28
|
-
|
29
|
-
log.info("Starting iptables rule generation run!")
|
20
|
+
log.info("RunManager.generate_data!")
|
30
21
|
|
31
22
|
if options.fetch(:empty)
|
32
23
|
log.warn("Generating empty (default DROP) rule set")
|
33
24
|
data = @group_manager.generate_rules_empty
|
34
25
|
else
|
35
|
-
|
26
|
+
if options.fetch(:increment_generation)
|
27
|
+
@group_manager.run_generation_increment!
|
28
|
+
end
|
29
|
+
data = @group_manager.generate_rules
|
36
30
|
end
|
37
31
|
|
38
|
-
|
32
|
+
log.info("Finished loading/generating data")
|
33
|
+
|
34
|
+
data
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [Hash] options
|
38
|
+
#
|
39
|
+
# @option options :dry_run [Boolean] (false) Don't actually apply changes
|
40
|
+
# @option options :interactive [Boolean] (false) Whether to prompt for
|
41
|
+
# confirmation before applying rules
|
42
|
+
# @option options :log_accept [Boolean] Whether to log packets on
|
43
|
+
# ACCEPT
|
44
|
+
# @option options :log_drop [Boolean] Whether to log packets on
|
45
|
+
# DROP
|
46
|
+
#
|
47
|
+
# @see {GroupManager#generate_iptables_script}
|
48
|
+
# @see {GroupManager#generate_ipsets}
|
49
|
+
#
|
50
|
+
def apply_data!(data, options={})
|
51
|
+
options = {
|
52
|
+
dry_run: false,
|
53
|
+
interactive: false,
|
54
|
+
}.merge(options)
|
55
|
+
|
56
|
+
log.info("RunManager.apply_data!")
|
57
|
+
|
58
|
+
gen_options = {}
|
59
|
+
if options.include?(:log_accept)
|
60
|
+
gen_options[:log_accept] = options.fetch(:log_accept)
|
61
|
+
end
|
62
|
+
if options.include?(:log_drop)
|
63
|
+
gen_options[:log_drop] = options.fetch(:log_drop)
|
64
|
+
end
|
65
|
+
|
66
|
+
new_rules = @group_manager.generate_iptables_script(data, gen_options)
|
39
67
|
new_ipsets = @group_manager.generate_ipsets(data)
|
40
68
|
|
41
69
|
show_iptables_diff(new_rules)
|
@@ -55,7 +83,7 @@ module Reyes
|
|
55
83
|
iptables_restore(new_rules)
|
56
84
|
|
57
85
|
# XXX(richo) Should we be pruning inside run! ?
|
58
|
-
log.info('Finished
|
86
|
+
log.info('Finished applying data')
|
59
87
|
end
|
60
88
|
|
61
89
|
def materialize_ipsets(new_ipsets)
|
@@ -64,6 +92,10 @@ module Reyes
|
|
64
92
|
end
|
65
93
|
end
|
66
94
|
|
95
|
+
# Call iptables-restore to atomically restore a set of iptables rules.
|
96
|
+
#
|
97
|
+
# @param new_rules [String]
|
98
|
+
#
|
67
99
|
def iptables_restore(new_rules)
|
68
100
|
log.info("Restoring #{new_rules.count("\n")} lines of iptables rules")
|
69
101
|
|
@@ -75,7 +107,7 @@ module Reyes
|
|
75
107
|
log.info('restored')
|
76
108
|
end
|
77
109
|
|
78
|
-
|
110
|
+
# Remove old IPSets from previous run generations.
|
79
111
|
def prune_ipsets
|
80
112
|
log.info('Pruning old IPSets')
|
81
113
|
old_ipsets = []
|
@@ -114,6 +146,10 @@ module Reyes
|
|
114
146
|
log.info('Done pruning old IPSets')
|
115
147
|
end
|
116
148
|
|
149
|
+
# Show a diff between current IPTables rules and new IPTables rules.
|
150
|
+
#
|
151
|
+
# @param new_rules [String]
|
152
|
+
#
|
117
153
|
def show_iptables_diff(new_rules)
|
118
154
|
diff = Diff.new
|
119
155
|
diff.old.puts(Subprocess.check_output(%w{iptables-save}))
|
data/lib/reyes/version.rb
CHANGED
data/lib/reyes.rb
CHANGED
@@ -14,10 +14,11 @@ require_relative './reyes/errors'
|
|
14
14
|
require_relative './reyes/aws_manager'
|
15
15
|
require_relative './reyes/config'
|
16
16
|
require_relative './reyes/diff'
|
17
|
+
require_relative './reyes/fake_aws'
|
17
18
|
require_relative './reyes/group_manager'
|
18
19
|
require_relative './reyes/group_tools'
|
19
20
|
require_relative './reyes/ipset'
|
20
21
|
require_relative './reyes/iptables'
|
21
22
|
require_relative './reyes/run_generation'
|
22
|
-
require_relative './reyes/utils'
|
23
23
|
require_relative './reyes/run_manager'
|
24
|
+
require_relative './reyes/utils'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reyes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Brody
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-02-
|
12
|
+
date: 2015-02-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -117,6 +117,7 @@ files:
|
|
117
117
|
- lib/reyes/config.rb
|
118
118
|
- lib/reyes/diff.rb
|
119
119
|
- lib/reyes/errors.rb
|
120
|
+
- lib/reyes/fake_aws.rb
|
120
121
|
- lib/reyes/group_manager.rb
|
121
122
|
- lib/reyes/group_tools.rb
|
122
123
|
- lib/reyes/ipset.rb
|