reyes 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.gitignore +2 -0
- data/.rubocop-disables.yml +11 -0
- data/.rubocop.yml +17 -0
- data/README.md +11 -1
- data/bin/reyes +95 -0
- data/config.yaml.example +13 -0
- data/lib/reyes.rb +15 -0
- data/lib/reyes/aws_manager.rb +116 -0
- data/lib/reyes/config.rb +21 -0
- data/lib/reyes/errors.rb +3 -0
- data/lib/reyes/group_manager.rb +414 -0
- data/lib/reyes/group_tools.rb +145 -0
- data/lib/reyes/ipset.rb +118 -0
- data/lib/reyes/iptables.rb +87 -0
- data/lib/reyes/run_generation.rb +75 -0
- data/lib/reyes/version.rb +1 -1
- data/reyes.gemspec +3 -0
- metadata +59 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
N2YwMTQ0NDA3OWVjMjQ3MTRkZWI4MTA3YTBlNWU3MTlhMDU5NzA2NA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MTM4NmQwZmVhY2EwOWJkZGQwODYzM2FkMDJiZGJkNDYyOTQ4NDFkZg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
NzNlN2IwMmMxNmUyNjkxMGYzYjljMTgyMTUwMDU5MjIwOTlmYTdiZmVhODBi
|
10
|
+
OWVhYzJiM2FiNjMzM2U5N2Q0NzIwYmEyNzAwYjQyOWNjMGQ2ZWYwZDE0MDFm
|
11
|
+
NzgyZTcwNzlkMzY3NDhjMTYxMWY1YTViMTU1MzVhZjc2NzBmZTc=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YjY5MTg0Y2RlYjBhMzlkZjA5Yjc1ZDkyZDgwMzdmMDVjYWNlNjEzNmE2MDVj
|
14
|
+
MDMwMzBiZDg4Nzc5NWM1ZjUzNDkzNzQ3YzQwMWY1NGMxMmU4MzJjMTcyYWY5
|
15
|
+
ODY5YzIwYmVkYWNmYWI4NjZiZWZjNDM1ZmZkZjFiOGExYjNhZmY=
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
inherit_from:
|
2
|
+
- .rubocop-disables.yml
|
3
|
+
|
4
|
+
Style/SpaceAroundEqualsInParameterDefault:
|
5
|
+
EnforcedStyle: no_space
|
6
|
+
|
7
|
+
Style/TrailingComma:
|
8
|
+
EnforcedStyleForMultiline: comma
|
9
|
+
|
10
|
+
Style/SignalException:
|
11
|
+
EnforcedStyle: only_raise
|
12
|
+
|
13
|
+
AllCops:
|
14
|
+
Include:
|
15
|
+
- '**/Rakefile'
|
16
|
+
Exclude:
|
17
|
+
- Gemfile
|
data/README.md
CHANGED
@@ -1,7 +1,17 @@
|
|
1
1
|
Reyes
|
2
2
|
=====
|
3
3
|
|
4
|
-
![Pt. Reyes Lighthouse](http://upload.wikimedia.org/wikipedia/commons/5/56/Point_Reyes_Lighthouse_%28April_2012%29.jpg)
|
4
|
+
![Pt. Reyes Lighthouse](http://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Point_Reyes_Lighthouse_%28April_2012%29.jpg/1266px-Point_Reyes_Lighthouse_%28April_2012%29.jpg)
|
5
5
|
|
6
6
|
Reyes populates IPTables firewall rules based on EC2 security group rules.
|
7
|
+
Named after the Pt. Reyes Lighthouse, which shines light through the fog,
|
8
|
+
preventing your ships from crashing on the rocks as they make their way to
|
9
|
+
port.
|
7
10
|
|
11
|
+
Use Case
|
12
|
+
--------
|
13
|
+
|
14
|
+
Reyes is designed to apply security group rules to IPsec VPN traffic that would
|
15
|
+
otherwise be injected past security group protection. This is useful for
|
16
|
+
enforcing firewalls on VPNs between EC2 instances and security groups in other
|
17
|
+
VPCs, even in other regions.
|
data/bin/reyes
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require_relative '../lib/reyes'
|
4
|
+
|
5
|
+
def command_dump(options)
|
6
|
+
instance_id = options.fetch(:instance_id)
|
7
|
+
region = options.fetch(:region)
|
8
|
+
g = Reyes::GroupManager.new(region, instance_id, options[:config])
|
9
|
+
|
10
|
+
AWS.memoize do
|
11
|
+
puts g.do_stuff.to_yaml
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def command_install(options)
|
16
|
+
instance_id = options.fetch(:instance_id)
|
17
|
+
region = options.fetch(:region)
|
18
|
+
AWS.memoize do
|
19
|
+
g = Reyes::GroupManager.new(region, instance_id, options[:config])
|
20
|
+
|
21
|
+
options[:run_options][:interactive] = true # TODO
|
22
|
+
options[:run_options][:log_accept] = true # TODO
|
23
|
+
|
24
|
+
g.run!(options.fetch(:run_options))
|
25
|
+
|
26
|
+
if options[:prune]
|
27
|
+
g.prune_ipsets
|
28
|
+
# g.prune_iptables_rules
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def parse_args
|
36
|
+
options = {
|
37
|
+
:region => 'us-west-1',
|
38
|
+
:run_options => {},
|
39
|
+
}
|
40
|
+
|
41
|
+
optparse = OptionParser.new do |opts|
|
42
|
+
opts.banner = <<-EOM
|
43
|
+
usage: #{File.basename($0)} [options]
|
44
|
+
|
45
|
+
Manipulate IPTables rules based on EC2 security groups.
|
46
|
+
|
47
|
+
For example:
|
48
|
+
|
49
|
+
#{File.basename($0)} --prune --install $(facter -p ec2_instance_id)
|
50
|
+
|
51
|
+
Options:
|
52
|
+
EOM
|
53
|
+
|
54
|
+
opts.on('-c', '--config CONFIG', 'YAML config file') do |config|
|
55
|
+
options[:config] = config
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on('--region REGION', 'Set EC2 region') do |region|
|
59
|
+
options[:region] = region
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on('--prune', 'Prune old generations after creating rules') do
|
63
|
+
options[:prune] = true
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on('--install INSTANCE_ID', 'Configure this instance as INSTANCE_ID') do |id|
|
67
|
+
options[:command] = :install
|
68
|
+
options[:instance_id] = id
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on('--empty', 'Generate empty (default DROP) rules') do
|
72
|
+
options[:run_options][:empty] = true
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on('-h', '--help', 'Display this help message') do
|
76
|
+
STDERR.puts opts
|
77
|
+
exit 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
optparse.parse!
|
82
|
+
|
83
|
+
case options[:command]
|
84
|
+
when :dump
|
85
|
+
command_dump(options)
|
86
|
+
when :install
|
87
|
+
command_install(options)
|
88
|
+
else
|
89
|
+
STDERR.puts optparse
|
90
|
+
STDERR.puts "\nError: Must provide a command"
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
parse_args
|
data/config.yaml.example
ADDED
data/lib/reyes.rb
CHANGED
@@ -1,4 +1,19 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'chalk-log'
|
5
|
+
require 'subprocess'
|
6
|
+
|
1
7
|
module Reyes
|
2
8
|
end
|
3
9
|
|
4
10
|
require_relative './reyes/version'
|
11
|
+
require_relative './reyes/errors'
|
12
|
+
|
13
|
+
require_relative './reyes/aws_manager'
|
14
|
+
require_relative './reyes/config'
|
15
|
+
require_relative './reyes/group_manager'
|
16
|
+
require_relative './reyes/group_tools'
|
17
|
+
require_relative './reyes/ipset'
|
18
|
+
require_relative './reyes/iptables'
|
19
|
+
require_relative './reyes/run_generation'
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Reyes
|
4
|
+
class AwsManager
|
5
|
+
class Error < Reyes::Error; end
|
6
|
+
|
7
|
+
include Chalk::Log
|
8
|
+
|
9
|
+
def self.with_retries(retries=5, delay=2)
|
10
|
+
raise ArgumentError.new('Block is required') unless block_given?
|
11
|
+
begin
|
12
|
+
yield
|
13
|
+
rescue AWS::EC2::Errors::RequestLimitExceeded => err
|
14
|
+
log.warn(err.inspect)
|
15
|
+
if retries > 0
|
16
|
+
retries -= 1
|
17
|
+
sleep delay
|
18
|
+
retry
|
19
|
+
else
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(config_path=nil)
|
26
|
+
@config ||= Reyes::Config.new(config_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ec2(region)
|
30
|
+
connections.fetch(:ec2).fetch(region)
|
31
|
+
end
|
32
|
+
|
33
|
+
def connections
|
34
|
+
@connections ||= connect!
|
35
|
+
end
|
36
|
+
|
37
|
+
def regions
|
38
|
+
aws_config.fetch('regions')
|
39
|
+
end
|
40
|
+
|
41
|
+
def vpcs
|
42
|
+
return @vpcs if @vpcs
|
43
|
+
|
44
|
+
@vpcs = aws_config.fetch('vpcs').map do |row|
|
45
|
+
unless row.length == 2
|
46
|
+
raise Error.new("Invalid VPC row: #{row.inspect}")
|
47
|
+
end
|
48
|
+
|
49
|
+
region, vpc_id = row
|
50
|
+
vpc = ec2(region).vpcs[vpc_id]
|
51
|
+
|
52
|
+
# warm cidr_block cache & ensure VPC exists
|
53
|
+
vpc.cidr_block
|
54
|
+
|
55
|
+
vpc
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def security_groups(region)
|
60
|
+
ec2(region).security_groups.to_a
|
61
|
+
end
|
62
|
+
|
63
|
+
# Look up foreign VPC security groups by name.
|
64
|
+
#
|
65
|
+
# Does not use native AWS API filtering to promote better cache behavior.
|
66
|
+
#
|
67
|
+
# @param name [String]
|
68
|
+
# @return [Array<AWS::EC2::SecurityGroup>]
|
69
|
+
#
|
70
|
+
def vpc_security_groups_by_name(name)
|
71
|
+
unless name.is_a?(String)
|
72
|
+
raise ArgumentError.new("#{name.inspect} must be a String")
|
73
|
+
end
|
74
|
+
|
75
|
+
vpcs.map { |vpc|
|
76
|
+
vpc.security_groups.find_all {|sg| sg.name == name }
|
77
|
+
}.flatten
|
78
|
+
end
|
79
|
+
|
80
|
+
def warm_sg_cache
|
81
|
+
connections.fetch(:ec2).each_pair do |region, ec2|
|
82
|
+
log.debug("Warming security group cache for #{region}")
|
83
|
+
ec2.security_groups.to_a
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def aws_config
|
90
|
+
@config.aws_config
|
91
|
+
end
|
92
|
+
|
93
|
+
def connect!
|
94
|
+
conns = {
|
95
|
+
ec2: {},
|
96
|
+
}
|
97
|
+
|
98
|
+
regions.each do |region|
|
99
|
+
conns[:ec2][region] = connect_class(AWS::EC2, region)
|
100
|
+
end
|
101
|
+
|
102
|
+
conns
|
103
|
+
end
|
104
|
+
|
105
|
+
def connect_class(klass, region)
|
106
|
+
opts = {
|
107
|
+
region: region,
|
108
|
+
access_key_id: @config.aws_credentials.fetch(:access_key_id),
|
109
|
+
secret_access_key: @config.aws_credentials.fetch(:secret_access_key),
|
110
|
+
logger: Chalk::Log::Logger.new("#{klass.name}<#{region}>"),
|
111
|
+
}
|
112
|
+
|
113
|
+
klass.new(opts)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/reyes/config.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Reyes
|
4
|
+
class Config
|
5
|
+
def initialize(path=nil)
|
6
|
+
@path = path || File.expand_path('../../../config.yaml', __FILE__)
|
7
|
+
end
|
8
|
+
|
9
|
+
def aws_config
|
10
|
+
config.fetch('aws')
|
11
|
+
end
|
12
|
+
|
13
|
+
def aws_credentials
|
14
|
+
aws_config.fetch('credentials')
|
15
|
+
end
|
16
|
+
|
17
|
+
def config
|
18
|
+
@config ||= YAML.load_file(@path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/reyes/errors.rb
ADDED
@@ -0,0 +1,414 @@
|
|
1
|
+
module Reyes
|
2
|
+
|
3
|
+
# TODO: use a more precise name
|
4
|
+
class GroupManager
|
5
|
+
include Chalk::Log
|
6
|
+
|
7
|
+
# Short names for AWS regions to save space in ipset names
|
8
|
+
RegionShortNames = {
|
9
|
+
'us-east-1' => 'VA',
|
10
|
+
'us-west-2' => 'OR',
|
11
|
+
'us-west-1' => 'CA',
|
12
|
+
'eu-west-1' => 'IE',
|
13
|
+
'eu-central-1' => 'DE',
|
14
|
+
'ap-southeast-1' => 'SG',
|
15
|
+
'ap-southeast-2' => 'AU',
|
16
|
+
'ap-northeast-1' => 'JP',
|
17
|
+
'sa-east-1' => 'BR',
|
18
|
+
'us-gov-west-1' => 'GV',
|
19
|
+
'cn-north-1' => 'CN',
|
20
|
+
}
|
21
|
+
|
22
|
+
ReyesInputChain = 'reyes-ipsec-input'
|
23
|
+
|
24
|
+
attr_reader :aws
|
25
|
+
|
26
|
+
def initialize(region, instance_id, config_file=nil)
|
27
|
+
log.info("Initializing #{self.class.name} for #{region} #{instance_id}")
|
28
|
+
|
29
|
+
@aws = Reyes::AwsManager.new(config_file)
|
30
|
+
@instance_id = instance_id
|
31
|
+
@instance = @aws.ec2(region).instances[instance_id]
|
32
|
+
end
|
33
|
+
|
34
|
+
def our_groups
|
35
|
+
@our_groups ||= Reyes::AwsManager.with_retries {
|
36
|
+
@instance.security_groups
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Hash] options
|
41
|
+
#
|
42
|
+
# @option options :empty [Boolean] (false) Generate an empty (default DROP)
|
43
|
+
# rule sets without actually looking up security groups
|
44
|
+
# @option options :interactive [Boolean] (false) Whether to prompt for
|
45
|
+
# confirmation before applying rules
|
46
|
+
# @option options :log_accept [Boolean] (false) Whether to log packets on
|
47
|
+
# ACCEPT
|
48
|
+
#
|
49
|
+
def run!(options={})
|
50
|
+
options = {
|
51
|
+
empty: false,
|
52
|
+
interactive: false,
|
53
|
+
log_accept: false,
|
54
|
+
}.merge(options)
|
55
|
+
|
56
|
+
log_accept = options.fetch(:log_accept)
|
57
|
+
|
58
|
+
log.info("Starting iptables rule generation run!")
|
59
|
+
|
60
|
+
if options.fetch(:empty)
|
61
|
+
log.warn("Generating empty (default DROP) rule set")
|
62
|
+
data = generate_rules_empty
|
63
|
+
else
|
64
|
+
data = generate_rules
|
65
|
+
end
|
66
|
+
|
67
|
+
new_rules = generate_iptables_script_file(data, log_accept: log_accept)
|
68
|
+
new_ipsets = generate_ipsets(data)
|
69
|
+
|
70
|
+
show_iptables_diff(new_rules)
|
71
|
+
show_ipsets_diff(new_ipsets)
|
72
|
+
|
73
|
+
if options.fetch(:interactive)
|
74
|
+
puts 'Press enter to continue...'
|
75
|
+
STDIN.gets
|
76
|
+
end
|
77
|
+
|
78
|
+
materialize_ipsets(new_ipsets)
|
79
|
+
iptables_restore(new_rules)
|
80
|
+
|
81
|
+
# XXX(richo) Should we be pruning inside run! ?
|
82
|
+
log.info('Finished firewall configuration run')
|
83
|
+
end
|
84
|
+
|
85
|
+
# TODO: actually do some kind of diff or logging here?
|
86
|
+
def show_iptables_diff(new_rules)
|
87
|
+
log.info("Old rules:")
|
88
|
+
Subprocess.check_call(%w{iptables-save})
|
89
|
+
|
90
|
+
log.info("New rules:")
|
91
|
+
puts new_rules
|
92
|
+
end
|
93
|
+
|
94
|
+
def iptables_restore(new_rules)
|
95
|
+
log.info("Restoring #{new_rules.count("\n")} lines of iptables rules")
|
96
|
+
|
97
|
+
log.info('+ iptables-restore')
|
98
|
+
Subprocess.check_call(['iptables-restore'],
|
99
|
+
stdin: Subprocess::PIPE) do |p|
|
100
|
+
p.communicate(new_rules)
|
101
|
+
end
|
102
|
+
log.info('restored')
|
103
|
+
end
|
104
|
+
|
105
|
+
def generate_rules_empty
|
106
|
+
{:groups => {}, :ipsets => {}}
|
107
|
+
end
|
108
|
+
|
109
|
+
# Given our instance ID and security group rules, generate IPTables rules
|
110
|
+
# needed to emulate security group behavior for foreign VPCs.
|
111
|
+
#
|
112
|
+
# Look up the set of instance IP addresses in any remote VPC security
|
113
|
+
# groups referenced by our rules, and add them to a list of ipsets to
|
114
|
+
# create.
|
115
|
+
#
|
116
|
+
# Also create a list of IPTables rules that will reference these ipsets.
|
117
|
+
#
|
118
|
+
# This method generates the data and returns it as a hash without making
|
119
|
+
# any changes to the system beyond incrementing the run generation (used
|
120
|
+
# for ipset garbage collection).
|
121
|
+
#
|
122
|
+
def generate_rules
|
123
|
+
run_generation_increment!
|
124
|
+
log.info("Generating rules for generation #{run_generation}")
|
125
|
+
|
126
|
+
data = generate_rules_empty
|
127
|
+
|
128
|
+
needed_groups = {}
|
129
|
+
|
130
|
+
our_groups.each do |group|
|
131
|
+
group_key = "#{group.group_id}:#{group.name}"
|
132
|
+
data[:groups][group_key] = []
|
133
|
+
|
134
|
+
# we only work with ingress permissions
|
135
|
+
group.ingress_ip_permissions.each do |perm|
|
136
|
+
perm_hash = {
|
137
|
+
protocol: perm.protocol,
|
138
|
+
port: perm.port_range,
|
139
|
+
remote_addrs: [],
|
140
|
+
remote_sets: [],
|
141
|
+
}
|
142
|
+
|
143
|
+
case perm.protocol
|
144
|
+
when :icmp
|
145
|
+
log.info("Skipping ICMP rule")
|
146
|
+
next
|
147
|
+
when :tcp, :udp
|
148
|
+
|
149
|
+
# append IP ranges
|
150
|
+
perm_hash[:remote_addrs] += perm.ip_ranges
|
151
|
+
|
152
|
+
# list the security groups we'll need to look up
|
153
|
+
perm.groups.each do |g|
|
154
|
+
foreign = foreign_groups_by_name(g.name)
|
155
|
+
foreign.each do |fg|
|
156
|
+
needed_groups[fg.group_id] = fg
|
157
|
+
perm_hash[:remote_sets] << ipset_name_for_group(fg)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
else
|
162
|
+
raise Error.new("Unexpected protocol #{perm.protocol.inspect}")
|
163
|
+
end
|
164
|
+
|
165
|
+
data[:groups][group_key] << perm_hash
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
data[:ipsets] = {}
|
170
|
+
needed_groups.each_value do |group|
|
171
|
+
data[:ipsets][ipset_name_for_group(group)] = \
|
172
|
+
addresses_for_group(group)
|
173
|
+
end
|
174
|
+
|
175
|
+
data
|
176
|
+
end
|
177
|
+
|
178
|
+
def generate_ipsets(data)
|
179
|
+
data.fetch(:ipsets).map do |name, hosts|
|
180
|
+
log.info "Creating IPSet for #{name.inspect}"
|
181
|
+
builder = IPSetBuilder.new(name)
|
182
|
+
hosts.each do |host|
|
183
|
+
log.info " - #{host.inspect}"
|
184
|
+
builder << host
|
185
|
+
end
|
186
|
+
|
187
|
+
builder
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def show_ipsets_diff(new_ipsets)
|
192
|
+
# TODO(richo)
|
193
|
+
end
|
194
|
+
|
195
|
+
def materialize_ipsets(new_ipsets)
|
196
|
+
new_ipsets.each do |ipset|
|
197
|
+
ipset.build
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# TODO: delurk
|
202
|
+
def create_iptables_rules(data)
|
203
|
+
data.fetch(:groups).each do |cluster, items|
|
204
|
+
log.info "Creating rules for #{cluster}"
|
205
|
+
items.each do |item|
|
206
|
+
rules = Reyes::IPTables.generate_rules_from_hash(item)
|
207
|
+
rules.each do |rule|
|
208
|
+
log.info(" #{rule.cmd.join(" ")}")
|
209
|
+
rule.materialize
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def prune_ipsets
|
216
|
+
log.info('Pruning old IPSets')
|
217
|
+
old_ipsets = []
|
218
|
+
current_ipsets = []
|
219
|
+
|
220
|
+
current_gen = run_generation
|
221
|
+
Reyes::IPSet.load_all.each do |set|
|
222
|
+
s = Reyes::GroupManager.parse_ipset_name(set.name)
|
223
|
+
unless s
|
224
|
+
log.warn("Skipping unparseable ipset name #{set.name.inspect}")
|
225
|
+
next
|
226
|
+
end
|
227
|
+
|
228
|
+
if s[:generation] < current_gen
|
229
|
+
old_ipsets << set
|
230
|
+
elsif s[:generation] == current_gen
|
231
|
+
current_ipsets << set
|
232
|
+
else
|
233
|
+
log.error("IPSet from a future generation detected: #{set.inspect}")
|
234
|
+
log.error("Cowardly refusing to proceed")
|
235
|
+
raise Reyes::Error.new("IPSet from future generation detected")
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
if current_ipsets.empty?
|
240
|
+
log.error("No IPSets from current generation.")
|
241
|
+
log.error("Cowardly refusing to proceed")
|
242
|
+
raise Reyes::Error.new("Pruning would remove all IPSets")
|
243
|
+
end
|
244
|
+
|
245
|
+
old_ipsets.each do |set|
|
246
|
+
log.info("Pruning IPSet: #{set.name}")
|
247
|
+
set.drop!
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# @return [Integer]
|
252
|
+
def run_generation
|
253
|
+
@generation ||= Reyes::RunGeneration.new
|
254
|
+
@generation.value
|
255
|
+
end
|
256
|
+
|
257
|
+
# Increment the run generation and persist it to disk
|
258
|
+
def run_generation_increment!
|
259
|
+
run_generation
|
260
|
+
@generation.increment!
|
261
|
+
end
|
262
|
+
|
263
|
+
# @return [String] A string title, at most 31 characters long
|
264
|
+
def ipset_name_for_group(group)
|
265
|
+
[
|
266
|
+
run_generation.to_s,
|
267
|
+
RegionShortNames.fetch(group.client.instance_variable_get(:@region)),
|
268
|
+
group.group_id,
|
269
|
+
group.name,
|
270
|
+
].join(':')[0...31]
|
271
|
+
end
|
272
|
+
|
273
|
+
# @return [Hash] the parsed names, or nil in case of error
|
274
|
+
def self.parse_ipset_name(name)
|
275
|
+
parts = name.split(":")
|
276
|
+
return nil unless parts.length == 4
|
277
|
+
|
278
|
+
generation, region, group_id, group_name = parts
|
279
|
+
return {
|
280
|
+
generation: Integer(generation),
|
281
|
+
region: region,
|
282
|
+
group_id: group_id,
|
283
|
+
group_name: group_name
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
# Look up addresses for given group in remote VPCs.
|
288
|
+
#
|
289
|
+
# @param group [AWS::EC2::SecurityGroup]
|
290
|
+
#
|
291
|
+
# @return [Array<String>] A list of private instance IP addresses
|
292
|
+
#
|
293
|
+
def addresses_for_group(group)
|
294
|
+
groups = foreign_groups_by_name(group.name)
|
295
|
+
groups.map { |g| g.instances.map(&:private_ip_address) }.flatten
|
296
|
+
end
|
297
|
+
|
298
|
+
# Look up remote VPC security groups by name.
|
299
|
+
#
|
300
|
+
# @param name [String]
|
301
|
+
#
|
302
|
+
# @return [Array<AWS::EC2::SecurityGroup>]
|
303
|
+
#
|
304
|
+
def foreign_groups_by_name(name)
|
305
|
+
aws.vpc_security_groups_by_name(name)
|
306
|
+
end
|
307
|
+
|
308
|
+
# @param [Hash] data
|
309
|
+
#
|
310
|
+
# @param [Hash] options
|
311
|
+
#
|
312
|
+
# @option options [Boolean] log_drop (true)
|
313
|
+
# @option options [Boolean] log_accept (false)
|
314
|
+
#
|
315
|
+
def generate_iptables_script_file(data, options={})
|
316
|
+
log.info("Generating script for iptables-restore")
|
317
|
+
|
318
|
+
options = {
|
319
|
+
log_drop: true,
|
320
|
+
log_accept: false,
|
321
|
+
}.merge(options)
|
322
|
+
|
323
|
+
log_drop = options.fetch(:log_drop)
|
324
|
+
log_accept = options.fetch(:log_accept)
|
325
|
+
|
326
|
+
lines = []
|
327
|
+
|
328
|
+
lines << "# Generated by Reyes v#{Reyes::VERSION} at #{Time.now.to_s}"
|
329
|
+
|
330
|
+
# we use the default filter table
|
331
|
+
lines << '*filter'
|
332
|
+
|
333
|
+
lines << ':INPUT ACCEPT'
|
334
|
+
lines << ':FORWARD DROP'
|
335
|
+
lines << ':OUTPUT ACCEPT'
|
336
|
+
lines << ''
|
337
|
+
|
338
|
+
lines << ":#{ReyesInputChain} -"
|
339
|
+
lines << ':reyes-log-drop -'
|
340
|
+
lines << ':reyes-log-accept -'
|
341
|
+
lines << ':reyes-accept -'
|
342
|
+
lines << ':reyes-drop -'
|
343
|
+
lines << ''
|
344
|
+
|
345
|
+
lines << IPTables.log_rule_string('reyes-log-drop', 'REYES BLOCK')
|
346
|
+
lines << IPTables.log_rule_string('reyes-log-accept', 'REYES ACCEPT')
|
347
|
+
|
348
|
+
lines << '-A reyes-drop -j reyes-log-drop' if log_drop
|
349
|
+
lines << '-A reyes-drop -j DROP'
|
350
|
+
lines << '-A reyes-accept -j reyes-log-accept' if log_accept
|
351
|
+
lines << '-A reyes-accept -j ACCEPT'
|
352
|
+
|
353
|
+
# filter all ipsec tunneled traffic through reyes
|
354
|
+
lines << "-A INPUT -m policy --pol ipsec --dir in -j #{ReyesInputChain}"
|
355
|
+
|
356
|
+
# allow normal ICMP traffic
|
357
|
+
IPTables.innocuous_icmp_rules(ReyesInputChain).each do |r|
|
358
|
+
lines << r.join(' ')
|
359
|
+
end
|
360
|
+
|
361
|
+
# allow established connections without logging
|
362
|
+
lines << "-A #{ReyesInputChain} -m conntrack --ctstate " \
|
363
|
+
"RELATED,ESTABLISHED -j ACCEPT"
|
364
|
+
|
365
|
+
# drop invalid connections
|
366
|
+
lines << "-A #{ReyesInputChain} -m conntrack --ctstate " \
|
367
|
+
"INVALID -j reyes-drop"
|
368
|
+
|
369
|
+
# add dynamic rules
|
370
|
+
lines << ''
|
371
|
+
lines << '# dynamic rules from security groups'
|
372
|
+
dynamic_rules_from_data(data).each do |rule|
|
373
|
+
lines << rule
|
374
|
+
end
|
375
|
+
|
376
|
+
# add reyes-drop to very end of reyes-ipsec-input chain
|
377
|
+
lines << ''
|
378
|
+
lines << '# default drop'
|
379
|
+
lines << "-A #{ReyesInputChain} -j reyes-drop"
|
380
|
+
|
381
|
+
# commit the filter table
|
382
|
+
lines << ''
|
383
|
+
lines << 'COMMIT'
|
384
|
+
|
385
|
+
text = lines.join("\n")
|
386
|
+
text << "\n"
|
387
|
+
|
388
|
+
text
|
389
|
+
end
|
390
|
+
|
391
|
+
private
|
392
|
+
|
393
|
+
def dynamic_rules_from_data(data)
|
394
|
+
log.info("Generating dynamic iptables rules")
|
395
|
+
|
396
|
+
rule_list = []
|
397
|
+
|
398
|
+
data.fetch(:groups).each do |cluster, items|
|
399
|
+
log.info "Generating rules for #{cluster}"
|
400
|
+
items.each do |item|
|
401
|
+
rules = Reyes::IPTables.generate_rules_from_hash(item,
|
402
|
+
ReyesInputChain,
|
403
|
+
'reyes-accept')
|
404
|
+
rules.each do |rule|
|
405
|
+
log.debug(' ' + rule.inspect)
|
406
|
+
rule_list << rule.join(' ')
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
rule_list
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Reyes
|
2
|
+
# A few methods for manipulating EC2 SecurityGroup and IpPermission objects
|
3
|
+
# that really should be in the aws-sdk. These are taken directly from
|
4
|
+
# space-commander.
|
5
|
+
module GroupTools
|
6
|
+
|
7
|
+
# Print a pretty representation of an AWS::EC2::SecurityGroup::IpPermission
|
8
|
+
# object. May issue API calls to resolve security group names.
|
9
|
+
#
|
10
|
+
# @param [AWS::EC2::SecurityGroup::IpPermission] perm Object to inspect
|
11
|
+
#
|
12
|
+
# @param [Hash] options
|
13
|
+
#
|
14
|
+
# @option options [Integer] :indent Indentation level (default 0)
|
15
|
+
#
|
16
|
+
# @option options [Boolean] :include_group Whether to print the security
|
17
|
+
# group to which the IpPermission belongs. (default true)
|
18
|
+
#
|
19
|
+
# @option options [SpaceCommander::SecurityGroup::Cache] :cache A cache to
|
20
|
+
# use for resolving security group names as needed.
|
21
|
+
#
|
22
|
+
def self.pretty_perm(perm, options={})
|
23
|
+
options = {:indent => 0, :include_group => true}.merge(options)
|
24
|
+
indent = options.fetch(:indent)
|
25
|
+
include_group = options.fetch(:include_group)
|
26
|
+
|
27
|
+
# use cache to find groups if one was provided
|
28
|
+
if options[:cache]
|
29
|
+
groups = options[:cache].unsafe_get_many(perm.groups)
|
30
|
+
else
|
31
|
+
groups = perm.groups
|
32
|
+
end
|
33
|
+
|
34
|
+
lines = []
|
35
|
+
group = perm.security_group
|
36
|
+
lines << "security_group: #{group.name} (#{group.id})" if include_group
|
37
|
+
lines.concat([
|
38
|
+
"type: #{(perm.egress? ? :egress : :ingress)}",
|
39
|
+
"proto: #{perm.protocol.inspect}",
|
40
|
+
"ports: #{perm.port_range.inspect}",
|
41
|
+
"cidr: #{perm.ip_ranges.inspect}",
|
42
|
+
"groups: [#{groups.map{|g| "<#{g.name} #{g.id}>"}.join(", ")}]"
|
43
|
+
])
|
44
|
+
return lines.map{|line| ' '*indent + line}.join("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
# Inspect an AWS::EC2::SecurityGroup::IpPermission object. May issue API
|
48
|
+
# calls to resolve security group names.
|
49
|
+
#
|
50
|
+
# @param [AWS::EC2::SecurityGroup::IpPermission] perm Object to inspect
|
51
|
+
#
|
52
|
+
# @param [Hash] options
|
53
|
+
#
|
54
|
+
# @option options [SpaceCommander::SecurityGroup::Cache] :cache A cache to
|
55
|
+
# use for resolving security group names as needed.
|
56
|
+
#
|
57
|
+
def self.inspect_perm(perm, options={})
|
58
|
+
unless perm.is_a?(AWS::EC2::SecurityGroup::IpPermission)
|
59
|
+
raise ArgumentError.new("Not an IpPermission: #{perm.inspect}")
|
60
|
+
end
|
61
|
+
|
62
|
+
s = "#<IpPermission #{perm.egress? ? :egress : :ingress}"
|
63
|
+
|
64
|
+
s << " @security_group=<#{perm.security_group.id} #{perm.security_group.name}>"
|
65
|
+
s << " @protocol=#{perm.protocol.inspect}"
|
66
|
+
s << " @port_range=#{perm.port_range.inspect}"
|
67
|
+
|
68
|
+
unless perm.ip_ranges.empty?
|
69
|
+
s << " @ip_ranges=#{perm.ip_ranges.inspect}"
|
70
|
+
end
|
71
|
+
|
72
|
+
unless perm.groups.empty?
|
73
|
+
# use cache to find groups if one was provided
|
74
|
+
if options[:cache]
|
75
|
+
groups = options[:cache].unsafe_get_many(perm.groups)
|
76
|
+
else
|
77
|
+
groups = perm.groups
|
78
|
+
end
|
79
|
+
|
80
|
+
s << " @groups=[#{groups.map{|g| "<#{g.name} #{g.id}>"}.join(", ")}]"
|
81
|
+
end
|
82
|
+
s
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.perm_to_hash(perm)
|
86
|
+
h = {}
|
87
|
+
|
88
|
+
h[:protocol] = perm.protocol.to_s
|
89
|
+
h[:label] = ''
|
90
|
+
|
91
|
+
port = perm.port_range
|
92
|
+
if port == all_ports(perm.protocol)
|
93
|
+
h[:port] = 'all'
|
94
|
+
else
|
95
|
+
if port.first == port.last
|
96
|
+
h[:port] = port.first
|
97
|
+
else
|
98
|
+
h[:port] = "#{port.first}-#{port.last}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# TODO: convert /32s back into hostname from config.yaml if possible
|
103
|
+
if perm.ip_ranges.length > 1
|
104
|
+
h[:cidr] = perm.ip_ranges
|
105
|
+
elsif perm.ip_ranges.length == 1
|
106
|
+
h[:cidr] = perm.ip_ranges.first
|
107
|
+
if h[:cidr] == '0.0.0.0/0'
|
108
|
+
h[:cidr] = 'all'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
if perm.groups.length > 0
|
113
|
+
h[:groups] = perm.groups.map(&:name)
|
114
|
+
end
|
115
|
+
|
116
|
+
h
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.group_to_hash(group)
|
120
|
+
h = {
|
121
|
+
:name => group.name,
|
122
|
+
:description => group.description,
|
123
|
+
:inbound => group.ingress_ip_permissions.map {|p| perm_to_hash(p)},
|
124
|
+
:outbound => group.egress_ip_permissions.map {|p| perm_to_hash(p)},
|
125
|
+
}
|
126
|
+
|
127
|
+
h
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.all_ports(protocol)
|
131
|
+
case protocol
|
132
|
+
when :icmp
|
133
|
+
-1..-1
|
134
|
+
when :tcp, :udp
|
135
|
+
0..65535
|
136
|
+
when :any, :'-1', -1
|
137
|
+
nil
|
138
|
+
else
|
139
|
+
msg = "Don't know how to allow 'all' ports for " + protocol.inspect
|
140
|
+
raise NotImplementedError.new(msg)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
data/lib/reyes/ipset.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
module Reyes
|
2
|
+
class IPSetBuilder
|
3
|
+
TYPE = "hash:ip"
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
# Constructor for ipsets, maintaining the invariant that once constructed,
|
7
|
+
# ipsets will not be altered.
|
8
|
+
# @param [String] name of the IPSet to create
|
9
|
+
def initialize(name)
|
10
|
+
@name = name
|
11
|
+
@members = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add a new member to this set. `member` should be something an ipset can
|
15
|
+
# understand, eg an ip address or cidr range
|
16
|
+
# @param [String] entity to add to the set
|
17
|
+
def <<(entity)
|
18
|
+
@members << entity
|
19
|
+
end
|
20
|
+
|
21
|
+
# Builds the ipset, retuning an IPSet
|
22
|
+
# @returns [IPSet]
|
23
|
+
def build
|
24
|
+
create
|
25
|
+
populate
|
26
|
+
IPSet.load(@name)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def create
|
32
|
+
begin
|
33
|
+
Subprocess.check_call(["ipset", "create", @name, TYPE])
|
34
|
+
rescue Subprocess::NonZeroExit
|
35
|
+
raise Error.new("Couldn't create ipset #@name")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def populate
|
40
|
+
@members.each do |e|
|
41
|
+
begin
|
42
|
+
Subprocess.check_call(["ipset", "add", @name, e])
|
43
|
+
rescue Subprocess::NonZeroExit
|
44
|
+
raise Error.new("Couldn't add #{e} to ipset #@name")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Error < StandardError
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class IPSet < Struct.new(:name, :type, :header, :size, :references, :members)
|
54
|
+
# An immutable view into an existing IPSet
|
55
|
+
|
56
|
+
# @param [Name] name of the set to load
|
57
|
+
# @returns IPSet
|
58
|
+
def self.load(name)
|
59
|
+
new(*load_by_name(name))
|
60
|
+
end
|
61
|
+
|
62
|
+
# Loads all IPSets from the local system
|
63
|
+
# @returns [Array<IPSet>]
|
64
|
+
def self.load_all
|
65
|
+
cmd = ['ipset', 'list']
|
66
|
+
output = Subprocess.check_output(cmd)
|
67
|
+
return output.split("\n\n").map { |x| new(*parse(x)) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Drops the currect IPSet, destroying it on the underlying system
|
71
|
+
def drop!
|
72
|
+
begin
|
73
|
+
Subprocess.check_call(["ipset", "destroy", name])
|
74
|
+
rescue Subprocess::NonZeroExit
|
75
|
+
raise Error.new("Couldn't destroy #{name}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def self.load_by_name(name)
|
82
|
+
cmd = ['ipset', 'list', name]
|
83
|
+
output = Subprocess.check_output(cmd)
|
84
|
+
entries = output.split("\n\n")
|
85
|
+
unless entries.length == 1
|
86
|
+
raise IPSetError.new("`ipset list #{name}` returned too many entries")
|
87
|
+
end
|
88
|
+
|
89
|
+
return parse(entries.first)
|
90
|
+
end
|
91
|
+
|
92
|
+
KEYS = ["Name", "Header", "Size in memory", "References", "Members", "Type"]
|
93
|
+
def self.parse(entry)
|
94
|
+
out = {:members => []}
|
95
|
+
entry.split(?\n).each do |line|
|
96
|
+
k, v = line.split(":", 2)
|
97
|
+
if KEYS.include?(k)
|
98
|
+
out[k] = v.strip
|
99
|
+
else
|
100
|
+
raise Error.new("Invalid data: #{entry.inspect}") unless out.include?("Members")
|
101
|
+
out[:members] << line.strip
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
return [
|
106
|
+
out["Name"],
|
107
|
+
out["Type"],
|
108
|
+
out["Header"],
|
109
|
+
Integer(out["Size in memory"]),
|
110
|
+
Integer(out["References"]),
|
111
|
+
out[:members],
|
112
|
+
]
|
113
|
+
end
|
114
|
+
|
115
|
+
class Error < StandardError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Reyes
|
2
|
+
# A collection of functionality related to generating IPTables rule sets.
|
3
|
+
module IPTables
|
4
|
+
|
5
|
+
# Generate IPTables rule arguments based on a specification and yield a
|
6
|
+
# series of arguments appropriate for passing to IPTables.
|
7
|
+
#
|
8
|
+
# @param [Symbol] protocol
|
9
|
+
# @param [Range] dport_range
|
10
|
+
# @param [Array<String>] remote_addrs
|
11
|
+
# @param [Array<String>] remote_sets
|
12
|
+
# @param [String] input_chain
|
13
|
+
# @param [String] accept_chain
|
14
|
+
#
|
15
|
+
# @yield [Array<String>] IPTables rules argument array
|
16
|
+
#
|
17
|
+
def self.generate_rules(protocol, dport_range, remote_addrs, remote_sets,
|
18
|
+
input_chain, accept_chain)
|
19
|
+
raise 'Unsupported protocol' unless [:tcp, :udp].include?(protocol)
|
20
|
+
|
21
|
+
unless block_given?
|
22
|
+
return enum_for(__method__, protocol, dport_range, remote_addrs,
|
23
|
+
remote_sets, input_chain, accept_chain)
|
24
|
+
end
|
25
|
+
|
26
|
+
cmd = ['-A', input_chain, '-p', protocol.to_s]
|
27
|
+
|
28
|
+
if dport_range.first == dport_range.last
|
29
|
+
# single port match
|
30
|
+
cmd += ['--dport', dport_range.first.to_s]
|
31
|
+
else
|
32
|
+
# port range match
|
33
|
+
cmd += ['-m', 'multiport', '--dports',
|
34
|
+
"#{dport_range.first}:#{dport_range.last}"]
|
35
|
+
end
|
36
|
+
|
37
|
+
jump_args = ['-j', accept_chain]
|
38
|
+
|
39
|
+
remote_addrs.each do |addr|
|
40
|
+
yield cmd + ['-s', addr] + jump_args
|
41
|
+
end
|
42
|
+
|
43
|
+
remote_sets.each do |set|
|
44
|
+
yield cmd + ['-m', 'set', '--match-set', set, 'src,dst'] + jump_args
|
45
|
+
end
|
46
|
+
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# Generate IPTables rules from a hash. This is a thin wrapper around
|
51
|
+
# {.generate_rules}.
|
52
|
+
#
|
53
|
+
# @param [Hash] hash
|
54
|
+
# @param [String] input_chain
|
55
|
+
# @param [String] accept_chain
|
56
|
+
#
|
57
|
+
def self.generate_rules_from_hash(hash, input_chain, accept_chain)
|
58
|
+
generate_rules(hash.fetch(:protocol), hash.fetch(:port),
|
59
|
+
hash.fetch(:remote_addrs), hash.fetch(:remote_sets),
|
60
|
+
input_chain, accept_chain)
|
61
|
+
end
|
62
|
+
|
63
|
+
class Rule
|
64
|
+
IPTABLES = "iptables"
|
65
|
+
attr_reader :cmd
|
66
|
+
def initialize(cmd)
|
67
|
+
@cmd = cmd
|
68
|
+
end
|
69
|
+
|
70
|
+
def materialize
|
71
|
+
cmd = [IPTABLES] + @cmd
|
72
|
+
Subprocess.check_call(cmd)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.log_rule_string(chain, message, limit='3/min', limit_burst=10)
|
77
|
+
"-A #{chain} -m limit --limit #{limit} --limit-burst #{limit_burst}" \
|
78
|
+
" -j LOG --log-prefix \"[#{message}] \""
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.innocuous_icmp_rules(chain)
|
82
|
+
[3, 4, 11, 12, 8].map do |type|
|
83
|
+
%W{-A #{chain} -p icmp -m icmp --icmp-type #{type} -j ACCEPT}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Reyes
|
2
|
+
# An abstraction over tmpfile based generation number storage.
|
3
|
+
class RunGeneration
|
4
|
+
class LoadError < Reyes::Error; end
|
5
|
+
|
6
|
+
include Chalk::Log
|
7
|
+
|
8
|
+
DefaultGenerationPath = "/tmp/reyes.u#{Process.euid}.generation"
|
9
|
+
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
# @param path [String] Path to generation file
|
13
|
+
#
|
14
|
+
# If the file at `path` does not exist, it will be created. Attempting to
|
15
|
+
# open a symbolic link will raise Errno::ELOOP to avoid security
|
16
|
+
# vulnerabilities.
|
17
|
+
#
|
18
|
+
def initialize(path=DefaultGenerationPath)
|
19
|
+
@path = path
|
20
|
+
reload
|
21
|
+
end
|
22
|
+
|
23
|
+
# Increment the generation value and save it to disk.
|
24
|
+
#
|
25
|
+
# @return [Integer] new generation value
|
26
|
+
#
|
27
|
+
def increment!
|
28
|
+
@value += 1
|
29
|
+
log.debug("Incrementing generation to #{@value}")
|
30
|
+
save
|
31
|
+
@value
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def reload
|
37
|
+
log.debug("Opening generation file at #{@path.inspect}")
|
38
|
+
@fh = open(@path)
|
39
|
+
if @fh.size == 0
|
40
|
+
@value = 0
|
41
|
+
else
|
42
|
+
@value = load_value
|
43
|
+
end
|
44
|
+
log.debug("Loaded generation value #{@value.inspect}")
|
45
|
+
end
|
46
|
+
|
47
|
+
# Open `path` read-write, creating it if it doesn't exist, refusing to
|
48
|
+
# follow symbolic links in the final path component.
|
49
|
+
#
|
50
|
+
# @param [String] path
|
51
|
+
#
|
52
|
+
# @raise [SystemCallError] in various circumstances involving failure to
|
53
|
+
# open or create the file.
|
54
|
+
#
|
55
|
+
def open(path)
|
56
|
+
File.open(path, File::RDWR | File::CREAT | File::NOFOLLOW)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_value
|
60
|
+
@fh.rewind
|
61
|
+
content = @fh.read
|
62
|
+
if content.empty?
|
63
|
+
0
|
64
|
+
else
|
65
|
+
Integer(content)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def save
|
70
|
+
@fh.rewind
|
71
|
+
@fh.puts(@value.to_s)
|
72
|
+
@fh.flush
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/reyes/version.rb
CHANGED
data/reyes.gemspec
CHANGED
@@ -35,7 +35,10 @@ Gem::Specification.new do |gem|
|
|
35
35
|
gem.version = Reyes::VERSION
|
36
36
|
|
37
37
|
gem.add_dependency 'aws-sdk', '~> 1.60'
|
38
|
+
gem.add_dependency 'chalk-log'
|
39
|
+
gem.add_dependency 'subprocess'
|
38
40
|
|
39
41
|
gem.add_development_dependency 'pry'
|
40
42
|
gem.add_development_dependency 'rake'
|
43
|
+
gem.add_development_dependency 'rubocop'
|
41
44
|
end
|
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.0.
|
4
|
+
version: 0.0.2
|
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-
|
12
|
+
date: 2015-02-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -25,6 +25,34 @@ dependencies:
|
|
25
25
|
- - ~>
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '1.60'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: chalk-log
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: subprocess
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
28
56
|
- !ruby/object:Gem::Dependency
|
29
57
|
name: pry
|
30
58
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,18 +81,46 @@ dependencies:
|
|
53
81
|
- - ! '>='
|
54
82
|
- !ruby/object:Gem::Version
|
55
83
|
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rubocop
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
56
98
|
description: ! " Reyes is a gem...\n\n TO DO\n"
|
57
99
|
email:
|
58
100
|
- security@stripe.com
|
59
|
-
executables:
|
101
|
+
executables:
|
102
|
+
- reyes
|
60
103
|
extensions: []
|
61
104
|
extra_rdoc_files: []
|
62
105
|
files:
|
106
|
+
- .gitignore
|
107
|
+
- .rubocop-disables.yml
|
108
|
+
- .rubocop.yml
|
63
109
|
- .ruby-version
|
64
110
|
- Gemfile
|
65
111
|
- README.md
|
66
112
|
- Rakefile
|
113
|
+
- bin/reyes
|
114
|
+
- config.yaml.example
|
67
115
|
- lib/reyes.rb
|
116
|
+
- lib/reyes/aws_manager.rb
|
117
|
+
- lib/reyes/config.rb
|
118
|
+
- lib/reyes/errors.rb
|
119
|
+
- lib/reyes/group_manager.rb
|
120
|
+
- lib/reyes/group_tools.rb
|
121
|
+
- lib/reyes/ipset.rb
|
122
|
+
- lib/reyes/iptables.rb
|
123
|
+
- lib/reyes/run_generation.rb
|
68
124
|
- lib/reyes/version.rb
|
69
125
|
- reyes.gemspec
|
70
126
|
homepage: ''
|