legion-crypt 1.5.12 → 1.5.37
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +146 -1
- data/README.md +68 -14
- data/legion-crypt.gemspec +2 -1
- data/lib/legion/crypt/cert_rotation.rb +83 -65
- data/lib/legion/crypt/cipher.rb +5 -15
- data/lib/legion/crypt/cluster_secret.rb +71 -19
- data/lib/legion/crypt/erasure.rb +3 -10
- data/lib/legion/crypt/jwks_client.rb +43 -31
- data/lib/legion/crypt/jwt.rb +8 -7
- data/lib/legion/crypt/kerberos_auth.rb +18 -8
- data/lib/legion/crypt/ldap_auth.rb +14 -13
- data/lib/legion/crypt/lease_manager.rb +485 -335
- data/lib/legion/crypt/mtls.rb +3 -6
- data/lib/legion/crypt/partition_keys.rb +2 -0
- data/lib/legion/crypt/settings.rb +2 -1
- data/lib/legion/crypt/spiffe/workload_api_client.rb +83 -71
- data/lib/legion/crypt/token_renewer.rb +212 -82
- data/lib/legion/crypt/vault.rb +113 -83
- data/lib/legion/crypt/vault_client.rb +355 -0
- data/lib/legion/crypt/vault_cluster.rb +161 -39
- data/lib/legion/crypt/vault_entity.rb +21 -34
- data/lib/legion/crypt/vault_jwt_auth.rb +9 -20
- data/lib/legion/crypt/vault_kerberos_auth.rb +11 -14
- data/lib/legion/crypt/vault_operation.rb +27 -0
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +116 -63
- metadata +23 -9
- data/lib/legion/logging/helper.rb +0 -101
- data/lib/legion/logging.rb +0 -58
data/lib/legion/crypt/vault.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
-
require '
|
|
5
|
-
require '
|
|
4
|
+
require 'legion/crypt/vault_operation'
|
|
5
|
+
require 'legion/crypt/vault_client'
|
|
6
6
|
|
|
7
7
|
module Legion
|
|
8
8
|
module Crypt
|
|
@@ -16,34 +16,30 @@ module Legion
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def connect_vault
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return nil if Legion::Settings[:crypt][:vault][:token].nil?
|
|
28
|
-
|
|
29
|
-
::Vault.token = Legion::Settings[:crypt][:vault][:token]
|
|
30
|
-
::Vault.namespace = namespace if namespace
|
|
19
|
+
return unless start_session_tracking
|
|
20
|
+
|
|
21
|
+
configuration = Legion::Settings[:crypt][:vault]
|
|
22
|
+
configuration[:token] = ENV['VAULT_DEV_ROOT_TOKEN_ID'] if ENV.key? 'VAULT_DEV_ROOT_TOKEN_ID'
|
|
23
|
+
return nil if configuration[:token].nil?
|
|
24
|
+
|
|
25
|
+
client = vault_client
|
|
26
|
+
log.info "Vault connection requested address=#{client.address} namespace=#{client.namespace || 'none'} ssl_verify=#{client.ssl_verify}"
|
|
31
27
|
if vault_healthy?
|
|
32
|
-
|
|
33
|
-
log.info "Vault connected at #{
|
|
28
|
+
configuration[:connected] = true
|
|
29
|
+
log.info "Vault connected at #{client.address} (namespace=#{client.namespace || 'none'})"
|
|
34
30
|
end
|
|
35
31
|
rescue StandardError => e
|
|
36
|
-
|
|
32
|
+
handle_exception(e, level: :error, operation: 'crypt.vault.connect_vault')
|
|
37
33
|
Legion::Settings[:crypt][:vault][:connected] = false
|
|
38
34
|
false
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
def vault_healthy?
|
|
42
|
-
|
|
43
|
-
rescue ::Vault::HTTPError => e
|
|
38
|
+
vault_client.sys.health_status.initialized?
|
|
39
|
+
rescue Legion::Crypt::Vault::HTTPError => e
|
|
44
40
|
# 429 = standby, 472 = DR secondary, 473 = performance standby
|
|
45
41
|
# All indicate an initialized, healthy Vault — just not the active node.
|
|
46
|
-
return true if
|
|
42
|
+
return true if [429, 472, 473].include?(e.code)
|
|
47
43
|
|
|
48
44
|
raise
|
|
49
45
|
end
|
|
@@ -51,7 +47,7 @@ module Legion
|
|
|
51
47
|
def read(path, type = nil, cluster_name: nil)
|
|
52
48
|
full_path = type.nil? || type.empty? ? path : "#{type}/#{path}"
|
|
53
49
|
log_read_context(full_path, cluster_name: cluster_name)
|
|
54
|
-
lease = logical_client(cluster_name: cluster_name).read(full_path)
|
|
50
|
+
lease = logical_client(cluster_name: cluster_name).read(path: full_path)
|
|
55
51
|
if lease.nil?
|
|
56
52
|
log_vault_debug("Vault read: #{full_path} returned nil")
|
|
57
53
|
return nil
|
|
@@ -68,7 +64,7 @@ module Legion
|
|
|
68
64
|
|
|
69
65
|
def get(path, cluster_name: nil)
|
|
70
66
|
log.debug "Vault kv get: path=#{path}"
|
|
71
|
-
result = kv_client(cluster_name: cluster_name).read(path)
|
|
67
|
+
result = kv_client(cluster_name: cluster_name).read(path: path)
|
|
72
68
|
if result.nil?
|
|
73
69
|
log.debug "Vault kv get: #{path} returned nil"
|
|
74
70
|
return nil
|
|
@@ -83,7 +79,7 @@ module Legion
|
|
|
83
79
|
|
|
84
80
|
def write(path, cluster_name: nil, **hash)
|
|
85
81
|
log.info "Vault kv write requested path=#{path}"
|
|
86
|
-
kv_client(cluster_name: cluster_name).write(path,
|
|
82
|
+
kv_client(cluster_name: cluster_name).write(path: path, data: hash)
|
|
87
83
|
log.info "Vault kv write complete path=#{path}"
|
|
88
84
|
rescue StandardError => e
|
|
89
85
|
handle_exception(e, level: :error, operation: 'crypt.vault.write', path: path)
|
|
@@ -91,7 +87,7 @@ module Legion
|
|
|
91
87
|
end
|
|
92
88
|
|
|
93
89
|
def delete(path, cluster_name: nil)
|
|
94
|
-
logical_client(cluster_name: cluster_name).delete(path)
|
|
90
|
+
logical_client(cluster_name: cluster_name).delete(path: path)
|
|
95
91
|
log.info "Vault delete complete path=#{path}"
|
|
96
92
|
{ success: true, path: path }
|
|
97
93
|
rescue StandardError => e
|
|
@@ -100,89 +96,149 @@ module Legion
|
|
|
100
96
|
end
|
|
101
97
|
|
|
102
98
|
def exist?(path, cluster_name: nil)
|
|
103
|
-
!kv_client(cluster_name: cluster_name).read_metadata(path).nil?
|
|
99
|
+
!kv_client(cluster_name: cluster_name).read_metadata(path: path).nil?
|
|
104
100
|
end
|
|
105
101
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
def start_session_tracking
|
|
103
|
+
session_mutex.synchronize do
|
|
104
|
+
return false if crypt_stopping?
|
|
105
|
+
|
|
106
|
+
@sessions = []
|
|
107
|
+
@sessions_stopped = false
|
|
108
|
+
@session_generation = (@session_generation || 0) + 1
|
|
109
|
+
end
|
|
110
|
+
true
|
|
109
111
|
end
|
|
110
112
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
+
def add_session(path:)
|
|
114
|
+
session_mutex.synchronize do
|
|
115
|
+
return if @sessions_stopped || crypt_stopping?
|
|
116
|
+
|
|
117
|
+
(@sessions ||= []).push(path)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
113
120
|
|
|
121
|
+
def close_sessions(deadline: nil)
|
|
122
|
+
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + VaultOperation::SHUTDOWN_TIMEOUT
|
|
123
|
+
pending = session_mutex.synchronize do
|
|
124
|
+
@sessions_stopped = true
|
|
125
|
+
@session_generation = (@session_generation || 0) + 1
|
|
126
|
+
detached = @sessions || []
|
|
127
|
+
@sessions = []
|
|
128
|
+
detached
|
|
129
|
+
end
|
|
114
130
|
log.info 'Closing all Legion::Crypt vault sessions'
|
|
131
|
+
closed = []
|
|
132
|
+
pending.each do |session|
|
|
133
|
+
break if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) >= deadline
|
|
115
134
|
|
|
116
|
-
|
|
117
|
-
|
|
135
|
+
VaultOperation.with_timeout(deadline: deadline) { close_session(session: session) }
|
|
136
|
+
closed << session
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
handle_exception(e, level: :error, operation: 'crypt.vault.close_session')
|
|
118
139
|
end
|
|
140
|
+
closed
|
|
119
141
|
end
|
|
120
142
|
|
|
121
143
|
def shutdown_renewer
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
renewer = session_mutex.synchronize do
|
|
145
|
+
@sessions_stopped = true
|
|
146
|
+
@session_generation = (@session_generation || 0) + 1
|
|
147
|
+
detached = @renewer
|
|
148
|
+
@renewer = nil
|
|
149
|
+
detached
|
|
150
|
+
end
|
|
151
|
+
return if renewer.nil?
|
|
124
152
|
|
|
125
153
|
log.info 'Shutting down Legion::Crypt::Vault::Renewer'
|
|
126
|
-
|
|
154
|
+
renewer.cancel
|
|
127
155
|
end
|
|
128
156
|
|
|
129
157
|
def close_session(session:)
|
|
130
|
-
|
|
158
|
+
vault_client.sys.revoke(lease_id: session)
|
|
131
159
|
end
|
|
132
160
|
|
|
133
161
|
def renew_session(session:)
|
|
134
|
-
|
|
162
|
+
vault_client.sys.renew(lease_id: session)
|
|
135
163
|
end
|
|
136
164
|
|
|
137
165
|
def renew_sessions(**_opts)
|
|
166
|
+
generation, pending = session_mutex.synchronize do
|
|
167
|
+
return [] if @sessions_stopped || crypt_stopping?
|
|
168
|
+
|
|
169
|
+
[@session_generation, (@sessions || []).dup]
|
|
170
|
+
end
|
|
138
171
|
log.debug 'Vault renewal cycle start'
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
172
|
+
return renew_cluster_tokens(generation: generation) if respond_to?(:connected_clusters) && connected_clusters.any?
|
|
173
|
+
|
|
174
|
+
renewed = []
|
|
175
|
+
pending.each do |session|
|
|
176
|
+
break unless session_maintenance_active?(generation: generation)
|
|
177
|
+
|
|
178
|
+
VaultOperation.with_timeout { renew_session(session: session) }
|
|
179
|
+
renewed << session
|
|
180
|
+
rescue StandardError => e
|
|
181
|
+
handle_exception(e, level: :error, operation: 'crypt.vault.renew_session')
|
|
182
|
+
end
|
|
146
183
|
log.debug 'Vault renewal cycle complete'
|
|
147
|
-
|
|
184
|
+
renewed
|
|
148
185
|
rescue StandardError => e
|
|
149
186
|
handle_exception(e, level: :error, operation: 'crypt.vault.renew_sessions')
|
|
150
187
|
raise
|
|
151
188
|
end
|
|
152
189
|
|
|
153
|
-
def renew_cluster_tokens
|
|
190
|
+
def renew_cluster_tokens(generation: session_mutex.synchronize { @session_generation })
|
|
191
|
+
renewed = []
|
|
154
192
|
connected_clusters.each_key do |name|
|
|
155
|
-
|
|
156
|
-
|
|
193
|
+
break unless session_maintenance_active?(generation: generation)
|
|
194
|
+
|
|
195
|
+
VaultOperation.with_timeout { vault_client(name).auth_token.renew_self }
|
|
196
|
+
renewed << name
|
|
157
197
|
log.info "Vault token renewed for cluster #{name}"
|
|
158
198
|
rescue StandardError => e
|
|
159
199
|
log_vault_error(name, e)
|
|
160
200
|
end
|
|
201
|
+
renewed
|
|
161
202
|
end
|
|
162
203
|
|
|
163
204
|
def vault_exists?(name)
|
|
164
|
-
|
|
205
|
+
vault_client.sys.mounts.key?(name.to_sym)
|
|
165
206
|
end
|
|
166
207
|
|
|
167
208
|
private
|
|
168
209
|
|
|
210
|
+
def session_mutex
|
|
211
|
+
@session_mutex ||= Mutex.new
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def crypt_stopping?
|
|
215
|
+
Legion::Crypt.respond_to?(:stopping) && Legion::Crypt.stopping
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def session_maintenance_active?(generation:, **)
|
|
219
|
+
session_mutex.synchronize do
|
|
220
|
+
!@sessions_stopped && !crypt_stopping? && @session_generation == generation
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
169
224
|
def kv_client(cluster_name: nil)
|
|
170
|
-
|
|
171
|
-
|
|
225
|
+
mount = Legion::Settings[:crypt][:vault][:kv_path]
|
|
226
|
+
if respond_to?(:connected_clusters) && (clusters.any? || connected_clusters.any?)
|
|
227
|
+
connected_vault_client(cluster_name).kv(mount: mount)
|
|
172
228
|
else
|
|
173
229
|
raise ArgumentError, "Vault cluster not connected: #{cluster_name}" if cluster_name
|
|
174
230
|
|
|
175
|
-
|
|
231
|
+
vault_client.kv(mount: mount)
|
|
176
232
|
end
|
|
177
233
|
end
|
|
178
234
|
|
|
179
235
|
def logical_client(cluster_name: nil)
|
|
180
|
-
if respond_to?(:connected_clusters) && connected_clusters.any?
|
|
236
|
+
if respond_to?(:connected_clusters) && (clusters.any? || connected_clusters.any?)
|
|
181
237
|
connected_vault_client(cluster_name).logical
|
|
182
238
|
else
|
|
183
239
|
raise ArgumentError, "Vault cluster not connected: #{cluster_name}" if cluster_name
|
|
184
240
|
|
|
185
|
-
|
|
241
|
+
vault_client.logical
|
|
186
242
|
end
|
|
187
243
|
end
|
|
188
244
|
|
|
@@ -191,16 +247,16 @@ module Legion
|
|
|
191
247
|
client = connected_vault_client(cluster_name)
|
|
192
248
|
client.respond_to?(:namespace) ? client.namespace : 'n/a'
|
|
193
249
|
else
|
|
194
|
-
|
|
250
|
+
vault_client.namespace
|
|
195
251
|
end
|
|
196
252
|
log.debug "Vault read: path=#{full_path}, namespace=#{namespace}"
|
|
197
253
|
end
|
|
198
254
|
|
|
199
255
|
def connected_vault_client(cluster_name = nil)
|
|
200
256
|
selected_cluster = selected_connected_cluster_name(cluster_name)
|
|
201
|
-
raise ArgumentError, "Vault cluster not connected: #{cluster_name}" if selected_cluster.nil?
|
|
257
|
+
raise ArgumentError, "Vault cluster not connected: #{cluster_name || default_cluster_name}" if selected_cluster.nil?
|
|
202
258
|
|
|
203
|
-
|
|
259
|
+
vault_client(selected_cluster)
|
|
204
260
|
end
|
|
205
261
|
|
|
206
262
|
def unwrap_kv_v2(data, full_path)
|
|
@@ -210,32 +266,6 @@ module Legion
|
|
|
210
266
|
data[:data]
|
|
211
267
|
end
|
|
212
268
|
|
|
213
|
-
def resolve_ssl_verify(tls_config)
|
|
214
|
-
return true if tls_config.nil?
|
|
215
|
-
|
|
216
|
-
verify = tls_config[:verify]&.to_s
|
|
217
|
-
verify != 'none'
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def resolve_vault_address(vault_settings)
|
|
221
|
-
protocol = vault_settings[:protocol] || 'http'
|
|
222
|
-
address = vault_settings[:address] || 'localhost'
|
|
223
|
-
port = vault_settings[:port] || 8200
|
|
224
|
-
|
|
225
|
-
if address.match?(%r{\Ahttps?://})
|
|
226
|
-
uri = URI.parse(address)
|
|
227
|
-
protocol = uri.scheme
|
|
228
|
-
address = uri.host
|
|
229
|
-
port = uri.port if vault_settings[:port].nil?
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
"#{protocol}://#{address}:#{port}"
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def log_vault_connection_error(error)
|
|
236
|
-
handle_exception(error, level: :error, operation: 'crypt.vault.connect_vault', address: ::Vault.address)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
269
|
def log_vault_debug(message)
|
|
240
270
|
log.debug(message)
|
|
241
271
|
end
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'legion/json'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Crypt
|
|
10
|
+
module Vault
|
|
11
|
+
class HTTPError < StandardError
|
|
12
|
+
attr_reader :code, :errors, :address
|
|
13
|
+
|
|
14
|
+
def initialize(code:, errors: [], address: nil, **)
|
|
15
|
+
@code = Integer(code)
|
|
16
|
+
@errors = Array(errors)
|
|
17
|
+
@address = address
|
|
18
|
+
super("Vault HTTP #{@code}#{": #{@errors.join('; ')}" unless @errors.empty?}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class HTTPClientError < HTTPError; end
|
|
23
|
+
class HTTPServerError < HTTPError; end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class VaultClient
|
|
27
|
+
CONNECTION_ATTRIBUTES = %i[address ssl_verify ca cert key timeout].freeze
|
|
28
|
+
|
|
29
|
+
attr_reader(*CONNECTION_ATTRIBUTES, :logical, :sys, :auth_token, :token)
|
|
30
|
+
attr_accessor :namespace
|
|
31
|
+
|
|
32
|
+
def initialize(address:, token: nil, namespace: nil, tls: {}, timeout: 5, **)
|
|
33
|
+
@address = validate_address(address: address)
|
|
34
|
+
@timeout = validate_timeout(timeout: timeout)
|
|
35
|
+
@token = token
|
|
36
|
+
@namespace = namespace
|
|
37
|
+
@ssl_verify = tls[:verify] != false && tls[:verify].to_s != 'none'
|
|
38
|
+
@ca = tls[:ca]
|
|
39
|
+
@cert = tls[:cert]
|
|
40
|
+
@key = tls[:key]
|
|
41
|
+
@mutex = Mutex.new
|
|
42
|
+
@retired = false
|
|
43
|
+
@logical = Logical.new(client: self)
|
|
44
|
+
@sys = Sys.new(client: self)
|
|
45
|
+
@auth_token = AuthToken.new(client: self)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
CONNECTION_ATTRIBUTES.each do |attribute|
|
|
49
|
+
define_method(:"#{attribute}=") do |value|
|
|
50
|
+
value = validate_address(address: value) if attribute == :address
|
|
51
|
+
value = validate_timeout(timeout: value) if attribute == :timeout
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
return value if instance_variable_get(:"@#{attribute}") == value
|
|
54
|
+
|
|
55
|
+
previous = @connection
|
|
56
|
+
@connection = nil
|
|
57
|
+
instance_variable_set(:"@#{attribute}", value)
|
|
58
|
+
previous&.close
|
|
59
|
+
end
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def token=(value)
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
raise IOError, 'Vault client has been shut down' if @retired
|
|
67
|
+
|
|
68
|
+
@token = value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def kv(mount:, **)
|
|
73
|
+
KV.new(client: self, mount: mount)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def put(path:, body:, headers: {}, **)
|
|
77
|
+
request(method: :put, path: path, body: body, headers: headers) || {}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def request(method:, path:, body: nil, headers: {}, nil_on_not_found: false, **)
|
|
81
|
+
connection, request_headers, address = @mutex.synchronize do
|
|
82
|
+
raise IOError, 'Vault client has been shut down' if @retired
|
|
83
|
+
|
|
84
|
+
[@connection ||= build_connection, vault_headers(headers: headers), @address]
|
|
85
|
+
end
|
|
86
|
+
payload = body.is_a?(String) || body.nil? ? body : Legion::JSON.dump(body)
|
|
87
|
+
response = connection.run_request(method, request_path(path: path), payload, request_headers)
|
|
88
|
+
return nil if nil_on_not_found && response.status == 404
|
|
89
|
+
|
|
90
|
+
decode_response(response: response, address: address)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def shutdown
|
|
94
|
+
@mutex.synchronize do
|
|
95
|
+
@retired = true
|
|
96
|
+
previous = @connection
|
|
97
|
+
@connection = nil
|
|
98
|
+
previous&.close
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def inspect
|
|
104
|
+
"#<#{self.class}>"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class Logical
|
|
108
|
+
def initialize(client:, **)
|
|
109
|
+
@client = client
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def read(path:, **)
|
|
113
|
+
Secret.decode(data: @client.request(method: :get, path: path, nil_on_not_found: true))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def write(path:, data:, **)
|
|
117
|
+
Secret.decode(data: @client.put(path: path, body: data))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def delete(path:, **)
|
|
121
|
+
@client.request(method: :delete, path: path)
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class Sys
|
|
127
|
+
class HealthStatus
|
|
128
|
+
def initialize(data:, **)
|
|
129
|
+
@initialized = data[:initialized]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def initialized?
|
|
133
|
+
@initialized == true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def initialize(client:, **)
|
|
138
|
+
@client = client
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def health_status
|
|
142
|
+
HealthStatus.new(data: @client.request(method: :get, path: 'sys/health'))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def mounts
|
|
146
|
+
@client.request(method: :get, path: 'sys/mounts').fetch(:data)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def renew(lease_id:, **)
|
|
150
|
+
Secret.decode(data: @client.put(path: 'sys/leases/renew', body: { lease_id: lease_id }))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def revoke(lease_id:, **)
|
|
154
|
+
@client.put(path: 'sys/leases/revoke', body: { lease_id: lease_id })
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class AuthToken
|
|
160
|
+
def initialize(client:, **)
|
|
161
|
+
@client = client
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def renew_self
|
|
165
|
+
Secret.decode(data: @client.put(path: 'auth/token/renew-self', body: {}))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def revoke_self
|
|
169
|
+
@client.put(path: 'auth/token/revoke-self', body: {})
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
class KV
|
|
175
|
+
def initialize(client:, mount:, **)
|
|
176
|
+
@client = client
|
|
177
|
+
mount = mount.to_s
|
|
178
|
+
first = mount.index(%r{[^/]})
|
|
179
|
+
@mount = first ? mount[first..mount.rindex(%r{[^/]})] : ''
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def read(path:, **)
|
|
183
|
+
response = @client.request(method: :get, path: "#{@mount}/data/#{path}", nil_on_not_found: true)
|
|
184
|
+
return nil if response.nil?
|
|
185
|
+
|
|
186
|
+
Secret.decode(data: response.merge(data: response.dig(:data, :data)))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def write(path:, data:, **)
|
|
190
|
+
Secret.decode(data: @client.put(path: "#{@mount}/data/#{path}", body: { data: data }))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def read_metadata(path:, **)
|
|
194
|
+
Secret.decode(data: @client.request(method: :get, path: "#{@mount}/metadata/#{path}", nil_on_not_found: true))
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
class Secret
|
|
199
|
+
attr_reader :data, :auth, :lease_id, :lease_duration
|
|
200
|
+
|
|
201
|
+
def self.decode(data:, **)
|
|
202
|
+
return nil if data.nil? || data.empty?
|
|
203
|
+
|
|
204
|
+
new(data: data)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def initialize(data:, **)
|
|
208
|
+
@data = data[:data]
|
|
209
|
+
@auth = Auth.new(data: data[:auth]) unless data[:auth].nil?
|
|
210
|
+
@lease_id = data[:lease_id]
|
|
211
|
+
@lease_duration = data[:lease_duration]
|
|
212
|
+
@renewable = data[:renewable]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def renewable?
|
|
216
|
+
@renewable == true
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def inspect
|
|
220
|
+
"#<#{self.class}>"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
class Auth
|
|
225
|
+
attr_reader :client_token, :lease_duration, :policies, :metadata
|
|
226
|
+
|
|
227
|
+
def initialize(data:, **)
|
|
228
|
+
@client_token = data[:client_token]
|
|
229
|
+
@lease_duration = data[:lease_duration]
|
|
230
|
+
@policies = data[:policies]
|
|
231
|
+
@metadata = data[:metadata]
|
|
232
|
+
@renewable = data[:renewable]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def renewable?
|
|
236
|
+
@renewable == true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def inspect
|
|
240
|
+
"#<#{self.class}>"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def validate_address(address:, **)
|
|
247
|
+
uri = URI.parse(address.to_s)
|
|
248
|
+
unless uri.is_a?(URI::HTTP) && uri.host && !uri.userinfo && !uri.query && !uri.fragment
|
|
249
|
+
raise ArgumentError, 'Vault address must be an HTTP(S) URL without credentials, query, or fragment'
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
uri.to_s.freeze
|
|
253
|
+
rescue URI::InvalidURIError
|
|
254
|
+
raise ArgumentError, 'Vault address must be a valid HTTP(S) URL'
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def validate_timeout(timeout:, **)
|
|
258
|
+
seconds = Float(timeout)
|
|
259
|
+
raise ArgumentError, 'Vault timeout must be positive and finite' unless seconds.positive? && seconds.finite?
|
|
260
|
+
|
|
261
|
+
seconds
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def vault_headers(headers:, **)
|
|
265
|
+
result = Faraday::Utils::Headers.new('Accept' => 'application/json', 'Content-Type' => 'application/json')
|
|
266
|
+
result.update(headers)
|
|
267
|
+
result.delete('X-Vault-Token')
|
|
268
|
+
result.delete('X-Vault-Namespace')
|
|
269
|
+
result['X-Vault-Token'] = @token unless @token.nil?
|
|
270
|
+
result['X-Vault-Namespace'] = @namespace unless @namespace.nil?
|
|
271
|
+
result
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def request_path(path:, **)
|
|
275
|
+
relative = path.to_s.sub(%r{\A/v1(?:/|\z)}, '').sub(%r{\A/+}, '')
|
|
276
|
+
encoded = relative.split('/', -1).map do |segment|
|
|
277
|
+
if ['.', '..'].include?(segment)
|
|
278
|
+
segment.gsub('.', '%2E')
|
|
279
|
+
else
|
|
280
|
+
URI.encode_www_form_component(segment).gsub('+', '%20')
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
"/v1/#{encoded.join('/')}"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def build_connection
|
|
287
|
+
Faraday.new(url: @address, ssl: tls_options) do |connection|
|
|
288
|
+
connection.options.timeout = @timeout
|
|
289
|
+
connection.options.open_timeout = @timeout
|
|
290
|
+
connection.options.read_timeout = @timeout
|
|
291
|
+
connection.options.write_timeout = @timeout
|
|
292
|
+
connection.adapter :net_http
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def tls_options
|
|
297
|
+
options = { verify: @ssl_verify != false && @ssl_verify.to_s != 'none' }
|
|
298
|
+
if @ca
|
|
299
|
+
if @ca.include?('-----BEGIN')
|
|
300
|
+
store = OpenSSL::X509::Store.new
|
|
301
|
+
certificates = @ca.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
|
|
302
|
+
raise ArgumentError, 'Vault CA PEM contains no certificates' if certificates.empty?
|
|
303
|
+
|
|
304
|
+
certificates.each { |certificate| store.add_cert(OpenSSL::X509::Certificate.new(certificate)) }
|
|
305
|
+
options[:cert_store] = store
|
|
306
|
+
else
|
|
307
|
+
options[:ca_file] = @ca
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
options[:client_cert] = tls_certificate(certificate: @cert) if @cert
|
|
311
|
+
options[:client_key] = tls_key(key: @key) if @key
|
|
312
|
+
options
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def tls_certificate(certificate:, **)
|
|
316
|
+
return certificate if certificate.is_a?(OpenSSL::X509::Certificate)
|
|
317
|
+
|
|
318
|
+
pem = certificate.include?('-----BEGIN') ? certificate : File.read(certificate)
|
|
319
|
+
certificates = pem.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
|
|
320
|
+
return OpenSSL::X509::Certificate.new(pem) if certificates.length < 2
|
|
321
|
+
|
|
322
|
+
certificates.map { |entry| OpenSSL::X509::Certificate.new(entry) }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def tls_key(key:, **)
|
|
326
|
+
return key if key.is_a?(OpenSSL::PKey::PKey)
|
|
327
|
+
|
|
328
|
+
OpenSSL::PKey.read(key.include?('-----BEGIN') ? key : File.read(key))
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def decode_response(response:, address:, **)
|
|
332
|
+
status = response.status
|
|
333
|
+
unless (200..299).cover?(status)
|
|
334
|
+
error_class = if (400..499).cover?(status)
|
|
335
|
+
Legion::Crypt::Vault::HTTPClientError
|
|
336
|
+
elsif (500..599).cover?(status)
|
|
337
|
+
Legion::Crypt::Vault::HTTPServerError
|
|
338
|
+
else
|
|
339
|
+
Legion::Crypt::Vault::HTTPError
|
|
340
|
+
end
|
|
341
|
+
begin
|
|
342
|
+
decoded = response.body.nil? || response.body.empty? ? {} : Legion::JSON.load(response.body)
|
|
343
|
+
rescue Legion::JSON::ParseError
|
|
344
|
+
raise error_class.new(code: status, errors: [response.body.to_s], address: address)
|
|
345
|
+
end
|
|
346
|
+
errors = decoded.is_a?(Hash) ? decoded[:errors] : [response.body.to_s]
|
|
347
|
+
raise error_class.new(code: status, errors: errors, address: address)
|
|
348
|
+
end
|
|
349
|
+
return nil if status == 204 || response.body.nil? || response.body.empty?
|
|
350
|
+
|
|
351
|
+
Legion::JSON.load(response.body)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|