sfn-vault 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,153 +1,13 @@
1
1
  require 'sfn'
2
2
  require 'vault'
3
3
 
4
- require 'sfn-vault/version'
5
-
6
- # Modeled after the Assume Role callback
7
- module Sfn
8
- class Callback
9
- class VaultRead < Callback
10
-
11
- # Cache credentials for re-use to prevent re-generation of temporary
12
- # credentials on every command request.
13
- VAULT_CACHED_ITEMS = [
14
- :vault_lease_id,
15
- :vault_lease_expiration,
16
- :aws_access_key_id,
17
- :aws_secret_access_key
18
- ]
19
-
20
- # Inject credentials read from vault path
21
- # into API provider configuration
22
- def after_config(*_)
23
- # if credentials block contains vault_read_path
24
- if(enabled? && config.fetch(:credentials, :vault_read_path))
25
- load_stored_session
26
- end
27
- end
28
-
29
- # Store session credentials until lease expires
30
- def after(*_)
31
- if(enabled?)
32
- if(config.fetch(:credentials, :vault_read_path) && api.connection.aws_region)
33
- path = cache_file
34
- FileUtils.touch(path)
35
- File.chmod(0600, path)
36
- values = load_stored_values(path)
37
- VAULT_CACHED_ITEMS.map do |key|
38
- values[key] = api.connection.data[key]
39
- end
40
- File.open(path, 'w') do |file|
41
- file.puts MultiJson.dump(values)
42
- end
43
- end
44
- end
45
- end
46
-
47
- # @return [TrueClass, FalseClass]
48
- def enabled?
49
- config.fetch(:vault, :status, 'enabled').to_s == 'enabled'
50
- end
51
-
52
- # @return String path
53
- def cache_file
54
- config.fetch(:vault, :cache_file, '.sfn-vault')
55
- end
56
-
57
- # @param [FixNum] expiration
58
- # @return [TrueClass, FalseClass]
59
- # check lease is just: time.now greater than lease expires?
60
- def expired?(expiration)
61
- Time.now.to_i > expiration
62
- end
63
-
64
- def vault_addr
65
- address = config.fetch(:vault, :vault_addr, ENV['VAULT_ADDR'])
66
- if address.nil?
67
- ui.error 'Set vault_addr in .sfn or VAULT_ADDR in environment'
68
- end
69
- address
70
- end
71
-
72
- def vault_token
73
- token = config.fetch(:vault, :vault_token, ENV['VAULT_TOKEN'])
74
- if token.nil?
75
- ui.error 'Set :vault_token in .sfn or VAULT_TOKEN in environment'
76
- end
77
- token
78
- end
79
-
80
- # @return [Object] of type Vault::Secret
81
- def vault_read
82
- require 'vault'
83
- client = Vault::Client.new(address: vault_addr, token: vault_token)
84
- read_path = config.fetch(:credentials, :vault_read_path, "aws/creds/deploy") # save this value?
85
- retries = config.fetch(:vault, :retries, 5)
86
- credential = client.with_retries(Vault::HTTPError, attempts: retries) do
87
- client.logical.read(read_path)
88
- end
89
- credential
90
- end
91
-
92
- # Load stored configuration data into the api connection
93
- # or read retrieve with Vault client
94
- # @return [TrueClass, FalseClass]
95
- def load_stored_session
96
- path = cache_file
97
- FileUtils.touch(path)
98
- if(File.exist?(path))
99
- values = load_stored_values(path)
100
- VAULT_CACHED_ITEMS.each do |key|
101
- api.connection.data[key] = values[key]
102
- end
103
- if values[:vault_lease_expiration].nil?
104
- values[:vault_lease_expiration] = 0
105
- end
106
- if(expired?(values[:vault_lease_expiration]))
107
- ui.debug "No credential or lease expired"
108
- begin
109
- secret = vault_read
110
- ui.debug "vault lease: #{secret.lease_id}"
111
- # without the sleep the credentials are not ready
112
- ui.info "Sleeping 30s for first time credentials system wide activation"
113
- # this is arbitrary
114
- timeout = config.fetch(:vault, :iam_delay, 15)
115
- sleep(timeout)
116
- api.connection.data[:vault_lease_id] = secret.lease_id # maybe unused?
117
- api.connection.data[:vault_lease_expiration] = Time.now.to_i + secret.lease_duration
118
- # update keys in api connection
119
- api.connection.data[:aws_access_key_id] = secret.data[:access_key]
120
- api.connection.data[:aws_secret_access_key] = secret.data[:secret_key]
121
- rescue
122
- end
123
- end
124
- true
125
- else
126
- false
127
- end
128
- end
129
-
130
- # Load stored values
131
- #
132
- # @param path [String]
133
- # @return [Hash]
134
- def load_stored_values(path)
135
- begin
136
- if(File.exist?(path))
137
- MultiJson.load(File.read(path)).to_smash
138
- else
139
- Smash.new
140
- end
141
- rescue MultiJson::ParseError
142
- Smash.new
143
- end
144
- end
145
-
146
- # Default quiet mode
147
- def quiet
148
- true unless config[:debug]
149
- end
150
-
151
- end
152
- end
4
+ module SfnVault
5
+ autoload :Platform, 'sfn-vault/platform'
6
+ autoload :Windows, 'sfn-vault/windows'
7
+ autoload :CertificateStore, 'sfn-vault/certificate_store'
8
+ autoload :Utils, 'sfn-vault/utils'
153
9
  end
