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
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Ruby 4.0 freezes OpenSSL::SSL::SSLContext::DEFAULT_PARAMS by default.
|
|
4
|
-
# The vault gem (0.18.x) mutates this hash in Vault.setup! — replace it
|
|
5
|
-
# with a mutable dup so the require succeeds on Ruby 4.0+.
|
|
6
3
|
require 'legion/logging/helper'
|
|
7
|
-
require '
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
OpenSSL::SSL::SSLContext.const_set(:DEFAULT_PARAMS, unfrozen)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
require 'vault'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'legion/crypt/vault_client'
|
|
6
|
+
require 'legion/crypt/tls'
|
|
7
|
+
require 'legion/crypt/vault_operation'
|
|
15
8
|
|
|
16
9
|
module Legion
|
|
17
10
|
module Crypt
|
|
@@ -20,8 +13,67 @@ module Legion
|
|
|
20
13
|
|
|
21
14
|
def vault_client(name = nil)
|
|
22
15
|
name = resolve_cluster_name(name)
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
loop do
|
|
17
|
+
candidate = accepted = nil
|
|
18
|
+
signature, generation = vault_clients_mutex.synchronize do
|
|
19
|
+
return @vault_clients&.dig(name, :client) if Legion::Crypt.respond_to?(:stopping) && Legion::Crypt.stopping
|
|
20
|
+
|
|
21
|
+
@vault_clients ||= {}
|
|
22
|
+
entry = @vault_clients[name]
|
|
23
|
+
signature = vault_client_signature(config: name.nil? ? vault_settings : clusters[name])
|
|
24
|
+
cached = cached_vault_client(entry: entry, signature: signature)
|
|
25
|
+
return cached if cached
|
|
26
|
+
|
|
27
|
+
unless signature
|
|
28
|
+
(@retired_vault_clients ||= ObjectSpace::WeakMap.new)[entry[:client]] = true if entry
|
|
29
|
+
@vault_clients.delete(name)
|
|
30
|
+
return nil
|
|
31
|
+
end
|
|
32
|
+
signature.transform_values! { |value| value.is_a?(String) ? value.dup.freeze : value }
|
|
33
|
+
[signature, @vault_client_generation || 0]
|
|
34
|
+
end
|
|
35
|
+
candidate = build_vault_client(signature, name: name)
|
|
36
|
+
accepted = install_vault_client(name: name, signature: signature, client: candidate, generation: generation)
|
|
37
|
+
return accepted if accepted
|
|
38
|
+
ensure
|
|
39
|
+
retire_vault_client(client: candidate) unless accepted.equal?(candidate)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def publish_vault_grant(client:, config:, grant:, auth_method:, issued_at: Time.now, **)
|
|
44
|
+
token = grant.fetch(:token)
|
|
45
|
+
ttl = grant[:lease_duration].to_f
|
|
46
|
+
expires_at = issued_at + ttl if ttl.positive?
|
|
47
|
+
vault_clients_mutex.synchronize do
|
|
48
|
+
pair = @vault_clients&.find { |_name, entry| entry[:client].equal?(client) }
|
|
49
|
+
raise IOError, 'Vault client replaced during authentication' if @retired_vault_clients&.key?(client)
|
|
50
|
+
|
|
51
|
+
if pair
|
|
52
|
+
name, entry = pair
|
|
53
|
+
current_config = name.nil? ? vault_settings : clusters[name]
|
|
54
|
+
unless config.equal?(current_config) && current_vault_client?(entry: entry, signature: vault_client_signature(config: current_config))
|
|
55
|
+
raise IOError, 'Vault client configuration changed during authentication'
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
client.token = token
|
|
59
|
+
connected = expires_at.nil? || expires_at > Time.now
|
|
60
|
+
config.merge!(token: token, lease_duration: grant[:lease_duration], renewable: grant[:renewable],
|
|
61
|
+
expires_at: expires_at, connected: connected, relogin_required: !connected, auth_method: auth_method)
|
|
62
|
+
entry[:signature][:token] = token&.dup&.freeze if entry
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clear_vault_clients(deadline: nil, **)
|
|
67
|
+
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::VaultOperation::SHUTDOWN_TIMEOUT
|
|
68
|
+
clients = vault_clients_mutex.synchronize do
|
|
69
|
+
@vault_client_generation = (@vault_client_generation || 0) + 1
|
|
70
|
+
previous = (@vault_clients || {}).values.map { |entry| entry[:client] }
|
|
71
|
+
previous.concat(@retired_vault_clients.keys) if @retired_vault_clients
|
|
72
|
+
@vault_clients = {}
|
|
73
|
+
@retired_vault_clients = nil
|
|
74
|
+
previous.uniq
|
|
75
|
+
end
|
|
76
|
+
clients.each { |client| retire_vault_client(client: client, deadline: deadline) }
|
|
25
77
|
end
|
|
26
78
|
|
|
27
79
|
def cluster(name = nil)
|
|
@@ -39,7 +91,13 @@ module Legion
|
|
|
39
91
|
end
|
|
40
92
|
|
|
41
93
|
def connected_clusters
|
|
42
|
-
|
|
94
|
+
vault_clients_mutex.synchronize do
|
|
95
|
+
now = Time.now
|
|
96
|
+
clusters.select do |_, config|
|
|
97
|
+
config[:token] && config[:connected] && !config[:relogin_required] &&
|
|
98
|
+
(!config[:expires_at] || config[:expires_at] > now)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
43
101
|
end
|
|
44
102
|
|
|
45
103
|
def connect_all_clusters
|
|
@@ -78,8 +136,8 @@ module Legion
|
|
|
78
136
|
|
|
79
137
|
def cluster_healthy?(client)
|
|
80
138
|
client.sys.health_status.initialized?
|
|
81
|
-
rescue ::Vault::HTTPError => e
|
|
82
|
-
return true if
|
|
139
|
+
rescue Legion::Crypt::Vault::HTTPError => e
|
|
140
|
+
return true if [429, 472, 473].include?(e.code)
|
|
83
141
|
|
|
84
142
|
handle_exception(e, level: :warn, operation: 'crypt.vault_cluster.cluster_healthy')
|
|
85
143
|
raise
|
|
@@ -114,18 +172,62 @@ module Legion
|
|
|
114
172
|
default_cluster_name
|
|
115
173
|
end
|
|
116
174
|
|
|
117
|
-
def
|
|
118
|
-
|
|
175
|
+
def vault_clients_mutex
|
|
176
|
+
@vault_clients_mutex ||= Mutex.new
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def current_vault_client?(entry:, signature:, **)
|
|
180
|
+
return false unless entry && signature
|
|
181
|
+
return false unless entry[:signature].all? { |key, value| key == :token || value == signature[key] }
|
|
182
|
+
|
|
183
|
+
entry[:signature][:token] == signature[:token] || entry[:client].token == signature[:token]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def cached_vault_client(entry:, signature:, **)
|
|
187
|
+
return unless current_vault_client?(entry: entry, signature: signature)
|
|
188
|
+
|
|
189
|
+
entry[:signature][:token] = signature[:token]&.dup if entry[:signature][:token] != signature[:token]
|
|
190
|
+
entry[:client]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def install_vault_client(name:, signature:, client:, generation:, **)
|
|
194
|
+
vault_clients_mutex.synchronize do
|
|
195
|
+
raise IOError, 'Vault clients cleared during construction' unless generation == (@vault_client_generation || 0)
|
|
196
|
+
raise IOError, 'Crypt stopped during client construction' if Legion::Crypt.respond_to?(:stopping) && Legion::Crypt.stopping
|
|
197
|
+
|
|
198
|
+
entry = @vault_clients[name]
|
|
199
|
+
current_signature = vault_client_signature(config: name.nil? ? vault_settings : clusters[name])
|
|
200
|
+
cached = cached_vault_client(entry: entry, signature: current_signature)
|
|
201
|
+
return cached if cached
|
|
202
|
+
return unless signature == current_signature
|
|
203
|
+
|
|
204
|
+
(@retired_vault_clients ||= ObjectSpace::WeakMap.new)[entry[:client]] = true if entry
|
|
205
|
+
@vault_clients[name] = { client: client, signature: signature }
|
|
206
|
+
client
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def resolve_vault_address(vault_settings:, **)
|
|
211
|
+
protocol = vault_settings[:protocol] || 'http'
|
|
212
|
+
address = vault_settings[:address] || 'localhost'
|
|
213
|
+
port = vault_settings[:port] || 8200
|
|
214
|
+
|
|
215
|
+
if address.match?(%r{\Ahttps?://})
|
|
216
|
+
uri = URI.parse(address)
|
|
217
|
+
protocol = uri.scheme
|
|
218
|
+
address = uri.host
|
|
219
|
+
port = uri.port if vault_settings[:port].nil?
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
"#{protocol}://#{address}:#{port}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def vault_client_signature(config:, **)
|
|
226
|
+
return unless config.is_a?(Hash)
|
|
227
|
+
|
|
228
|
+
tls = Legion::Crypt::TLS.migrate_legacy(config[:tls] || {})
|
|
229
|
+
verify = %w[none mutual].include?(tls[:verify].to_s) ? tls[:verify].to_s : 'peer'
|
|
119
230
|
|
|
120
|
-
addr = "#{config[:protocol]}://#{config[:address]}:#{config[:port]}"
|
|
121
|
-
ssl_verify = resolve_cluster_ssl_verify(config[:tls])
|
|
122
|
-
log.info "Building Vault client address=#{addr} namespace=#{config[:namespace].inspect} ssl_verify=#{ssl_verify}"
|
|
123
|
-
log_vault_debug("build_vault_client: address=#{addr}")
|
|
124
|
-
client = ::Vault::Client.new(
|
|
125
|
-
address: addr,
|
|
126
|
-
token: config[:token],
|
|
127
|
-
ssl_verify: ssl_verify
|
|
128
|
-
)
|
|
129
231
|
namespace =
|
|
130
232
|
if config.key?(:namespace)
|
|
131
233
|
config[:namespace]
|
|
@@ -133,16 +235,40 @@ module Legion
|
|
|
133
235
|
crypt_settings = Legion::Settings[:crypt]
|
|
134
236
|
crypt_settings.respond_to?(:dig) ? crypt_settings.dig(:vault, :vault_namespace) : nil
|
|
135
237
|
end
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
238
|
+
{
|
|
239
|
+
address: resolve_vault_address(vault_settings: config), namespace: namespace, token: config[:token],
|
|
240
|
+
verify: verify, ca: tls[:ca], cert: tls[:cert], key: tls[:key],
|
|
241
|
+
timeout: vault_settings[:operation_timeout] || Legion::Crypt::VaultOperation::DEFAULT_TIMEOUT
|
|
242
|
+
}
|
|
139
243
|
end
|
|
140
244
|
|
|
141
|
-
def
|
|
142
|
-
|
|
245
|
+
def build_vault_client(signature, name:)
|
|
246
|
+
building = Thread.current[:legion_vault_clients_building] ||= []
|
|
247
|
+
build_key = [self, name]
|
|
248
|
+
raise IOError, 'Circular Vault TLS material dependency' if building.include?(build_key)
|
|
143
249
|
|
|
144
|
-
|
|
145
|
-
|
|
250
|
+
building << build_key
|
|
251
|
+
begin
|
|
252
|
+
tls = Legion::Crypt::TLS.resolve(signature.slice(:verify, :ca, :cert, :key))
|
|
253
|
+
%i[ca cert key].each do |attribute|
|
|
254
|
+
raise IOError, "Configured Vault TLS #{attribute} could not be resolved" if signature[attribute] && tls[attribute].nil?
|
|
255
|
+
end
|
|
256
|
+
options = signature.slice(:address, :namespace, :token, :timeout).merge(tls: tls)
|
|
257
|
+
log.info "Building Vault client address=#{options[:address]} namespace=#{options[:namespace].inspect} tls_verify=#{tls[:verify]}"
|
|
258
|
+
Legion::Crypt::VaultClient.new(**options)
|
|
259
|
+
ensure
|
|
260
|
+
building.delete(build_key)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def retire_vault_client(client:, deadline: nil, **)
|
|
265
|
+
return unless client.respond_to?(:shutdown)
|
|
266
|
+
|
|
267
|
+
Legion::Crypt::VaultOperation.with_timeout(deadline: deadline) do
|
|
268
|
+
client.shutdown
|
|
269
|
+
end
|
|
270
|
+
rescue StandardError => e
|
|
271
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_cluster.retire_vault_client')
|
|
146
272
|
end
|
|
147
273
|
|
|
148
274
|
def log_vault_error(name, error, operation: 'crypt.vault_cluster.error')
|
|
@@ -173,11 +299,7 @@ module Legion
|
|
|
173
299
|
auth_path: auth_path
|
|
174
300
|
)
|
|
175
301
|
|
|
176
|
-
config
|
|
177
|
-
config[:lease_duration] = result[:lease_duration]
|
|
178
|
-
config[:renewable] = result[:renewable]
|
|
179
|
-
config[:connected] = true
|
|
180
|
-
vault_client(name).token = result[:token]
|
|
302
|
+
publish_vault_grant(client: client, config: config, grant: result, auth_method: 'kerberos')
|
|
181
303
|
log_vault_debug("connect_kerberos_cluster[#{name}]: policies=#{result[:policies].inspect}")
|
|
182
304
|
log_cluster_connected(name, config)
|
|
183
305
|
true
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault_client'
|
|
5
|
+
require 'legion/crypt/lease_manager'
|
|
4
6
|
|
|
5
7
|
module Legion
|
|
6
8
|
module Crypt
|
|
@@ -10,17 +12,19 @@ module Legion
|
|
|
10
12
|
# Create or lookup a Vault entity for a Legion principal.
|
|
11
13
|
# Returns the Vault entity ID string, or nil on failure.
|
|
12
14
|
def self.ensure_entity(principal_id:, canonical_name:, metadata: {})
|
|
13
|
-
existing = find_by_name(canonical_name)
|
|
15
|
+
existing = find_by_name(canonical_name: canonical_name)
|
|
14
16
|
return existing if existing
|
|
15
17
|
|
|
16
|
-
response = vault_logical.write(
|
|
17
|
-
'identity/entity',
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
response = Legion::Crypt::LeaseManager.instance.vault_logical.write(
|
|
19
|
+
path: 'identity/entity',
|
|
20
|
+
data: {
|
|
21
|
+
name: "legion-#{canonical_name}",
|
|
22
|
+
metadata: metadata.merge(
|
|
23
|
+
legion_principal_id: principal_id,
|
|
24
|
+
legion_canonical_name: canonical_name,
|
|
25
|
+
managed_by: 'legion'
|
|
26
|
+
)
|
|
27
|
+
}
|
|
24
28
|
)
|
|
25
29
|
extract_id(response)
|
|
26
30
|
rescue StandardError => e
|
|
@@ -31,13 +35,11 @@ module Legion
|
|
|
31
35
|
# Create an alias linking an auth method mount to the entity.
|
|
32
36
|
# Idempotent — swallows "already exists" 4xx errors.
|
|
33
37
|
def self.ensure_alias(entity_id:, mount_accessor:, alias_name:)
|
|
34
|
-
vault_logical.write(
|
|
35
|
-
'identity/entity-alias',
|
|
36
|
-
name:
|
|
37
|
-
canonical_id: entity_id,
|
|
38
|
-
mount_accessor: mount_accessor
|
|
38
|
+
Legion::Crypt::LeaseManager.instance.vault_logical.write(
|
|
39
|
+
path: 'identity/entity-alias',
|
|
40
|
+
data: { name: alias_name, canonical_id: entity_id, mount_accessor: mount_accessor }
|
|
39
41
|
)
|
|
40
|
-
rescue ::Vault::HTTPClientError => e
|
|
42
|
+
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
41
43
|
if e.message.include?('already exists')
|
|
42
44
|
log.debug 'Vault entity alias already exists (idempotent)'
|
|
43
45
|
else
|
|
@@ -51,14 +53,9 @@ module Legion
|
|
|
51
53
|
|
|
52
54
|
# Look up a Vault entity by its Legion canonical name.
|
|
53
55
|
# Returns the Vault entity ID string, or nil if not found.
|
|
54
|
-
def self.find_by_name(canonical_name)
|
|
55
|
-
response = vault_logical.read("identity/entity/name/legion-#{canonical_name}")
|
|
56
|
+
def self.find_by_name(canonical_name:)
|
|
57
|
+
response = Legion::Crypt::LeaseManager.instance.vault_logical.read(path: "identity/entity/name/legion-#{canonical_name}")
|
|
56
58
|
extract_id(response)
|
|
57
|
-
rescue ::Vault::HTTPClientError => e
|
|
58
|
-
unless e.message.match?(/not found|does not exist|404/i)
|
|
59
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.find_by_name', canonical_name: canonical_name)
|
|
60
|
-
end
|
|
61
|
-
nil
|
|
62
59
|
rescue StandardError => e
|
|
63
60
|
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.find_by_name', canonical_name: canonical_name)
|
|
64
61
|
nil
|
|
@@ -68,22 +65,12 @@ module Legion
|
|
|
68
65
|
# Private helpers
|
|
69
66
|
# ---------------------------------------------------------------------------
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
if defined?(Legion::Crypt::LeaseManager)
|
|
73
|
-
Legion::Crypt::LeaseManager.instance.vault_logical
|
|
74
|
-
else
|
|
75
|
-
::Vault.logical
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
private_class_method :vault_logical
|
|
79
|
-
|
|
80
|
-
# Extract entity ID from a Vault response, supporting both symbol and
|
|
81
|
-
# string keys (Vault SDK may return either depending on version/transport).
|
|
68
|
+
# Extract the entity ID from the client's symbol-keyed response data.
|
|
82
69
|
def self.extract_id(response)
|
|
83
70
|
data = response&.data
|
|
84
71
|
return nil unless data
|
|
85
72
|
|
|
86
|
-
data[:id]
|
|
73
|
+
data[:id]
|
|
87
74
|
end
|
|
88
75
|
private_class_method :extract_id
|
|
89
76
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault_client'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
@@ -30,14 +31,10 @@ module Legion
|
|
|
30
31
|
# @param auth_path [String] Vault auth mount path (default: 'auth/jwt/login')
|
|
31
32
|
# @return [Hash] { token:, lease_duration:, policies:, metadata: }
|
|
32
33
|
def self.login(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
|
|
33
|
-
raise AuthError, 'Vault is not connected' unless vault_connected?
|
|
34
|
+
raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
|
|
34
35
|
|
|
35
36
|
log.info "[crypt:vault_jwt] authenticating role=#{role} auth_path=#{auth_path}"
|
|
36
|
-
response = ::
|
|
37
|
-
auth_path,
|
|
38
|
-
role: role,
|
|
39
|
-
jwt: jwt
|
|
40
|
-
)
|
|
37
|
+
response = Legion::Crypt.vault_client.logical.write(path: auth_path, data: { role: role, jwt: jwt })
|
|
41
38
|
|
|
42
39
|
raise AuthError, 'Vault JWT auth returned no auth data' unless response&.auth
|
|
43
40
|
|
|
@@ -48,11 +45,11 @@ module Legion
|
|
|
48
45
|
policies: response.auth.policies,
|
|
49
46
|
metadata: response.auth.metadata
|
|
50
47
|
}
|
|
51
|
-
rescue ::Vault::HTTPClientError => e
|
|
48
|
+
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
52
49
|
handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
|
|
53
50
|
category: 'client_error')
|
|
54
51
|
raise AuthError, "Vault JWT auth failed: #{e.message}"
|
|
55
|
-
rescue ::Vault::HTTPServerError => e
|
|
52
|
+
rescue Legion::Crypt::Vault::HTTPServerError => e
|
|
56
53
|
handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
|
|
57
54
|
category: 'server_error')
|
|
58
55
|
raise AuthError, "Vault server error during JWT auth: #{e.message}"
|
|
@@ -68,8 +65,11 @@ module Legion
|
|
|
68
65
|
#
|
|
69
66
|
# @return [Hash] Same as login
|
|
70
67
|
def self.login!(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
|
|
68
|
+
generation = Legion::Crypt.vault_auth_generation
|
|
69
|
+
client = Legion::Crypt.vault_client
|
|
70
|
+
config = Legion::Crypt.cluster || Legion::Settings[:crypt][:vault]
|
|
71
71
|
result = login(jwt: jwt, role: role, auth_path: auth_path)
|
|
72
|
-
::
|
|
72
|
+
Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'jwt', generation: generation)
|
|
73
73
|
log.info "[crypt:vault_jwt] authenticated via JWT auth, policies=#{result[:policies].join(',')}"
|
|
74
74
|
result
|
|
75
75
|
end
|
|
@@ -92,17 +92,6 @@ module Legion
|
|
|
92
92
|
|
|
93
93
|
login(jwt: jwt, role: role)
|
|
94
94
|
end
|
|
95
|
-
|
|
96
|
-
def self.vault_connected?
|
|
97
|
-
defined?(::Vault) &&
|
|
98
|
-
defined?(Legion::Settings) &&
|
|
99
|
-
Legion::Settings[:crypt][:vault][:connected] == true
|
|
100
|
-
rescue StandardError => e
|
|
101
|
-
handle_exception(e, level: :debug, operation: 'crypt.vault_jwt_auth.vault_connected?')
|
|
102
|
-
false
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
private_class_method :vault_connected?
|
|
106
95
|
end
|
|
107
96
|
end
|
|
108
97
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault_client'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
@@ -12,10 +13,13 @@ module Legion
|
|
|
12
13
|
extend Legion::Logging::Helper
|
|
13
14
|
|
|
14
15
|
def self.login(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
|
|
15
|
-
raise AuthError, 'Vault is not connected' unless vault_connected?
|
|
16
|
+
raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
|
|
16
17
|
|
|
17
18
|
log.info "[crypt:vault_kerberos] login requested auth_path=#{auth_path}"
|
|
18
|
-
|
|
19
|
+
data = Legion::Crypt.vault_client.put(
|
|
20
|
+
path: auth_path, body: '{}', headers: { 'Authorization' => "Negotiate #{spnego_token}" }
|
|
21
|
+
)
|
|
22
|
+
response = Legion::Crypt::VaultClient::Secret.decode(data: data)
|
|
19
23
|
raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
|
|
20
24
|
|
|
21
25
|
{
|
|
@@ -25,7 +29,7 @@ module Legion
|
|
|
25
29
|
policies: response.auth.policies,
|
|
26
30
|
metadata: response.auth.metadata
|
|
27
31
|
}
|
|
28
|
-
rescue ::Vault::HTTPClientError => e
|
|
32
|
+
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
29
33
|
handle_exception(e, level: :warn, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
|
|
30
34
|
raise AuthError, "Vault Kerberos auth failed: #{e.message}"
|
|
31
35
|
rescue StandardError => e
|
|
@@ -34,21 +38,14 @@ module Legion
|
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def self.login!(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
|
|
41
|
+
generation = Legion::Crypt.vault_auth_generation
|
|
42
|
+
client = Legion::Crypt.vault_client
|
|
43
|
+
config = Legion::Crypt.cluster || Legion::Settings[:crypt][:vault]
|
|
37
44
|
result = login(spnego_token: spnego_token, auth_path: auth_path)
|
|
38
|
-
::
|
|
45
|
+
Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'kerberos', generation: generation)
|
|
39
46
|
log.info "[crypt:vault_kerberos] authenticated via Kerberos auth, policies=#{result[:policies].join(',')}"
|
|
40
47
|
result
|
|
41
48
|
end
|
|
42
|
-
|
|
43
|
-
def self.vault_connected?
|
|
44
|
-
defined?(::Vault) && defined?(Legion::Settings) &&
|
|
45
|
-
Legion::Settings[:crypt][:vault][:connected] == true
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
handle_exception(e, level: :debug, operation: 'crypt.vault_kerberos_auth.vault_connected')
|
|
48
|
-
false
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private_class_method :vault_connected?
|
|
52
49
|
end
|
|
53
50
|
end
|
|
54
51
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'timeout'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Crypt
|
|
7
|
+
module VaultOperation
|
|
8
|
+
DEFAULT_TIMEOUT = 5
|
|
9
|
+
SHUTDOWN_TIMEOUT = 10
|
|
10
|
+
|
|
11
|
+
def self.with_timeout(deadline: nil, **)
|
|
12
|
+
configuration = defined?(Legion::Settings) ? Legion::Settings[:crypt] : nil
|
|
13
|
+
timeout = configuration&.dig(:vault, :operation_timeout) || DEFAULT_TIMEOUT
|
|
14
|
+
raise ArgumentError, 'Vault operation_timeout must be a finite positive number' unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
15
|
+
|
|
16
|
+
if deadline
|
|
17
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
18
|
+
raise Timeout::Error, 'Vault operation deadline expired' unless remaining.positive?
|
|
19
|
+
|
|
20
|
+
timeout = [timeout, remaining].min
|
|
21
|
+
end
|
|
22
|
+
operation_deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
|
|
23
|
+
Timeout.timeout(timeout) { yield(operation_deadline) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|