sty 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: be460e9dd95543a4956eddfaa6e45b8f1d8f9f89d74560bc769e23ad8284fd71
4
+ data.tar.gz: 0f6383f3d4913d6997420b78b6a30a9c266d029a45b2539c2fee6fa0c33aba77
5
+ SHA512:
6
+ metadata.gz: 1a6cab7166156c2be6e4f4ca0e7c66fe34687ec30b738ad4b35cf229f04852540fcc29bd2488f583e09a666c3f837bad12c1eb01c7c12c065ce282dce53e4d90
7
+ data.tar.gz: 67468b0793e2073b989d91d20d4e0e81e067b3e7cc9429bed8cb67de829e2161ce9cc0c898a9c97141dee04d9200e910bda0e6f8aa0d27bb902e0d19e8ce4400
@@ -0,0 +1,2 @@
1
+ lib/*.rb
2
+ lib/**/*.rb
@@ -0,0 +1,5 @@
1
+ # Sty
2
+ [![Build Status](https://travis-ci.org/sozonnyk/sty.svg?branch=master)](https://travis-ci.org/sozonnyk/sty)
3
+
4
+ <a title="Jean-Pol GRANDMONT [CC BY 3.0 (https://creativecommons.org/licenses/by/3.0)], via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:Fourneau_St-Michel_-_Porcherie_(Forri%C3%A8res).JPG">
5
+ <img alt="Fourneau St-Michel - Porcherie (Forrières)" src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/7c/Fourneau_St-Michel_-_Porcherie_%28Forri%C3%A8res%29.JPG/1024px-Fourneau_St-Michel_-_Porcherie_%28Forri%C3%A8res%29.JPG"></a>
@@ -0,0 +1,8 @@
1
+ username:
2
+ ssh-keys: keys
3
+ accounts:
4
+ ga:
5
+ prod:
6
+ users:
7
+ key_id:
8
+ secret_key:
@@ -0,0 +1,19 @@
1
+ accounts:
2
+ ga:
3
+ prod:
4
+ users:
5
+ acc_id: 123
6
+ tools:
7
+ acc_id: 456
8
+ parent: ga/prod/users
9
+ role: abc
10
+ mgmt:
11
+ acc_id: 789
12
+ parent: ga/prod/users
13
+ role: cde
14
+ hub:
15
+ acc_id: 121
16
+ parent: ga/prod/users
17
+
18
+
19
+
data/bin/sty ADDED
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+
3
+ if [ -z "$ZSH_VERSION" ] && [ -z "$BASH_VERSION" ]; then
4
+ echo "Your shell is neither bash nor zsh. Sty will not work."
5
+ exit 1
6
+ fi
7
+
8
+ if [[ $0 == "$BASH_SOURCE" ]]; then
9
+ export STY_SOURCE_RUN=false
10
+ else
11
+ export STY_SOURCE_RUN=true
12
+ fi
13
+
14
+ result=`ruby -e "require 'sty'" $@`
15
+
16
+ marker="#EVAL#"
17
+ if [[ "$result" =~ $marker ]]; then
18
+ eval "$result"
19
+ else
20
+ printf "$result\n"
21
+ fi
@@ -0,0 +1,7 @@
1
+ ga:
2
+ prod:
3
+ mgmt:
4
+ linux: sshjumphost.internal
5
+ windows: rdp-proxy.internal
6
+ tools: ga/prod/mgmt
7
+ test: ga/prod/mgmt
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'fileutils'
3
+
4
+ task default: ['install_script']
5
+
6
+ task :install_script do
7
+
8
+ fail "Sty doesnt work on Windows" if Gem.win_platform?
9
+
10
+ # Create /usr/local/bin/sty
11
+ src = "#{Dir.pwd}/../../bin/sty"
12
+ dst = '/usr/local/bin/sty'
13
+
14
+ #TODO check if /usr/local/bin exists
15
+ begin
16
+ FileUtils.cp(src, dst)
17
+ rescue Errno::EPERM, Errno::EACCES => e
18
+ puts "No permission to create #{dst}, let's try with sudo."
19
+ `sudo cp #{src} #{dst}`
20
+ end
21
+
22
+ # Create ~/.sty
23
+ home = File.expand_path('~')
24
+ sty_home = "#{home}/.sty/"
25
+ FileUtils.mkdir_p(sty_home)
26
+ FileUtils.mkdir_p("#{sty_home}/auth-cache")
27
+
28
+ # Copy config file examples
29
+ yamls = Dir.glob("#{Dir.pwd}/../../*.yaml")
30
+ dest_yamls = Dir.glob("#{sty_home}/*.yaml").map{|f| File.basename(f)}
31
+
32
+ yamls.reject! do |y|
33
+ dest_yamls.include?(File.basename(y))
34
+ end
35
+
36
+ FileUtils.cp(yamls, sty_home)
37
+
38
+ puts "All done"
39
+ end
@@ -0,0 +1,7 @@
1
+ trap "INT" do
2
+ $stderr.reopen(IO::NULL)
3
+ $stdout.reopen(IO::NULL)
4
+ exit 1
5
+ end
6
+
7
+ require_relative 'sty/cli'
@@ -0,0 +1,18 @@
1
+ require_relative 'functions'
2
+
3
+ class Account
4
+
5
+ def self.from_fqn(fqn)
6
+
7
+ end
8
+
9
+ def self.from_path(path)
10
+
11
+ end
12
+
13
+ def initialize(path)
14
+
15
+ end
16
+
17
+
18
+ end
@@ -0,0 +1,69 @@
1
+ require_relative 'functions'
2
+
3
+ require 'aws-sdk-iam'
4
+
5
+ DEFAULT_KEY_AGE = 90
6
+ Aws.config.update(:http_proxy => ENV['https_proxy'])
7
+
8
+ class Rotator
9
+
10
+ def rotate
11
+ keys = yaml('auth-keys')
12
+ path = to_path(act_acc)
13
+ current_key = keys['accounts'].dig(*path)
14
+ puts "Current account #{white(act_acc)}"
15
+
16
+ unless current_key
17
+ puts red("You need to authenticate to userstore account to rotate keys.")
18
+ exit(1)
19
+ end
20
+ puts "Current key #{white(current_key['key_id'])}"
21
+
22
+ iam = Aws::IAM::Client.new(region: region)
23
+
24
+ account_keys = iam.list_access_keys.access_key_metadata
25
+
26
+ key_to_rotate = account_keys.select {|k| k.access_key_id == current_key['key_id']}.first
27
+
28
+ unless key_to_rotate
29
+ puts red("Key #{current_key['key_id']} for account #{ act_acc } doesn't exist in AWS.")
30
+ exit(1)
31
+ end
32
+
33
+ if account_keys.size > 1
34
+ puts "You have #{white(account_keys.size)} keys already. Remove other keys before trying to rotate."
35
+ account_keys.each do |k|
36
+ key = k.access_key_id == current_key['key_id'] ? "#{white(k.access_key_id)} <-- Keep" : "#{red(k.access_key_id)} <-- Remove"
37
+ puts key
38
+ end
39
+ exit(1)
40
+ end
41
+
42
+ key_age_days = ((Time.now - key_to_rotate.create_date)/3600/24).round
43
+ puts "The key is #{white(key_age_days)} days old."
44
+
45
+ if key_age_days < DEFAULT_KEY_AGE
46
+ puts green('All good.')
47
+ exit(0)
48
+ else
49
+ puts 'Key needs rotation.'
50
+ end
51
+
52
+ new_key = iam.create_access_key()
53
+ current_key['key_id'] = new_key.access_key.access_key_id
54
+ current_key['secret_key'] = new_key.access_key.secret_access_key
55
+
56
+ puts "New key #{white(current_key['key_id'])} was created"
57
+
58
+ dump(keys, 'auth-keys')
59
+
60
+ account_keys.each do |k|
61
+ puts "Removing old key #{white(k.access_key_id)}"
62
+ iam.delete_access_key(access_key_id: k.access_key_id)
63
+ end
64
+
65
+ puts green('Key was rotated successfully.')
66
+ end
67
+ end
68
+
69
+ Rotator.new.rotate
@@ -0,0 +1,188 @@
1
+ require_relative 'functions'
2
+ require_relative 'dig'
3
+
4
+ SESSION_DURATION_SECONDS = 43_200
5
+ DEFAULT_ROLE_NAME = 'ReadOnlyRole'
6
+ DEFAULT_REGION = 'ap-southeast-2'
7
+
8
+ class Auth
9
+
10
+ def logout
11
+ current = ENV['AWS_ACTIVE_ACCOUNT']
12
+ identity = ENV['AWS_ACTIVE_IDENTITY']
13
+ STDERR.puts "Logging off from: #{white(current)}"
14
+ if current
15
+ cache = cache_file(to_path(current),identity)
16
+ begin
17
+ File.delete(cache)
18
+ rescue Errno::ENOENT => e
19
+ end
20
+ end
21
+ puts "#EVAL#"
22
+ puts "unset AWS_ACTIVE_ACCOUNT AWS_SESSION_EXPIRY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_ACTIVE_IDENTITY"
23
+ end
24
+
25
+ def deep_merge(h1,h2)
26
+ h1.merge(h2){|k,v1,v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? deep_merge(v1,v2) : v2}
27
+ end
28
+
29
+ def initialize
30
+ #aws-sdk is slow, so load it only when needed
31
+ require 'aws-sdk-core'
32
+ Aws.config.update(:http_proxy => ENV['https_proxy'])
33
+ @config = deep_merge(yaml('auth'),yaml('auth-keys'))
34
+ end
35
+
36
+ def check_proxy
37
+ unless ENV.find { |k,v| k =~ /HTTPS_PROXY/i }
38
+ STDERR.puts red("WARNING! \"https_proxy\" env variable is not set.")
39
+ end
40
+ end
41
+
42
+ def user
43
+ @config['username']
44
+ end
45
+
46
+ def account(path)
47
+ acc = @config['accounts'].dig(*path)
48
+ unless acc
49
+ STDERR.puts red("ERROR! Account #{to_fqn(path)} not found in config")
50
+ exit 1
51
+ end
52
+
53
+ acc['path'] = path
54
+ acc
55
+ end
56
+
57
+ def parent(acc)
58
+ acc['parent']
59
+ end
60
+
61
+ def cached_creds(path, identity)
62
+ acc_fqn = to_fqn(path)
63
+ begin
64
+ cached_creds = Psych.load_file(cache_file(path, identity))
65
+ raise(RuntimeError) unless cached_creds
66
+ rescue Errno::ENOENT, RuntimeError
67
+ STDERR.puts "No cached creds for #{acc_fqn}"
68
+ return nil
69
+ end
70
+
71
+ remained_minutes = ((cached_creds['expiration'] - Time.now) / 60).to_i
72
+
73
+ if remained_minutes > 0
74
+ STDERR.puts "Loaded cached creds for #{acc_fqn}"
75
+ STDERR.puts "Credentials will stay active for the next #{remained_minutes} min"
76
+ return {creds: Aws::Credentials.new(cached_creds['access_key_id'],
77
+ cached_creds['secret_access_key'],
78
+ cached_creds['session_token']),
79
+ expiry: cached_creds['expiration']}
80
+ else
81
+ STDERR.puts "Cached creds for #{acc_fqn} expired"
82
+ end
83
+ end
84
+
85
+ def save_creds(acc, creds, expiration, identity)
86
+ creds_hash = {'access_key_id' => creds.access_key_id,
87
+ 'secret_access_key' => creds.secret_access_key,
88
+ 'session_token' => creds.session_token,
89
+ 'expiration' => expiration
90
+ }
91
+ File.open(cache_file(acc['path'], identity), 'w') do |file|
92
+ file.write(Psych.dump(creds_hash))
93
+ end
94
+ end
95
+
96
+ def login_bare(acc)
97
+
98
+ path = acc['path']
99
+ acc_fqn = to_fqn(path)
100
+
101
+ cached = cached_creds(path, user)
102
+ return { creds: cached[:creds], expiry: cached[:expiry], identity: user } if cached
103
+
104
+ STDERR.puts "Enter MFA for #{acc_fqn}"
105
+ token = STDIN.gets.chomp
106
+
107
+ mfa = "arn:aws:iam::#{acc['acc_id']}:mfa/#{user}"
108
+
109
+ bare_creds = Aws::Credentials.new(acc['key_id'], acc['secret_key'])
110
+
111
+ sts = Aws::STS::Client.new(credentials: bare_creds, region: region)
112
+
113
+ begin
114
+ session = sts.get_session_token(duration_seconds: SESSION_DURATION_SECONDS,
115
+ serial_number: mfa,
116
+ token_code: token)
117
+
118
+ creds = Aws::Credentials.new(session.credentials.access_key_id,
119
+ session.credentials.secret_access_key,
120
+ session.credentials.session_token)
121
+ rescue Exception => e
122
+ STDERR.puts red("ERROR! Unable to obtain credentials for #{acc_fqn}")
123
+ STDERR.puts white(e.message)
124
+ exit 1
125
+ end
126
+
127
+ STDERR.puts green("Successfully obtained creds for #{acc_fqn}")
128
+
129
+ save_creds(acc, creds, session.credentials.expiration, user)
130
+
131
+ {creds: creds, expiry: session.credentials.expiration, identity: user}
132
+ end
133
+
134
+ def login_role(acc, role)
135
+ path = acc['path']
136
+ active_role = role || acc['role'] || DEFAULT_ROLE_NAME
137
+ role_arn = "arn:aws:iam::#{acc['acc_id']}:role/#{active_role}"
138
+
139
+ cached = cached_creds(path, active_role)
140
+ return { creds: cached[:creds], expiry: cached[:expiry], identity: active_role } if cached
141
+
142
+ parent_path = to_path(parent(acc))
143
+ parent_acc = account(parent_path)
144
+ parent_creds = login_bare(parent_acc)[:creds]
145
+ sts = Aws::STS::Client.new(
146
+ credentials: parent_creds,
147
+ endpoint: 'https://sts.ap-southeast-2.amazonaws.com',
148
+ region: region
149
+ )
150
+ begin
151
+ creds = sts.assume_role(role_arn: role_arn,
152
+ role_session_name: "#{user}-#{parent_path.join('-')}",
153
+ duration_seconds: 3600).credentials
154
+ rescue Exception => e
155
+ STDERR.puts red("ERROR! Unable to obtain credentials for #{to_fqn(path)}")
156
+ STDERR.puts white(e.message)
157
+ exit 1
158
+ end
159
+ STDERR.puts green("Successfully obtained creds for #{to_fqn(path)}")
160
+ save_creds(acc, creds, creds.expiration, active_role)
161
+
162
+ {creds: creds, expiry: creds.expiration, identity: active_role}
163
+ end
164
+
165
+ def print_creds(acc, creds)
166
+ puts "#EVAL#"
167
+ puts "export AWS_ACTIVE_ACCOUNT=#{to_fqn(acc['path'])}"
168
+ puts "export AWS_ACTIVE_IDENTITY=#{creds[:identity]}"
169
+ puts "export AWS_SESSION_EXPIRY=\"#{creds[:expiry]}\""
170
+ puts "export AWS_ACCESS_KEY_ID=#{creds[:creds].access_key_id}"
171
+ puts "export AWS_SECRET_ACCESS_KEY=#{creds[:creds].secret_access_key}"
172
+ puts "export AWS_SESSION_TOKEN=#{creds[:creds].session_token}"
173
+ end
174
+
175
+ def login(fqn, role = nil)
176
+ check_proxy
177
+ acc = account(to_path(fqn))
178
+
179
+ if parent(acc)
180
+ creds = login_role(acc, role)
181
+ else
182
+ creds = login_bare(acc)
183
+ end
184
+
185
+ print_creds(acc, creds) if creds
186
+ end
187
+
188
+ end
@@ -0,0 +1,71 @@
1
+ require 'thor'
2
+ require_relative 'info'
3
+ require_relative 'auth'
4
+ require_relative 'console'
5
+ require_relative 'proxy'
6
+ require_relative 'ssh'
7
+
8
+ class Cli < Thor
9
+ map auth: :login, acct: :account, px: :proxy
10
+
11
+ def self.basename
12
+ 'sty'
13
+ end
14
+
15
+ desc "ssh [OPTIONS] <SEARCH_TERM...>","Creates ssh connection to desired ec2 instance through existing jumphost. SEARCH_TERM to search in EC2 instance ID, name or IP address"
16
+ method_option :no_jumphost, type: :boolean, default: false, aliases: "-n", desc: "Connect directly without jumphost"
17
+ method_option :select_jumphost, type: :boolean, aliases: "-s", desc: "Select jumphost instance"
18
+ method_option :use_key, type: :boolean, aliases: "-k", desc: "Use private key auth for target instance. Keys are searched recursively in ~/.sty/keys"
19
+ def ssh(*search_term)
20
+ Ssh.new.connect(search_term, options[:no_jumphost], options[:select_jumphost], options[:use_key])
21
+ end
22
+
23
+ desc "console", "Opens AWS console in browser for currently authenticated session"
24
+ method_option :browser, type: :string, aliases: "-b", enum: Console::BROWSERS, desc: "Use specific browser"
25
+ method_option :incognito, type: :boolean, aliases: "-i", desc: "Create new incognito window"
26
+ method_option :logout, type: :boolean, aliases: "-l", dssc: "Logout from current session"
27
+ def console
28
+ Console.new.action(options[:browser], options[:incognito], options[:logout])
29
+ end
30
+
31
+ desc "login ACCOUNT_PATH", "Authenticate to the account"
32
+ method_option :role, aliases: "-r", dssc: "Override role name"
33
+ def login(path)
34
+ source_run(__method__)
35
+ Auth.new.login(path, options[:browser])
36
+ end
37
+
38
+ desc "logout", "Forget current credentials and clear cache"
39
+ def logout
40
+ source_run(__method__)
41
+ Auth.new.logout
42
+ end
43
+
44
+ desc "info", "Get current session information"
45
+ def info
46
+ Info.new.session_info
47
+ end
48
+
49
+ desc "account ACCOUNT_ID", "Find account information"
50
+ def account(path)
51
+ Info.new.account_info(path)
52
+ end
53
+
54
+ desc "proxy [PROXY_ID]", "Switch session proxy (use 'off' to disable)"
55
+ def proxy(px = nil)
56
+ source_run(__method__)
57
+ Proxy.new.action(px)
58
+ end
59
+
60
+ no_tasks do
61
+ def source_run(method)
62
+ unless ENV['STY_SOURCE_RUN'] == 'true'
63
+ puts "When using '#{method.to_s}' command, you must source it, i.e.: '. sty #{method.to_s}'"
64
+ exit 128
65
+ end
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ Cli.start(ARGV)