10
+
11
+ require 'sfn-vault/version'
12
+ require 'sfn-vault/callback'
13
+ require 'sfn-vault/inject'
@@ -0,0 +1,224 @@
1
+ require 'sfn-parameters'
2
+ require 'securerandom'
3
+ require 'vault'
4
+
5
+ # Modeled after the Assume Role callback
6
+ module Sfn
7
+ class Callback
8
+ class VaultRead < Callback
9
+
10
+
11
+ include SfnVault::Utils
12
+
13
+ # Cache credentials for re-use to prevent re-generation of temporary
14
+ # credentials on every command request.
15
+ VAULT_CACHED_ITEMS = [
16
+ :vault_lease_id,
17
+ :vault_lease_expiration,
18
+ :aws_access_key_id,
19
+ :aws_secret_access_key
20
+ ]
21
+
22
+ def template(*args)
23
+ # search for all parameters of type 'Vault::Generic::Secret'
24
+ # 1. use the sparkleformation instance to get at the parameter hash,
25
+ config[:parameters] ||= Smash.new
26
+ stack = args.first[:sparkle_stack]
27
+ # 2. find names for things you want,
28
+ client = vault_client
29
+ pseudo_parameters(stack).each do |param|
30
+ param_path = vault_path_name(args, param)
31
+ ui.debug "Using #{param_path} for saved parameter"
32
+ # check if already saved in vault
33
+ # Save the secret unless one already exists at the defined path
34
+ unless client.logical.read(param_path)
35
+ ui.info "Vault: No pre-existing value for parameter #{param} saving new secret"
36
+ client.logical.write(param_path, value: random_secret)
37
+ end
38
+ # Read saved secret back from Vault and update parameters config
39
+ # 3. set param into config
40
+ config[:parameters][param] = client.logical.read(param_path).data[:value]
41
+ # 4. update type in template and that should do it
42
+ stack.compile.parameters.set!(param).type 'String'
43
+ end
44
+ end
45
+
46
+ # Use SecureRandom to generate a suitable password
47
+ # Length is configurable by setting `pseudo_parameter_length` in the vault
48
+ # section of the sfn config
49
+ #
50
+ # @return [String] The generated string
51
+ def random_secret
52
+ SecureRandom.base64(config.fetch(:vault, :pseudo_parameter_length, 15))
53
+ end
54
+
55
+ # Build the path where generated secrets can be saved in Vault
56
+ # This will use the value of `:pseudo_parameter_path` from the config if set. If
57
+ # unset it will attempt to build a type of standardized path based on the
58
+ # combined value any stack 'Project' tag and Stack name.
59
+ # Project will fallback to 'SparkleFormation' if unset
60
+ #
61
+ # @param args [Array] Array of args passed to the sfn instance
62
+ # @param parameter [String] Template parameter to save value for in vault
63
+ # @return [String] String value or stack name if available or default to template name
64
+ def vault_path_name(args, parameter)
65
+ pref = config.get(:vault, :pseudo_parameter_path)
66
+ # If we have a stack name use it, otherwise try to get from env and fallback to just template name
67
+ stack = args.first[:sparkle_stack]
68
+ stack_name = args.first[:stack_name].nil? ? ENV.fetch('STACK_NAME', stack.name).to_s : args.first[:stack_name]
69
+ project = config[:options][:tags].fetch('Project', 'SparkleFormation')
70
+
71
+ # When running in a detectable CI environment assume that we have rights to save a generic secret
72
+ # but honor user preference value if set
73
+ vault_path = if ci_environment?
74
+ # write to vault at generic path
75
+ base = pref.nil? ? "secret" : pref
76
+ File.join(base, project, stack_name, parameter)
77
+ else
78
+ base = pref.nil? ? "cubbyhole" : pref
79
+ # or for local dev use cubbyhole
80
+ File.join(base, project, stack_name, parameter)
81
+ end
82
+ vault_path
83
+ end
84
+
85
+ # Lookup all pseudo parameters in the template
86
+ #
87
+ # @param stack [SparkleFormation] An instance of the stack template
88
+ # @param parameter [String] The string value of the pseudo type to lookup
89
+ # @return [Array] Array of parameter names matching the pseudo type
90
+ def pseudo_parameters(stack, parameter: 'Vault::Generic::Secret')
91
+ stack.dump.fetch('Parameters', {}).map{|k,v| k if v['Type'] == parameter}.compact
92
+ end
93
+
94
+
95
+ # Check if we are running in any detectable CI type environments
96
+ #
97
+ # @return [TrueClass, FalseClass]
98
+ def ci_environment?
99
+ # check for any ci system env variables
100
+ return true if ENV['GO_PIPELINE_NAME']
101
+ return true if ENV['CI']
102
+ false
103
+ end
104
+
105
+ def after_config(*_)
106
+ # Inject credentials read from configured vault path
107
+ # into API provider configuration
108
+ # if credentials block contains vault_read_path
109
+ # TODO: this could be done earlier if at all possible so credentials
110
+ # struct does not need the aws config
111
+ if(enabled? && config.fetch(:credentials, :vault_read_path))
112
+ load_stored_session
113
+ end
114
+ end
115
+
116
+ # Store session credentials until lease expires
117
+ def after(*_)
118
+ if(enabled?)
119
+ if(config.fetch(:credentials, :vault_read_path) && api.connection.aws_region)
120
+ path = cache_file
121
+ FileUtils.touch(path)
122
+ File.chmod(0600, path)
123
+ values = load_stored_values(path)
124
+ VAULT_CACHED_ITEMS.map do |key|
125
+ values[key] = api.connection.data[key]
126
+ end
127
+ File.open(path, 'w') do |file|
128
+ file.puts MultiJson.dump(values)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ # @return [TrueClass, FalseClass]
135
+ def enabled?
136
+ config.fetch(:vault, :status, 'enabled').to_s == 'enabled'
137
+ end
138
+
139
+ # @return [String ]path
140
+ def cache_file
141
+ config.fetch(:vault, :cache_file, '.sfn-vault')
142
+ end
143
+
144
+ # @param [FixNum] expiration
145
+ # @return [TrueClass, FalseClass]
146
+ # check lease is just: time.now greater than lease expires?
147
+ def expired?(expiration)
148
+ Time.now.to_i >= expiration
149
+ end
150
+
151
+ # @return [Object] of type Vault::Secret
152
+ def vault_read
153
+ client = vault_client
154
+ ui.debug "Have Vault client, configured with: #{client.options}"
155
+ read_path = config.fetch(:credentials, :vault_read_path, "aws/creds/deploy") # save this value?
156
+ credential = client.logical.read(read_path)
157
+ credential
158
+ end
159
+
160
+ # Load stored configuration data into the api connection
161
+ # or read retrieve with Vault client
162
+ # @return [TrueClass, FalseClass]
163
+ def load_stored_session
164
+ path = cache_file
165
+ FileUtils.touch(path)
166
+ if(File.exist?(path))
167
+ values = load_stored_values(path)
168
+ VAULT_CACHED_ITEMS.each do |key|
169
+ api.connection.data[key] = values[key]
170
+ if [:aws_access_key_id, :aws_secret_access_key].member?(key)
171
+ ui.debug "Updating environment #{key} with #{values[key]}"
172
+ # also update environment for this process
173
+ ENV[key.to_s] = values[key]
174
+ end
175
+ end
176
+ if values[:vault_lease_expiration].nil?
177
+ values[:vault_lease_expiration] = 0
178
+ end
179
+ if(expired?(values[:vault_lease_expiration]))
180
+ begin
181
+ secret = vault_read
182
+ # without the sleep the credentials are not ready
183
+ # this is arbitrary
184
+ timeout = config.fetch(:vault, :iam_delay, 30)
185
+ ui.info "Sleeping #{timeout}s for first time credentials system wide activation"
186
+ sleep(timeout)
187
+ api.connection.data[:vault_lease_id] = secret.lease_id # maybe unused?
188
+ api.connection.data[:vault_lease_expiration] = Time.now.to_i + secret.lease_duration
189
+ # update keys in api connection
190
+ api.connection.data[:aws_access_key_id] = secret.data[:access_key]
191
+ api.connection.data[:aws_secret_access_key] = secret.data[:secret_key]
192
+ rescue
193
+ end
194
+ end
195
+ true
196
+ else
197
+ false
198
+ end
199
+ end
200
+
201
+ # Load stored values
202
+ #
203
+ # @param path [String]
204
+ # @return [Hash]
205
+ def load_stored_values(path)
206
+ begin
207
+ if(File.exist?(path))
208
+ MultiJson.load(File.read(path)).to_smash
209
+ else
210
+ Smash.new
211
+ end
212
+ rescue MultiJson::ParseError
213
+ Smash.new
214
+ end
215
+ end
216
+
217
+ # Default quiet mode
218
+ def quiet
219
+ true unless config[:debug]
220
+ end
221
+
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,40 @@
1
+ require 'sfn-parameters'
2
+
3
+ # This module implements helper functions to build a valid X509 bundle on
4
+ # for SSL verification based on installed certificates.
5
+ module SfnVault
6
+ module CertificateStore
7
+
8
+ include SfnVault::Platform
9
+
10
+ # Add to an X509 store, ignoring duplicates
11
+ def self.safe_add(cert_store, cert)
12
+ cert_store.add_cert(cert)
13
+ rescue OpenSSL::X509::StoreError => e
14
+ raise unless e.message == 'cert already in hash table'
15
+ end
16
+
17
+ # Return a certificate store that can be used to validate certificates with
18
+ # the system certificate authorities. This will probably not do anything on
19
+ # OS X, which monkey patches OpenSSL in terrible ways to insert its own
20
+ # validation. On most *nix platforms, this will add the system certificates
21
+ # using OpenSSL::X509::Store#set_default_paths. On Windows, this will use
22
+ # SfnVault::Windows::RootCerts to look up the CAs trusted by the system.
23
+ #
24
+ # @return [OpenSSL::X509::Store]
25
+ #
26
+ def self.default_ssl_cert_store
27
+ cert_store = OpenSSL::X509::Store.new
28
+ cert_store.set_default_paths
29
+ # set_default_paths() doesn't do anything on Windows, so look up
30
+ # certificates using the win32 API.
31
+ if SfnVault::Platform.windows?
32
+ require 'sfn-vault/windows/root_certs'
33
+ SfnVault::Windows::RootCerts.instance.to_a.uniq.each do |cert|
34
+ safe_add(cert_store, cert)
35
+ end
36
+ end
37
+ cert_store
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ class SparkleFormation
2
+ module SparkleAttribute
3
+ module Aws
4
+
5
+ # A small helper method for adding the specific named
6
+ # parameter struct with the custom type
7
+ def _vault_parameter(vp_name)
8
+ __t_stringish(vp_name)
9
+ parameters.set!(vp_name) do
10
+ no_echo true
11
+ description "Generated secret automatically stored in Vault"
12
+ type 'Vault::Generic::Secret'
13
+ end
14
+ end
15
+ alias_method :vault_parameter!, :_vault_parameter
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ require 'sfn-vault'
2
+
3
+ module SfnVault
4
+ module Platform
5
+
6
+ # Return true if we are running on Windows.
7
+ #
8
+ # @return [Boolean]
9
+ #
10
+ def self.windows?
11
+ # Ruby only sets File::ALT_SEPARATOR on Windows, and the Ruby standard
12
+ # library uses that to test what platform it's on.
13
+ # https://github.com/rest-client/rest-client/blob/master/lib/restclient/platform.rb#L17
14
+ !!File::ALT_SEPARATOR
15
+ end
16
+ end
17
+ end