sfn-vault 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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