portalign 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2012 The Agile League, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ portalign
2
+ =========
3
+ By Micah Wedemeyer of [The Agile League](http://agileleague.com)
4
+
5
+
6
+ Summary
7
+ =======
8
+ A tool to automatically add and remove your current IP address to Amazon EC2 security groups.
9
+
10
+ Description
11
+ ===========
12
+
13
+ It's good policy to keep your security groups as restrictive as
14
+ possible. But, as is often the case with security, convenience suffers
15
+ as you get more secure. portalign is a tool that allows you to maintain
16
+ restrictive policies in your security groups while also giving
17
+ convenient access.
18
+
19
+ With portalign, you can easily update a security group to add your
20
+ current IP address to the list of allowed IPs for a port (usually 22).
21
+ So, instead of complicated port knocking schemes or ssh tunnelling
22
+ through other allowed EC2 nodes, you can ssh directly in to your machine
23
+ normally.
24
+
25
+ When you're done working on the node, portalign can restore the policy back to extreme strictness. Enable the port, do your work, disable the port. Easy and secure.
26
+
27
+ Quick Start
28
+ ===========
29
+
30
+ * gem install portalign
31
+ * create a .portalign.yml file in your current project with your AWS
32
+ credentials and the security group.
33
+ * portalign
34
+ * ssh me@myserver (do what you need to on the server)
35
+ * portalign -d (to remove the authorization when you're done)
36
+
37
+
38
+ Installation
39
+ ============
40
+
41
+ * gem install portalign
42
+
43
+ Configuration
44
+ =============
45
+
46
+ The preferred configuration method is using a .portalign.yml file. It
47
+ will look for one in $HOME/.portalign.yml and $PWD/.portalign.yml Any
48
+ settings found in the current directory will override those in the $HOME
49
+ directory. So, if you have multiple projects with different security
50
+ groups, you can set your AWS credentials in one file (in $HOME) and put
51
+ the various security groups in configuration files in each project.
52
+
53
+ Example File:
54
+
55
+ access_key_id: "acb1234"
56
+ secret_access_key: "1234abc"
57
+ security_groups:
58
+ - "mygroup"
59
+ - "othergroup"
60
+ ports:
61
+ - 22
62
+ - 8080
63
+ - 10000
64
+
65
+ Note: As you're probably aware, your AWS credentials are the keys to the kingdom. It's a good idea to restrict and protect the .portalign.yml file as you would a private key. chmod 600 is a good start.
66
+
67
+ Configuration Options
68
+ ---------------------
69
+ * access_key_id - AWS access key
70
+ * secret_access_key - AWS secret access key
71
+ * security_groups - A list of EC2 security groups (by name, not id)
72
+ * ports - A list of ports to open (defaults to 22)
73
+ * protocol - The protocol (tcp, udp, icmp), defaults to tcp
74
+
75
+ Usage
76
+ =====
77
+ To add your current IP to the security group
78
+
79
+ portalign
80
+
81
+ To add 0.0.0.0/0 (wide open, allow any IP) to the security group
82
+
83
+ portalign -w
84
+
85
+ To remove your current IP (and 0.0.0.0/0) from the security group
86
+
87
+ portalign -d
88
+
89
+ You can also specify many configuration options on the command line. Those specified on the command line will override anything from a config file.
90
+
91
+ portalign --access-key-id=abc123 --secret-access-key=123abc --ports=22,80 --security-groups=mygroup,othergroup
92
+
93
+ License
94
+ -------
95
+ See LICENSE.md for details.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+
4
+ task :default => :spec
data/bin/portalign ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib portalign]))
4
+ require File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib portalign config]))
5
+
6
+ args = ARGV
7
+ config = Portalign.build_config(args)
8
+ valid, msg = Portalign.validate_config(config)
9
+ if valid
10
+ Portalign.run(config)
11
+ else
12
+ puts msg
13
+ exit
14
+ end
@@ -0,0 +1,93 @@
1
+ require 'yaml'
2
+ require 'optparse'
3
+
4
+ class Portalign
5
+ module Config
6
+ CONFIG_FILE_NAME = ".portalign.yml"
7
+ CONFIG_FILE_PATHS = ["#{ENV["HOME"]}/#{CONFIG_FILE_NAME}", "#{CONFIG_FILE_NAME}"]
8
+
9
+ def self.load_from_file
10
+ {}.tap do |config|
11
+ config_file_paths.each do |config_path|
12
+ if File.exist?(config_path)
13
+ YAML.load_file(config_path).each do |k,v|
14
+ config[k.to_sym] = v
15
+ end
16
+ end
17
+ end
18
+
19
+ %w(ports security_groups).each {|opt| force_array(config, opt)}
20
+ end
21
+ end
22
+
23
+ def self.parse_opts(args)
24
+ {}.tap do |config|
25
+ opts = OptionParser.new do |opts|
26
+ opts.banner = "Usage: portalign [options]"
27
+ opts.separator ""
28
+ opts.separator "General options:"
29
+ opts.on("--access-key-id=ACCESS_KEY_ID", "The AWS access key id. Better to specify in a config file. See README") do |access_key_id|
30
+ config[:access_key_id] = access_key_id
31
+ end
32
+ opts.on("--secret-access-key=SECRET_ACCESS_KEY", "The AWS secret access key. Better to specify in a config file. See README") do |secret_access_key|
33
+ config[:secret_access_key] = secret_access_key
34
+ end
35
+ opts.on("-p PORTS", "--ports=PORTS", Array, "A comma delimited list of ports to align. Defaults to 22") do |ports|
36
+ config[:ports] = ports.map(&:to_i)
37
+ end
38
+ opts.on("-s SECURITY_GROUPS", "--security_groups=SECURITY_GROUPS", Array, "A comma delimited list of security groups to update.") do |security_groups|
39
+ config[:security_groups] = security_groups
40
+ end
41
+ opts.on("--protocol=PROTOCOL", [:tcp, :udp, :icmp], "The protocol to use. Defaults to tcp.") do |protocol|
42
+ config[:protocol] = protocol
43
+ end
44
+ opts.on("-d", "--deauthorize", "Remove the current IP (and 0.0.0.0/0) from the security groups.") do |deauthorize|
45
+ config[:deauthorize] = deauthorize
46
+ end
47
+ opts.on("-w", "--wide", "Authorizes 0.0.0.0/0 in the security groups.") do |wide|
48
+ config[:wide] = wide
49
+ end
50
+ opts.separator ""
51
+ opts.separator "Common options:"
52
+ opts.on_tail("-h", "--help", "Show this message") do
53
+ puts opts
54
+ exit
55
+ end
56
+ opts.on_tail("-v", "--version", "Show version") do
57
+ puts "portalign v#{Portalign::VERSION}"
58
+ exit
59
+ end
60
+ end
61
+
62
+ opts.parse!(args)
63
+
64
+ %w(ports security_groups).each {|opt| force_array(config, opt)}
65
+ end
66
+ end
67
+
68
+ def self.validate_config(config)
69
+ unless config.keys.include?(:access_key_id) && config.keys.include?(:secret_access_key)
70
+ return [false, "You must specify an AWS access_key_id and secret_access_key"]
71
+ end
72
+
73
+ unless config[:security_groups] && config[:security_groups].any?
74
+ return [false, "You must specify at least one security group."]
75
+ end
76
+
77
+ true
78
+ end
79
+
80
+ protected
81
+
82
+ def self.config_file_paths
83
+ CONFIG_FILE_PATHS
84
+ end
85
+
86
+ def self.force_array(config, option)
87
+ option = option.to_sym
88
+ if config[option]
89
+ config[option] = config[option].is_a?(Array) ? config[option] : [config[option]]
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ class Portalign
2
+ VERSION = "0.1.0"
3
+ end
data/lib/portalign.rb ADDED
@@ -0,0 +1,102 @@
1
+ require "open-uri"
2
+ require "aws"
3
+
4
+ require File.join(File.dirname(__FILE__), "portalign", "version")
5
+ require File.join(File.dirname(__FILE__), "portalign", "config")
6
+
7
+ class Portalign
8
+ CHECK_IP_URL = "http://checkip.dyndns.org"
9
+ CHECK_IP_REGEX = /(\d+\.){3}\d+/
10
+
11
+ NARROW_CIDR = "32"
12
+ WIDE_IP = "0.0.0.0"
13
+ WIDE_CIDR = "0"
14
+
15
+ def self.build_config(args)
16
+ {
17
+ :ports => [22],
18
+ :wide => false,
19
+ :deauthorize => false,
20
+ :protocol => "tcp"
21
+ }.merge!(Config.load_from_file).merge!(Config.parse_opts(args))
22
+ end
23
+
24
+ def self.validate_config(config)
25
+ Config.validate_config(config)
26
+ end
27
+
28
+ def self.run(config)
29
+ unless config[:wide]
30
+ ip_address = resolve_ip
31
+ exit unless ip_address
32
+ puts "Resolved local IP to #{ip_address}"
33
+ end
34
+
35
+ ec2 = ec2_instance(config[:access_key_id], config[:secret_access_key])
36
+
37
+ if config[:deauthorize]
38
+ deauthorize_ingress(ec2, ip_address, NARROW_CIDR, config[:security_groups], config[:ports], config[:protocol])
39
+ elsif config[:wide]
40
+ authorize_ingress(ec2, WIDE_IP, WIDE_CIDR, config[:security_groups], config[:ports], config[:protocol])
41
+ else
42
+ authorize_ingress(ec2, ip_address, NARROW_CIDR, config[:security_groups], config[:ports], config[:protocol])
43
+ end
44
+ end
45
+
46
+ def self.authorize_ingress(ec2, ip_address, cidr, security_groups, ports, protocol)
47
+ security_groups.each do |security_group|
48
+ ports.each do |port|
49
+ puts "Authorizing #{ip_address}/#{cidr} for #{security_group} on port #{port}"
50
+ begin
51
+ ec2.authorize_security_group_IP_ingress(security_group, port, port, protocol, "#{ip_address}/#{cidr}")
52
+ rescue Aws::AwsError => e
53
+ # It will throw an error if already authorized, but that's OK
54
+ # with us.
55
+ unless e.message =~ /has already been authorized/
56
+ raise
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.deauthorize_ingress(ec2, ip_address, cidr, security_groups, ports, protocol)
64
+ security_groups.each do |security_group|
65
+ ports.each do |port|
66
+ # We deauthorize both the specific IP and also the wide open IP
67
+ puts "Deauthorizing #{ip_address}/#{cidr} for #{security_group} on port #{port}"
68
+ ec2.revoke_security_group_IP_ingress(security_group, port, port, protocol, "#{ip_address}/#{cidr}")
69
+
70
+ puts "Deauthorizing #{WIDE_IP}/#{WIDE_CIDR} for #{security_group} on port #{port}"
71
+ ec2.revoke_security_group_IP_ingress(security_group, port, port, protocol, "#{WIDE_IP}/#{WIDE_CIDR}")
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.resolve_ip
77
+ # TODO: Perhaps have several services in case one is down?
78
+ begin
79
+ parse_checkip(call_checkip)
80
+ rescue Exception => e
81
+ puts "Unable to resolve local IP address. Exiting."
82
+ puts "Error: #{e.message}"
83
+ false
84
+ end
85
+ end
86
+
87
+ protected
88
+
89
+ def self.ec2_instance(access_key_id, secret_access_key)
90
+ logger = Logger.new(File.new("/dev/null", "w"))
91
+ @ec2_instance ||= Aws::Ec2.new(access_key_id, secret_access_key, :logger => logger)
92
+ end
93
+
94
+ def self.call_checkip
95
+ open(CHECK_IP_URL).read
96
+ end
97
+
98
+ def self.parse_checkip(response)
99
+ match_data = CHECK_IP_REGEX.match(response)
100
+ match_data ? match_data[0] : nil
101
+ end
102
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+ describe Portalign::Config do
5
+ let(:access_key_id) { "123abc" }
6
+ let(:secret_access_key) { "abc123" }
7
+ let(:ports) { [22,80] }
8
+ let(:security_groups) { ["mygroup"] }
9
+
10
+ context "reading from the YAML files" do
11
+
12
+ let(:config1) do
13
+ t = Tempfile.new("config1")
14
+ t.write("access_key_id: wrong_key\n")
15
+ t.write("secret_access_key: wrong_key\n")
16
+ t.write("security_groups: #{security_groups.join(',')}\n")
17
+ t.close
18
+ t
19
+ end
20
+
21
+ let(:config2) do
22
+ t = Tempfile.new("config2")
23
+ t.write("access_key_id: #{access_key_id}\n")
24
+ t.write("secret_access_key: #{secret_access_key}\n")
25
+ t.write("ports:\n")
26
+ ports.each { |p| t.write("- #{p}\n") }
27
+ t.close
28
+ t
29
+ end
30
+
31
+ before do
32
+ Portalign::Config.stub(:config_file_paths).and_return([config1.path, config2.path])
33
+ end
34
+
35
+ it "should parse the files in order" do
36
+ Portalign::Config.load_from_file.should == {
37
+ :access_key_id => access_key_id,
38
+ :secret_access_key => secret_access_key,
39
+ :ports => ports,
40
+ :security_groups => security_groups
41
+ }
42
+ end
43
+
44
+ end
45
+
46
+ context "parsing the CLI options" do
47
+ let(:args) { "--access-key-id=#{access_key_id} --secret-access-key #{secret_access_key} --ports=#{ports.join(',')} -s #{security_groups.join(',')}".split(/\s/) }
48
+ it "should extract all the options" do
49
+ Portalign::Config.parse_opts(args).should == {
50
+ :access_key_id => access_key_id,
51
+ :secret_access_key => secret_access_key,
52
+ :ports => ports,
53
+ :security_groups => security_groups
54
+ }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe Portalign do
4
+
5
+ let(:portalign_config) do
6
+ {
7
+ :access_key_id => "54321",
8
+ :secret_access_key => "12345",
9
+ :security_groups => "portalign",
10
+ :ports => 22,
11
+ :protocol => "tcp"
12
+ }
13
+ end
14
+
15
+ let(:current_ip) { "66.56.44.38" }
16
+
17
+ let(:checkip_response) do
18
+ "<html><head><title>Current IP Check</title></head><body>Current IP Address: #{current_ip}</body></html>"
19
+ end
20
+
21
+ let(:ec2_instance) { stub("ec2_instance") }
22
+
23
+ before do
24
+ Portalign.stub(:call_checkip).and_return(checkip_response)
25
+ end
26
+
27
+ context "#resolve_ip" do
28
+ it "should call checkip" do
29
+ Portalign.should_receive(:call_checkip).and_return(checkip_response)
30
+ Portalign.resolve_ip
31
+ end
32
+
33
+ it "should get the correct IP address" do
34
+ Portalign.resolve_ip.should == current_ip
35
+ end
36
+ end
37
+
38
+ context "#authorize_ingress" do
39
+ context "successfully" do
40
+ it "should update the security group" do
41
+ ec2_instance.should_receive(:authorize_security_group_IP_ingress).with(
42
+ portalign_config[:security_groups],
43
+ portalign_config[:ports],
44
+ portalign_config[:ports],
45
+ "tcp",
46
+ "#{current_ip}/32"
47
+ )
48
+
49
+ Portalign.authorize_ingress(ec2_instance, current_ip, Portalign::NARROW_CIDR, [portalign_config[:security_groups]], [portalign_config[:ports]], portalign_config[:protocol])
50
+ end
51
+ end
52
+ end
53
+
54
+ context "#deauthorize_ingress" do
55
+ context "successfully" do
56
+ it "should update the security group" do
57
+ ec2_instance.should_receive(:revoke_security_group_IP_ingress).with(
58
+ portalign_config[:security_groups],
59
+ portalign_config[:ports],
60
+ portalign_config[:ports],
61
+ "tcp",
62
+ "#{current_ip}/32"
63
+ )
64
+
65
+ ec2_instance.should_receive(:revoke_security_group_IP_ingress).with(
66
+ portalign_config[:security_groups],
67
+ portalign_config[:ports],
68
+ portalign_config[:ports],
69
+ "tcp",
70
+ "0.0.0.0/0"
71
+ )
72
+
73
+ Portalign.deauthorize_ingress(ec2_instance, current_ip, Portalign::NARROW_CIDR, [portalign_config[:security_groups]], [portalign_config[:ports]], portalign_config[:protocol])
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,9 @@
1
+ require 'rspec'
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'portalign')
4
+
5
+ RSpec.configure do |config|
6
+ config.color_enabled = true
7
+ config.formatter = 'progress'
8
+ end
9
+
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: portalign
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Micah Wedemeyer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws
16
+ requirement: &70273097692780 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - =
20
+ - !ruby/object:Gem::Version
21
+ version: 2.5.6
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70273097692780
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &70273097692380 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70273097692380
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &70273097691800 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.10.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70273097691800
47
+ description: Easily set and unset your current IP as an allowed ingress for a given
48
+ EC2 security group. This allows you to securely close port 22 (or whatever you use
49
+ for SSH) except for your current exact IP.
50
+ email: micah@agileleague.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - lib/portalign/config.rb
56
+ - lib/portalign/version.rb
57
+ - lib/portalign.rb
58
+ - bin/portalign
59
+ - Rakefile
60
+ - LICENSE.md
61
+ - README.md
62
+ - spec/portalign/config_spec.rb
63
+ - spec/portalign_spec.rb
64
+ - spec/spec_helper.rb
65
+ homepage: http://github.com/agileleague/portalign
66
+ licenses: []
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: 1.3.6
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 1.8.15
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: A tool to automatically add and remove your current IP address to Amazon
89
+ EC2 security groups.
90
+ test_files:
91
+ - spec/portalign/config_spec.rb
92
+ - spec/portalign_spec.rb
93
+ - spec/spec_helper.rb