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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -4
- data/LICENSE +201 -201
- data/README.md +86 -5
- data/lib/sfn-vault.rb +9 -149
- data/lib/sfn-vault/callback.rb +224 -0
- data/lib/sfn-vault/certificate_store.rb +40 -0
- data/lib/sfn-vault/inject.rb +18 -0
- data/lib/sfn-vault/platform.rb +17 -0
- data/lib/sfn-vault/utils.rb +56 -0
- data/lib/sfn-vault/version.rb +1 -1
- data/lib/sfn-vault/windows.rb +10 -0
- data/lib/sfn-vault/windows/root_certs.rb +110 -0
- data/sfn-vault.gemspec +4 -2
- metadata +49 -13
- data/bin/coderay +0 -17
- data/bin/generate_sparkle_docs +0 -17
- data/bin/graph +0 -17
- data/bin/pry +0 -17
- data/bin/sfn +0 -17
data/lib/sfn-vault.rb
CHANGED
@@ -1,153 +1,13 @@
|
|
1
1
|
require 'sfn'
|
2
2
|
require 'vault'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|