├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── aws-security-group-manager.gemspec ├── bin └── aws-security-groups ├── lib ├── aws-security-group-manager.rb └── manager │ ├── compiler.rb │ ├── core.rb │ ├── updater.rb │ └── version.rb └── spec ├── manager └── core_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | security-groups.yml 3 | vendor/ 4 | .bundle 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | bundler_args: --without guard 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | - ruby-head 6 | - jruby-18mode 7 | - jruby-19mode -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | gem "rake" 5 | 6 | group :guard do 7 | gem "guard-rspec", "~>0.6.0" 8 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | aws-security-group-manager (0.0.1) 5 | fog (~> 1.5.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | builder (3.1.4) 11 | coderay (1.0.8) 12 | diff-lcs (1.1.3) 13 | excon (0.16.10) 14 | fog (1.5.0) 15 | builder 16 | excon (~> 0.14) 17 | formatador (~> 0.2.0) 18 | mime-types 19 | multi_json (~> 1.0) 20 | net-scp (~> 1.0.4) 21 | net-ssh (>= 2.1.3) 22 | nokogiri (~> 1.5.0) 23 | ruby-hmac 24 | formatador (0.2.4) 25 | guard (1.5.4) 26 | listen (>= 0.4.2) 27 | lumberjack (>= 1.0.2) 28 | pry (>= 0.9.10) 29 | thor (>= 0.14.6) 30 | guard-rspec (0.6.0) 31 | guard (>= 0.10.0) 32 | listen (0.6.0) 33 | lumberjack (1.0.2) 34 | method_source (0.8.1) 35 | mime-types (1.19) 36 | multi_json (1.5.0) 37 | net-scp (1.0.4) 38 | net-ssh (>= 1.99.1) 39 | net-ssh (2.6.2) 40 | nokogiri (1.5.5) 41 | pry (0.9.10) 42 | coderay (~> 1.0.5) 43 | method_source (~> 0.8) 44 | slop (~> 3.3.1) 45 | rake (10.0.2) 46 | rspec (2.8.0) 47 | rspec-core (~> 2.8.0) 48 | rspec-expectations (~> 2.8.0) 49 | rspec-mocks (~> 2.8.0) 50 | rspec-core (2.8.0) 51 | rspec-expectations (2.8.0) 52 | diff-lcs (~> 1.1.2) 53 | rspec-mocks (2.8.0) 54 | ruby-hmac (0.4.0) 55 | slop (3.3.3) 56 | thor (0.16.0) 57 | 58 | PLATFORMS 59 | ruby 60 | 61 | DEPENDENCIES 62 | aws-security-group-manager! 63 | guard-rspec (~> 0.6.0) 64 | rake 65 | rspec (~> 2.8.0) 66 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard("rspec", :all_after_pass => false, :cli => "--fail-fast --color") do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) {|match| "spec/#{match[1]}_spec.rb"} 4 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Zachary Anker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Overview 2 | === 3 | This is a solution to managing AWS security groups across multiple regions, where you want to only allow certain groups of servers, without having to manually deal with the ips. 4 | 5 | You setup a configuration file of how you want it to work, as well as the security groups or ips that can have access to it. Depending on the configuration, you can do things like "Add the IP for every server in the foo security group on all regions to the bar security group, except for us-west-1, just add the actual foo group." 6 | 7 | AWS has limits on how many rules can be in a single security group (100 on EC2, 50 on VPS). Amazon also recommends you don't have too many rules in a security group as EC2 instances can have multiple security groups. 8 | 9 | Keep that in mind if you're using this tool, it's mostly intended as a way of keeping a small scale cross region deployment secure and to bridge the gap before having to deal with large scale locking down of EC2 security groups across regions. 10 | 11 | With that out of the way: 12 | 13 | Examples 14 | - 15 | 16 | See `aws-security-groups --help` for a list of commands, an example configuration file can be found at https://gist.github.com/3231660. 17 | 18 | Quick syntax notes: 19 | 20 | `group` can either be the specific name of the group (`foobar`) or `:ALL` to indicate all servers 21 | `region` you can filter it to only add servers from a specific group by a single region (`us-west-1`), servers from the same region as the security group with `:SAME`, or servers from all regions with `:ALL` 22 | `ip` can be the IP range to allow access too, `0.0.0.0/0` will give everyone access 23 | `protocol` can either be `tcp` or `udp` 24 | `port` can either be a single port (`4000`) or a range (`3000-4000`) 25 | `note` doesn't do anything, just gives you a text note of what the rule is for 26 | 27 | You can either specify `group` or `ip` per rule, but not both. 28 | 29 | Hypothetically, if you have a server setup like so: 30 | 31 | mem1 in us-west-1 and mem2 in us-east-1 (part of the memcached and default group) 32 | app1 in us-west-1 and app2 in us-east-1 (part of the app and default group) 33 | mongo1 in us-west-1 and mongo2 in us-east-1 (part of the mongo and default group) 34 | mongo-backup in us-west-2 (part of the mongo-backup and default group) 35 | monitor in us-west-2 (part of the monitor group) 36 | puppetmaster in us-west-2 (part of the puppetmaster group) 37 | rds server in us-east-1 (part of app-database) 38 | 39 | Using the example configuration file, it will generate a security group of: 40 | 41 | *us-east-1* 42 | `memcached` - app group can access it by port 11211 using TCP 43 | `mongo` - app group, app servers on us-west-1 with the IP 22.33.44.55, mongo-backup on us-west-2 with the IP 77.77.88.99, other servers in the mongo group, and the mongo server in us-west-1 with the IP 55.11.22.33 can access servers in the mongo group 44 | `default` - anyone can SSH in. the monitor server with IP 50.50.50.50 can access servers by TCP on ports 4949 and 4313 45 | `app-database` (rds) - app group, app servers on us-west-1 with the IP 22.33.44.55 can access the RDS server 46 | 47 | *us-west-1* 48 | `memcached` - app group can access it by port 11211 using TCP 49 | `mongo` - app group, app servers on us-east-1 with the IP 88.33.99.22, mongo-backup on us-west-2 with the IP 77.77.88.99, other servers in the mongo group, and the mongo server in us-east-1 with the IP 11.88.33.99 can access servers in the mongo group 50 | `default` - anyone can SSH in. the monitor server with IP 50.50.50.50 can access servers by TCP on ports 4949 and 4313 51 | 52 | *us-west-2* 53 | `monitor` - no rules handled as it's not specified in the config file 54 | `puppetmaster` - The IPs of all the servers in us-west-1 and us-east-1 are able to access TCP over port 8100, and any servers part of the monitor, mongo-backup or puppetmaster group are able to access it over TCP on port 8100 as well. 55 | 56 | If a security group is not listed in the configuration file, it will not be touched. 57 | This will not make destructive changes to your security groups, if a rule already exists in a security group, but not in the one from the config file, it will leave it alone unless you specify `--destructive` 58 | 59 | Confirmation is asked before making any changes, use `--noop` to not make any changes, or `--assumeyes` and it won't ask for confirmation before changing things. 60 | 61 | License 62 | - 63 | Available under the MIT license -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.setup 3 | 4 | require "rake" 5 | require "rspec" 6 | require "rspec/core/rake_task" 7 | 8 | RSpec::Core::RakeTask.new("spec") do |spec| 9 | spec.pattern = "spec/**/*_spec.rb" 10 | end 11 | 12 | task :default => :spec -------------------------------------------------------------------------------- /aws-security-group-manager.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 2 | require "manager/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "aws-security-group-manager" 6 | s.version = AWSSecurityGroups::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = ["Zachary Anker"] 9 | s.email = ["zach.anker@gmail.com"] 10 | s.homepage = "http://github.com/zanker/aws-security-group-manager" 11 | s.summary = "Simplifies AWS/EC2 security group management" 12 | s.description = "Gem for managing AWS/EC2 security groups across regions" 13 | 14 | s.required_rubygems_version = ">= 1.3.6" 15 | 16 | s.add_runtime_dependency "fog", "~>1.5.0" 17 | 18 | s.add_development_dependency "rspec", "~>2.8.0" 19 | s.add_development_dependency "guard-rspec", "~>0.6.0" 20 | 21 | s.executables = ["aws-security-groups"] 22 | s.files = Dir.glob("lib/**/*") + %w[LICENSE README.md Rakefile] 23 | s.require_path = "lib" 24 | end -------------------------------------------------------------------------------- /bin/aws-security-groups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") 3 | 4 | require "optparse" 5 | require "aws-security-group-manager" 6 | require "deep_merge" 7 | require "pp" 8 | 9 | options = {:aws_access_key => ENV["AWS_ACCESS_KEY"], :aws_secret_key => ENV["AWS_SECRET_KEY"]} 10 | 11 | OptionParser.new do |opts| 12 | opts.banner = "Usage: #{__FILE__} [options]" 13 | 14 | opts.separator("") 15 | opts.on("--config", "-s", String, :REQUIRED, "Configuration file (or directory of files) to base security group changes off of") {|v| options[:config] = v} 16 | opts.on("--assumeyes", "-y", :OPTIONAL, "Doesn't confirm before changing security groups") {|v| options[:assumeyes] = true} 17 | opts.on("--noop", :OPTIONAL, "No changes are made, just a list of changes it would make and it exits") {|v| options[:noop] = true} 18 | opts.on("--destructive", :OPTIONAL, "If a security group is being managed and a rule isn't found, it will remove it") {|v| options[:destructive] = true} 19 | 20 | opts.on("--aws-access-key", "-O", String, :OPTIONAL, "AWS Access Key to use. Defaults to the value of AWS_ACCESS_KEY") {|v| options[:aws_access_key] = v} 21 | opts.on("--aws-secret-key", "-W", String, :OPTIONAL, "AWS Secret Key to use. Defaults to the value of AWS_SECRET_KEY") {|v| options[:aws_secret_key] = v} 22 | end.parse! 23 | 24 | if options[:aws_access_key].nil? or options[:aws_access_key] == "" 25 | puts "*** No AWS Access Key specified" 26 | exit 27 | end 28 | 29 | if options[:aws_secret_key].nil? or options[:aws_secret_key] == "" 30 | puts "*** No AWS Secret Key specified" 31 | exit 32 | end 33 | 34 | unless File.exists?(options[:config]) 35 | puts "*** Cannot load configuration file" 36 | exit 37 | end 38 | 39 | core = AWSSecurityGroups::Core.new(options[:aws_access_key], options[:aws_secret_key]) 40 | 41 | puts "Loading regions" 42 | core.load_regions 43 | 44 | puts "Loading server list" 45 | core.load_servers 46 | 47 | puts "Loading security groups" 48 | core.load_security_groups 49 | 50 | 51 | files = [] 52 | if File.directory?(options[:config]) 53 | puts options[:config] 54 | files = Dir.glob("#{options[:config]}/*.{yaml,yml}") 55 | else 56 | files.push(options[:config]) 57 | end 58 | 59 | configs = {} 60 | files.each do |file| 61 | group_config = YAML::load(File.read(file)) 62 | configs.deep_merge(group_config) 63 | end 64 | 65 | new_groups = {} 66 | configs.each do |product, settings| 67 | puts "Compiling security groups for #{product.upcase}" 68 | new_groups[product] = core.compile_security_groups(product, settings) 69 | end 70 | 71 | # Easy map of ip -> name for clarity 72 | @ip_to_name = {} 73 | core.servers["ec2"].each do |region, servers| 74 | servers.each do |instance_id, instance| 75 | name = instance[:tags]["Name"] 76 | if !name 77 | name = instance[:tags].inspect 78 | end 79 | 80 | @ip_to_name["#{instance[:ip_address]}/32"] = "#{instance[:az]}, #{name}" 81 | end 82 | end 83 | 84 | # Summarize what changes we would be making 85 | def puts_rule(product, rule, prefix="") 86 | if product == "ec2" 87 | if rule[:from_port] == rule[:to_port] 88 | port = rule[:from_port] 89 | else 90 | port = "#{rule[:from_port]}-#{rule[:to_port]}" 91 | end 92 | 93 | if rule[:ip] 94 | puts "#{prefix}IP range #{rule[:ip]} (#{@ip_to_name[rule[:ip]] || "unknown"}) on #{port} over #{rule[:protocol]}" 95 | else 96 | puts "#{prefix}Group #{rule[:group]} on #{port} over #{rule[:protocol]}" 97 | end 98 | elsif product == "rds" 99 | if rule[:ip] 100 | puts "#{prefix}IP range #{rule[:ip]} (#{@ip_to_name[rule[:ip]] || "unknown"})" 101 | else 102 | puts "#{prefix}Group #{rule[:group]}" 103 | end 104 | end 105 | end 106 | 107 | added_rules, removed_rules = {}, {} 108 | has_destructive = nil 109 | 110 | puts 111 | new_groups.each do |product, groups| 112 | puts "**** Summary for #{product.upcase}" 113 | 114 | added_rules[product] ||= {} 115 | removed_rules[product] ||= {} 116 | 117 | regions = {} 118 | groups.each do |group_name, data| 119 | data.each do |region, group| 120 | regions[region] ||= {} 121 | regions[region][group_name] = group 122 | end 123 | end 124 | 125 | regions.each do |region, data| 126 | added_rules[product][region] ||= {} 127 | removed_rules[product][region] ||= {} 128 | 129 | data.each do |group_name, list| 130 | puts "** #{region}, #{group_name}" 131 | 132 | added_rules[product][region][group_name] = {} 133 | removed_rules[product][region][group_name] = {} 134 | 135 | # List any new rules 136 | list.sort {|a, b| (a[:ip] || a[:group]) <=> (b[:ip] || b[:group]) }.reverse.each do |rule| 137 | rule_id = "#{rule[:ip]}#{rule[:group]}#{rule[:protocol]}#{rule[:from_port]}#{rule[:to_port]}" 138 | added_rules[product][region][group_name][rule_id] = true 139 | 140 | next if core.security_groups[product][region][group_name][:rules][rule_id] 141 | puts_rule(product, rule, "NEW: ") 142 | 143 | added_rules[product][region][group_name][rule_id] = rule 144 | end 145 | 146 | core.security_groups[product][region][group_name][:rules].each do |rule_id, rule| 147 | # Already exists, no change 148 | if added_rules[product][region][group_name][rule_id] 149 | puts_rule(product, rule, "SAME: ") 150 | 151 | # Removing it 152 | elsif options[:destructive] and !removed_rules[product][region][group_name][rule_id] 153 | puts_rule(product, rule, "REMOVING: ") 154 | removed_rules[product][region][group_name][rule_id] = rule 155 | 156 | has_destructive = true 157 | end 158 | end 159 | 160 | added_rules[product][region][group_name] = added_rules[product][region][group_name].values 161 | added_rules[product][region][group_name].delete_if {|v| v == true } 162 | 163 | removed_rules[product][region][group_name] = removed_rules[product][region][group_name].values 164 | 165 | puts 166 | end 167 | 168 | puts 169 | end 170 | 171 | puts 172 | end 173 | 174 | # Nope, we're done 175 | if options[:noop] 176 | puts "--noop, no changes made" 177 | exit 178 | end 179 | 180 | # Confirm 181 | unless options[:assumeyes] 182 | puts "**** ARE YOU SURE ****" 183 | 184 | if options[:destructive] 185 | if has_destructive 186 | puts "This will make destructive changes to your security groups and remove anything listed under \"Destructive\". Please make sure the changes are correct." 187 | else 188 | puts "Destructive changes are enabled, but no changes require a rule to be removed." 189 | end 190 | end 191 | 192 | print "Type [Confirm] to change: " 193 | 194 | confirm = gets.chomp 195 | unless confirm.to_s.downcase == "confirm" 196 | puts "Exited, no changes made." 197 | exit 198 | end 199 | end 200 | 201 | puts 202 | puts "Here we go..." 203 | puts 204 | 205 | new_groups.each_key do |product| 206 | # First add any new rules so if something breaks, it won't lock out people 207 | if added_rules[product] 208 | added_rules[product].each do |region, groups| 209 | updater = AWSSecurityGroups::Updater.new(:product => product, :region => region, :owner_id => core.owner_id, :access_key => options[:aws_access_key], :secret_key => options[:aws_secret_key]) 210 | 211 | groups.each do |name, rules| 212 | next if rules.empty? 213 | 214 | puts 215 | puts "Adding rules for #{product.upcase} in #{region} for #{name}" 216 | updater.add_rules(name, rules) 217 | end 218 | end 219 | end 220 | 221 | # Then remove 222 | if options[:destructive] and removed_rules[product] 223 | puts 224 | 225 | removed_rules[product].each do |region, groups| 226 | updater = AWSSecurityGroups::Updater.new(:product => product, :region => region, :owner_id => core.owner_id, :access_key => options[:aws_access_key], :secret_key => options[:aws_secret_key]) 227 | 228 | groups.each do |name, rules| 229 | next if rules.empty? 230 | 231 | puts 232 | puts "Removing rules for #{product.upcase} in #{region} for #{name}" 233 | updater.remove_rules(name, rules) 234 | end 235 | end 236 | end 237 | 238 | puts 239 | end 240 | 241 | puts 242 | puts "Finished" 243 | -------------------------------------------------------------------------------- /lib/aws-security-group-manager.rb: -------------------------------------------------------------------------------- 1 | path = File.expand_path("../manager", __FILE__) 2 | require "#{path}/version" 3 | require "#{path}/core" 4 | require "#{path}/compiler" 5 | require "#{path}/updater" 6 | -------------------------------------------------------------------------------- /lib/manager/compiler.rb: -------------------------------------------------------------------------------- 1 | module AWSSecurityGroups 2 | class Compiler 3 | def initialize(security_groups, servers) 4 | @security_groups, @servers = security_groups, servers 5 | end 6 | 7 | def build(product, region, group_name, group_config) 8 | unless product == "ec2" or product == "rds" 9 | raise "Unknown or unsupported AWS product #{product}" 10 | end 11 | 12 | compiled = [] 13 | added_groups = {} 14 | 15 | group_config.each do |config| 16 | if product == "ec2" 17 | unless config.has_key?("protocol") and config.has_key?("port") 18 | puts "WARNING: #{product}, #{group_name}, #{group_config}" 19 | puts "Cannot find protocol and/or port" 20 | end 21 | 22 | next unless config["protocol"] and config["port"] 23 | 24 | if config["port"].is_a?(String) 25 | from_port, to_port = config["port"].split("-", 2) 26 | else 27 | from_port = config["port"] 28 | end 29 | 30 | to_port ||= from_port 31 | end 32 | 33 | # We're configuring a group to have access 34 | if config["group"] 35 | groups, ips = self.find_servers(region, config) 36 | groups.each do |group| 37 | added_groups["#{group}#{config["protocol"]}#{from_port}#{to_port}"] = {:group => group, :protocol => config["protocol"], :from_port => from_port, :to_port => to_port} 38 | end 39 | 40 | # We're configuring an IP range to have access 41 | elsif config["ip"] 42 | ips = [config["ip"]] 43 | end 44 | 45 | ips.each do |ip| 46 | compiled.push(:ip => ip, :from_port => from_port, :to_port => to_port, :protocol => config["protocol"]) 47 | end 48 | end 49 | 50 | compiled.concat(added_groups.values) 51 | 52 | compiled 53 | end 54 | 55 | # Find servers and groups matching the ruleset given 56 | def find_servers(region, config) 57 | groups, ips = [], [] 58 | 59 | if config["region"] == :SAME 60 | return [config["group"]], ips 61 | end 62 | 63 | skip = {} 64 | if config["skip"].is_a?(Array) 65 | config["skip"].each {|k| skip[k] = true} 66 | elsif config["skip"].is_a?(String) 67 | skip[config["skip"]] = true 68 | end 69 | 70 | @servers.each do |server_region, list| 71 | # Only add servers that are part of the security group in a specific region 72 | if config["region"].is_a?(String) and config["region"] != server_region 73 | next 74 | end 75 | 76 | list.each do |instance_id, instance| 77 | # We're filtering by a specific group 78 | if config["group"].is_a?(String) and !instance[:group_names].include?(config["group"]) 79 | next 80 | end 81 | 82 | skip_instance = nil 83 | instance[:group_names].each do |group| 84 | if skip[group] 85 | skip_instance = true 86 | break 87 | end 88 | end 89 | 90 | next if skip_instance 91 | 92 | # Same region, so add the security group to itself 93 | if server_region == region 94 | if config["group"] == :ALL 95 | instance[:group_names].each do |group| 96 | groups.push(group) 97 | end 98 | else 99 | groups.push(config["group"]) 100 | end 101 | 102 | # Different region, authorize the IP 103 | else 104 | ips.push("#{instance[:ip_address]}/32") 105 | end 106 | end 107 | end 108 | 109 | return groups, ips 110 | end 111 | end 112 | end -------------------------------------------------------------------------------- /lib/manager/core.rb: -------------------------------------------------------------------------------- 1 | require "fog" 2 | require "yaml" 3 | 4 | module AWSSecurityGroups 5 | class Core 6 | attr_reader :servers, :security_groups, :owner_id 7 | 8 | def initialize(access_key, secret_key) 9 | @access_key, @secret_key = access_key, secret_key 10 | end 11 | 12 | def load_regions 13 | aws = Fog::Compute.new(:provider => "AWS", :region => "us-east-1", :aws_access_key_id => @access_key, :aws_secret_access_key => @secret_key) 14 | @regions = aws.describe_regions.body["regionInfo"].map {|r| r["regionName"]} 15 | end 16 | 17 | def load_security_groups 18 | @security_groups = {"ec2" => {}, "rds" => {}} 19 | 20 | # EC2 21 | @regions.each do |region| 22 | @security_groups["ec2"][region] = {} 23 | 24 | aws = Fog::Compute.new(:provider => "AWS", :region => region, :aws_access_key_id => @access_key, :aws_secret_access_key => @secret_key) 25 | aws.security_groups.each do |group| 26 | rules = {} 27 | group.ip_permissions.each do |rule| 28 | next if rule["ipProtocol"] != "tcp" and rule["ipProtocol"] != "udp" 29 | 30 | rule["groups"].each do |group| 31 | rules["#{group["groupName"]}#{rule["ipProtocol"]}#{rule["fromPort"]}#{rule["toPort"]}"] = {:group => group["groupName"], :protocol => rule["ipProtocol"], :from_port => rule["fromPort"], :to_port => rule["toPort"]} 32 | end 33 | 34 | rule["ipRanges"].each do |ip| 35 | rules["#{ip["cidrIp"]}#{rule["ipProtocol"]}#{rule["fromPort"]}#{rule["toPort"]}"] = {:ip => ip["cidrIp"], :protocol => rule["ipProtocol"], :from_port => rule["fromPort"], :to_port => rule["toPort"]} 36 | end 37 | end 38 | 39 | @security_groups["ec2"][region][group.name] = {:group_id => group.group_id, :owner_id => group.owner_id, :rules => rules} 40 | @owner_id = group.owner_id 41 | end 42 | 43 | @security_groups["ec2"].delete(region) if @security_groups["ec2"][region].empty? 44 | end 45 | 46 | # RDS 47 | @regions.each do |region| 48 | @security_groups["rds"][region] = {} 49 | 50 | rds = Fog::AWS::RDS.new(:region => region, :aws_access_key_id => @access_key, :aws_secret_access_key => @secret_key) 51 | rds.describe_db_security_groups.body["DescribeDBSecurityGroupsResult"]["DBSecurityGroups"].each do |group| 52 | rules = {} 53 | 54 | group["EC2SecurityGroups"].each do |data| 55 | rules[data["EC2SecurityGroupName"]] = {:group => data["EC2SecurityGroupName"]} 56 | end 57 | 58 | group["IPRanges"].each do |ip| 59 | rules[ip["CIDRIP"]] = {:ip => ip["CIDRIP"]} 60 | end 61 | 62 | @security_groups["rds"][region][group["DBSecurityGroupName"]] = {:owner_id => group["OwnerId"], :rules => rules} 63 | @owner_id = group["OwnerId"] 64 | end 65 | end 66 | end 67 | 68 | def load_servers 69 | @servers = {"ec2" => {}, "rds" => {}} 70 | 71 | # Load a list of EC2 servers 72 | @regions.each do |region| 73 | @servers["ec2"][region] = {} 74 | 75 | aws = Fog::Compute.new(:provider => "AWS", :region => region, :aws_access_key_id => @access_key, :aws_secret_access_key => @secret_key) 76 | aws.describe_instances("instance-state-name" => "running").body["reservationSet"].each do |instance| 77 | instance_set = instance["instancesSet"].first 78 | @servers["ec2"][region][instance_set["instanceId"]] = {:tags => instance_set["tagSet"], :az => instance_set["placement"]["availabilityZone"], :group_names => instance["groupSet"], :group_ids => instance["groupIds"], :ip_address => instance_set["ipAddress"]} 79 | end 80 | 81 | @servers["ec2"].delete(region) if @servers["ec2"][region].empty? 82 | end 83 | 84 | # Now RDS 85 | @regions.each do |region| 86 | @servers["rds"][region] = {} 87 | 88 | rds = Fog::AWS::RDS.new(:region => region, :aws_access_key_id => @access_key, :aws_secret_access_key => @secret_key) 89 | rds.describe_db_instances.body["DescribeDBInstancesResult"]["DBInstances"].each do |instance| 90 | group_names = [] 91 | instance["DBSecurityGroups"].each do |group| 92 | group_names.push(group["DBSecurityGroupName"]) if group["Status"] == "active" 93 | end 94 | 95 | @servers["rds"][region][instance["DBInstanceIdentifier"]] = {:group_names => group_names} 96 | end 97 | 98 | @servers["rds"].delete(region) if @servers["rds"][region].empty? 99 | end 100 | end 101 | 102 | def compile_security_groups(product, settings) 103 | compiler = Compiler.new(@security_groups[product], @servers["ec2"]) 104 | 105 | new_groups = {} 106 | settings.each do |group_name, group_config| 107 | new_groups[group_name] = {} 108 | 109 | @regions.each do |region| 110 | # Don't set security groups if the region has no servers 111 | # Or if the region doesn't actually have the security group, that would be pointless 112 | unless @servers[product][region] and @security_groups[product][region] and @security_groups[product][region][group_name] 113 | next 114 | end 115 | 116 | new_groups[group_name][region] = compiler.build(product, region, group_name, group_config) 117 | end 118 | end 119 | 120 | new_groups 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/manager/updater.rb: -------------------------------------------------------------------------------- 1 | module AWSSecurityGroups 2 | class Updater 3 | def initialize(args) 4 | @product, @region, @owner_id = args[:product], args[:region], args[:owner_id] 5 | 6 | if @product == "ec2" 7 | @aws = Fog::Compute.new(:provider => "AWS", :region => @region, :aws_access_key_id => args[:access_key], :aws_secret_access_key => args[:secret_key]) 8 | elsif @product == "rds" 9 | @aws = Fog::AWS::RDS.new(:region => @region, :aws_access_key_id => args[:access_key], :aws_secret_access_key => args[:secret_key]) 10 | end 11 | end 12 | 13 | def add_rules(group_name, rules) 14 | self.send("add_#{@product}_rules", group_name, rules) 15 | end 16 | 17 | def remove_rules(group_name, rules) 18 | self.send("remove_#{@product}_rules", group_name, rules) 19 | end 20 | 21 | private 22 | # EC2 23 | def format_ec2_rule(rule) 24 | if rule[:ip] 25 | data = {"IpRanges" => [{"CidrIp" => rule[:ip]}]} 26 | else 27 | data = {"Groups" => [{"GroupName" => rule[:group], "UserId" => @owner_id}]} 28 | end 29 | 30 | {"IpPermissions" => [data.merge("IpProtocol" => rule[:protocol], "FromPort" => rule[:from_port].to_i, "ToPort" => rule[:to_port].to_i)]} 31 | end 32 | 33 | def add_ec2_rules(group_name, rules) 34 | rules.each do |rule| 35 | data = format_ec2_rule(rule) 36 | 37 | puts "AUTHORIZE: #{data}" 38 | @aws.authorize_security_group_ingress(group_name, data) 39 | end 40 | end 41 | 42 | def remove_ec2_rules(group_name, rules) 43 | rules.each do |rule| 44 | data = format_ec2_rule(rule) 45 | 46 | puts "REVOKING: #{data}" 47 | @aws.revoke_security_group_ingress(group_name, data) 48 | end 49 | end 50 | 51 | # RDS 52 | def add_rds_rules(group_name, rules) 53 | rules.each do |rule| 54 | if rule[:ip] 55 | data = {"CIDRIP" => rule[:ip]} 56 | else 57 | data = {"EC2SecurityGroupName" => rule[:group], "EC2SecurityGroupOwnerId" => @owner_id} 58 | end 59 | 60 | puts "AUTHORIZE: #{data}" 61 | @aws.authorize_db_security_group_ingress(group_name, {"CIDRIP" => rule[:ip]}) 62 | end 63 | end 64 | 65 | def remove_rds_rules(group_name, rules) 66 | rules.each do |rule| 67 | if rule[:ip] 68 | data = {"CIDRIP" => rule[:ip]} 69 | else 70 | data = {"EC2SecurityGroupName" => rule[:group], "EC2SecurityGroupOwnerId" => @owner_id} 71 | end 72 | 73 | puts "REVOKING: #{data}" 74 | @aws.revoke_db_security_group_ingress(group_name, data) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/manager/version.rb: -------------------------------------------------------------------------------- 1 | module AWSSecurityGroups 2 | VERSION = "0.0.1" 3 | end -------------------------------------------------------------------------------- /spec/manager/core_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe AWSSecurityGroups::Core do 4 | 5 | 6 | end 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | path = File.expand_path("../../", __FILE__) 2 | require "#{path}/lib/aws-security-group-manager" 3 | 4 | Dir["#{path}/spec/support/*.rb"].each {|file| require file} 5 | 6 | RSpec.configure do |c| 7 | c.mock_with :rspec 8 | end 9 | --------------------------------------------------------------------------------