legion-crypt 1.5.38 → 1.5.42
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/.github/workflows/ci.yml +30 -3
- data/.rspec +2 -0
- data/AGENTS.md +2 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +64 -192
- data/CODEOWNERS +4 -4
- data/Gemfile +4 -1
- data/README.md +60 -16
- data/legion-crypt.gemspec +1 -0
- data/lib/legion/crypt/cert_rotation.rb +7 -21
- data/lib/legion/crypt/cluster_secret.rb +3 -14
- data/lib/legion/crypt/ed25519.rb +2 -10
- data/lib/legion/crypt/erasure.rb +3 -13
- data/lib/legion/crypt/jwks_client.rb +4 -4
- data/lib/legion/crypt/kerberos_auth.rb +2 -2
- data/lib/legion/crypt/ldap_auth.rb +2 -2
- data/lib/legion/crypt/lease_manager.rb +7 -7
- data/lib/legion/crypt/mtls.rb +8 -46
- data/lib/legion/crypt/partition_keys.rb +2 -6
- data/lib/legion/crypt/settings.rb +24 -21
- data/lib/legion/crypt/spiffe/svid_rotation.rb +6 -9
- data/lib/legion/crypt/spiffe.rb +14 -39
- data/lib/legion/crypt/token_renewer.rb +6 -6
- data/lib/legion/crypt/vault/client.rb +355 -0
- data/lib/legion/crypt/vault/cluster.rb +330 -0
- data/lib/legion/crypt/vault/entity.rb +80 -0
- data/lib/legion/crypt/vault/jwt_auth.rb +99 -0
- data/lib/legion/crypt/vault/kerberos_auth.rb +53 -0
- data/lib/legion/crypt/vault/operation.rb +29 -0
- data/lib/legion/crypt/vault.rb +6 -11
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +69 -80
- metadata +23 -8
- data/lib/legion/crypt/vault_client.rb +0 -355
- data/lib/legion/crypt/vault_cluster.rb +0 -331
- data/lib/legion/crypt/vault_entity.rb +0 -78
- data/lib/legion/crypt/vault_jwt_auth.rb +0 -97
- data/lib/legion/crypt/vault_kerberos_auth.rb +0 -51
- data/lib/legion/crypt/vault_operation.rb +0 -27
- /data/lib/legion/crypt/{vault_renewer.rb → vault/renewer.rb} +0 -0
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/logging/helper'
|
|
4
|
-
require 'uri'
|
|
5
|
-
require 'legion/crypt/vault_client'
|
|
6
|
-
require 'legion/crypt/tls'
|
|
7
|
-
require 'legion/crypt/vault_operation'
|
|
8
|
-
|
|
9
|
-
module Legion
|
|
10
|
-
module Crypt
|
|
11
|
-
module VaultCluster
|
|
12
|
-
include Legion::Logging::Helper
|
|
13
|
-
|
|
14
|
-
def vault_client(name = nil)
|
|
15
|
-
name = resolve_cluster_name(name)
|
|
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) }
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def cluster(name = nil)
|
|
80
|
-
name = resolve_cluster_name(name)
|
|
81
|
-
clusters[name]
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def default_cluster_name
|
|
85
|
-
name = vault_settings[:default]
|
|
86
|
-
name ? name.to_sym : clusters.keys.first
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def clusters
|
|
90
|
-
vault_settings[:clusters] || {}
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def connected_clusters
|
|
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
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def connect_all_clusters
|
|
104
|
-
log.info "Vault cluster connect requested configured_clusters=#{clusters.size}"
|
|
105
|
-
log_vault_debug("connect_all_clusters: #{clusters.size} cluster(s) configured")
|
|
106
|
-
results = {}
|
|
107
|
-
clusters.each do |name, config|
|
|
108
|
-
log_vault_debug("connect_all_clusters: #{name} (auth_method=#{config[:auth_method].inspect})")
|
|
109
|
-
case config[:auth_method]&.to_s
|
|
110
|
-
when 'kerberos'
|
|
111
|
-
results[name] = connect_kerberos_cluster(name, config)
|
|
112
|
-
when 'ldap'
|
|
113
|
-
next # handled by ldap_login_all
|
|
114
|
-
else
|
|
115
|
-
next unless config[:token]
|
|
116
|
-
|
|
117
|
-
client = vault_client(name)
|
|
118
|
-
config[:connected] = cluster_healthy?(client)
|
|
119
|
-
results[name] = config[:connected]
|
|
120
|
-
log_cluster_connected(name, config) if config[:connected]
|
|
121
|
-
end
|
|
122
|
-
rescue StandardError => e
|
|
123
|
-
config[:connected] = false
|
|
124
|
-
results[name] = false
|
|
125
|
-
log_vault_error(name, e, operation: 'crypt.vault_cluster.connect_all_clusters')
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
connected = results.select { |_, v| v }
|
|
129
|
-
log.info "Vault cluster connect complete connected=#{connected.size} attempted=#{results.size}"
|
|
130
|
-
log_vault_debug("connect_all_clusters: #{connected.size}/#{results.size} connected")
|
|
131
|
-
sync_vault_connected(connected.any?)
|
|
132
|
-
results
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
private
|
|
136
|
-
|
|
137
|
-
def cluster_healthy?(client)
|
|
138
|
-
client.sys.health_status.initialized?
|
|
139
|
-
rescue Legion::Crypt::Vault::HTTPError => e
|
|
140
|
-
return true if [429, 472, 473].include?(e.code)
|
|
141
|
-
|
|
142
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_cluster.cluster_healthy')
|
|
143
|
-
raise
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def sync_vault_connected(connected)
|
|
147
|
-
return unless defined?(Legion::Settings)
|
|
148
|
-
|
|
149
|
-
Legion::Settings[:crypt][:vault][:connected] = connected
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def selected_connected_cluster_name(name = nil)
|
|
153
|
-
active_clusters = connected_clusters
|
|
154
|
-
return nil if active_clusters.empty?
|
|
155
|
-
|
|
156
|
-
if name
|
|
157
|
-
cluster_name = name.to_sym
|
|
158
|
-
raise ArgumentError, "Vault cluster not connected: #{cluster_name}" unless active_clusters.key?(cluster_name)
|
|
159
|
-
|
|
160
|
-
return cluster_name
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
default_name = vault_settings[:default]&.to_sym
|
|
164
|
-
return default_name if default_name && active_clusters.key?(default_name)
|
|
165
|
-
|
|
166
|
-
active_clusters.keys.first
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def resolve_cluster_name(name)
|
|
170
|
-
return name.to_sym if name
|
|
171
|
-
|
|
172
|
-
default_cluster_name
|
|
173
|
-
end
|
|
174
|
-
|
|
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'
|
|
230
|
-
|
|
231
|
-
namespace =
|
|
232
|
-
if config.key?(:namespace)
|
|
233
|
-
config[:namespace]
|
|
234
|
-
elsif defined?(Legion::Settings)
|
|
235
|
-
crypt_settings = Legion::Settings[:crypt]
|
|
236
|
-
crypt_settings.respond_to?(:dig) ? crypt_settings.dig(:vault, :vault_namespace) : nil
|
|
237
|
-
end
|
|
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
|
-
}
|
|
243
|
-
end
|
|
244
|
-
|
|
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)
|
|
249
|
-
|
|
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')
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def log_vault_error(name, error, operation: 'crypt.vault_cluster.error')
|
|
275
|
-
handle_exception(error, level: :error, operation: operation, cluster_name: name)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def connect_kerberos_cluster(name, config)
|
|
279
|
-
krb_config = config[:kerberos] || {}
|
|
280
|
-
spn = krb_config[:service_principal]
|
|
281
|
-
auth_path = krb_config[:auth_path] || Legion::Crypt::KerberosAuth::DEFAULT_AUTH_PATH
|
|
282
|
-
|
|
283
|
-
log_vault_debug("connect_kerberos_cluster[#{name}]: SPN=#{spn}, auth_path=#{auth_path}, namespace=#{config[:namespace].inspect}")
|
|
284
|
-
|
|
285
|
-
unless spn
|
|
286
|
-
log_vault_warn(name, 'Kerberos auth missing service_principal, skipping')
|
|
287
|
-
config[:connected] = false
|
|
288
|
-
return false
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
require 'legion/crypt/kerberos_auth'
|
|
292
|
-
client = vault_client(name)
|
|
293
|
-
log_vault_debug("connect_kerberos_cluster[#{name}]: client.namespace=#{client.respond_to?(:namespace) ? client.namespace.inspect : 'n/a'}")
|
|
294
|
-
log.info "Connecting Vault cluster #{name} via Kerberos auth_path=#{auth_path}"
|
|
295
|
-
|
|
296
|
-
result = Legion::Crypt::KerberosAuth.login(
|
|
297
|
-
vault_client: client,
|
|
298
|
-
service_principal: spn,
|
|
299
|
-
auth_path: auth_path
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
publish_vault_grant(client: client, config: config, grant: result, auth_method: 'kerberos')
|
|
303
|
-
log_vault_debug("connect_kerberos_cluster[#{name}]: policies=#{result[:policies].inspect}")
|
|
304
|
-
log_cluster_connected(name, config)
|
|
305
|
-
true
|
|
306
|
-
rescue Legion::Crypt::KerberosAuth::GemMissingError => e
|
|
307
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_cluster.connect_kerberos_cluster', cluster_name: name)
|
|
308
|
-
log_vault_warn(name, e.message)
|
|
309
|
-
config[:connected] = false
|
|
310
|
-
false
|
|
311
|
-
rescue Legion::Crypt::KerberosAuth::AuthError => e
|
|
312
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_cluster.connect_kerberos_cluster', cluster_name: name)
|
|
313
|
-
log_vault_warn(name, "Kerberos auth failed: #{e.message}")
|
|
314
|
-
config[:connected] = false
|
|
315
|
-
false
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def log_cluster_connected(name, config)
|
|
319
|
-
log.info "Vault cluster connected: #{name} at #{config[:address]}"
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def log_vault_warn(name, message)
|
|
323
|
-
log.warn("Vault cluster #{name}: #{message}")
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def log_vault_debug(message)
|
|
327
|
-
log.debug(message)
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
end
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/logging/helper'
|
|
4
|
-
require 'legion/crypt/vault_client'
|
|
5
|
-
require 'legion/crypt/lease_manager'
|
|
6
|
-
|
|
7
|
-
module Legion
|
|
8
|
-
module Crypt
|
|
9
|
-
module VaultEntity
|
|
10
|
-
extend Legion::Logging::Helper
|
|
11
|
-
|
|
12
|
-
# Create or lookup a Vault entity for a Legion principal.
|
|
13
|
-
# Returns the Vault entity ID string, or nil on failure.
|
|
14
|
-
def self.ensure_entity(principal_id:, canonical_name:, metadata: {})
|
|
15
|
-
existing = find_by_name(canonical_name: canonical_name)
|
|
16
|
-
return existing if existing
|
|
17
|
-
|
|
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
|
-
}
|
|
28
|
-
)
|
|
29
|
-
extract_id(response)
|
|
30
|
-
rescue StandardError => e
|
|
31
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_entity', canonical_name: canonical_name)
|
|
32
|
-
nil
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Create an alias linking an auth method mount to the entity.
|
|
36
|
-
# Idempotent — swallows "already exists" 4xx errors.
|
|
37
|
-
def self.ensure_alias(entity_id:, mount_accessor:, alias_name:)
|
|
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 }
|
|
41
|
-
)
|
|
42
|
-
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
43
|
-
if e.message.include?('already exists')
|
|
44
|
-
log.debug 'Vault entity alias already exists (idempotent)'
|
|
45
|
-
else
|
|
46
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_alias', alias_name: alias_name)
|
|
47
|
-
end
|
|
48
|
-
nil
|
|
49
|
-
rescue StandardError => e
|
|
50
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_alias', alias_name: alias_name)
|
|
51
|
-
nil
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Look up a Vault entity by its Legion canonical name.
|
|
55
|
-
# Returns the Vault entity ID string, or nil if not found.
|
|
56
|
-
def self.find_by_name(canonical_name:)
|
|
57
|
-
response = Legion::Crypt::LeaseManager.instance.vault_logical.read(path: "identity/entity/name/legion-#{canonical_name}")
|
|
58
|
-
extract_id(response)
|
|
59
|
-
rescue StandardError => e
|
|
60
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.find_by_name', canonical_name: canonical_name)
|
|
61
|
-
nil
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# ---------------------------------------------------------------------------
|
|
65
|
-
# Private helpers
|
|
66
|
-
# ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
# Extract the entity ID from the client's symbol-keyed response data.
|
|
69
|
-
def self.extract_id(response)
|
|
70
|
-
data = response&.data
|
|
71
|
-
return nil unless data
|
|
72
|
-
|
|
73
|
-
data[:id]
|
|
74
|
-
end
|
|
75
|
-
private_class_method :extract_id
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/logging/helper'
|
|
4
|
-
require 'legion/crypt/vault_client'
|
|
5
|
-
|
|
6
|
-
module Legion
|
|
7
|
-
module Crypt
|
|
8
|
-
# Vault JWT auth backend integration.
|
|
9
|
-
#
|
|
10
|
-
# Allows Legion workers to authenticate to Vault using JWT tokens
|
|
11
|
-
# via Vault's JWT/OIDC auth method. The worker presents a signed JWT
|
|
12
|
-
# and receives a Vault token with policies scoped to the worker's role.
|
|
13
|
-
#
|
|
14
|
-
# Vault config prerequisites:
|
|
15
|
-
# vault auth enable jwt
|
|
16
|
-
# vault write auth/jwt/config jwks_url="..." (or bound_issuer + jwt_validation_pubkeys)
|
|
17
|
-
# vault write auth/jwt/role/legion-worker bound_audiences="legion" ...
|
|
18
|
-
module VaultJwtAuth
|
|
19
|
-
DEFAULT_AUTH_PATH = 'auth/jwt/login'
|
|
20
|
-
DEFAULT_ROLE = 'legion-worker'
|
|
21
|
-
|
|
22
|
-
class AuthError < StandardError; end
|
|
23
|
-
|
|
24
|
-
extend Legion::Logging::Helper
|
|
25
|
-
|
|
26
|
-
# Authenticate to Vault using a JWT token.
|
|
27
|
-
# Returns a Vault token string on success.
|
|
28
|
-
#
|
|
29
|
-
# @param jwt [String] Signed JWT token (issued by Legion or Entra ID)
|
|
30
|
-
# @param role [String] Vault JWT auth role name (default: 'legion-worker')
|
|
31
|
-
# @param auth_path [String] Vault auth mount path (default: 'auth/jwt/login')
|
|
32
|
-
# @return [Hash] { token:, lease_duration:, policies:, metadata: }
|
|
33
|
-
def self.login(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
|
|
34
|
-
raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
|
|
35
|
-
|
|
36
|
-
log.info "[crypt:vault_jwt] authenticating role=#{role} auth_path=#{auth_path}"
|
|
37
|
-
response = Legion::Crypt.vault_client.logical.write(path: auth_path, data: { role: role, jwt: jwt })
|
|
38
|
-
|
|
39
|
-
raise AuthError, 'Vault JWT auth returned no auth data' unless response&.auth
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
token: response.auth.client_token,
|
|
43
|
-
lease_duration: response.auth.lease_duration,
|
|
44
|
-
renewable: response.auth.renewable?,
|
|
45
|
-
policies: response.auth.policies,
|
|
46
|
-
metadata: response.auth.metadata
|
|
47
|
-
}
|
|
48
|
-
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
49
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
|
|
50
|
-
category: 'client_error')
|
|
51
|
-
raise AuthError, "Vault JWT auth failed: #{e.message}"
|
|
52
|
-
rescue Legion::Crypt::Vault::HTTPServerError => e
|
|
53
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
|
|
54
|
-
category: 'server_error')
|
|
55
|
-
raise AuthError, "Vault server error during JWT auth: #{e.message}"
|
|
56
|
-
rescue StandardError => e
|
|
57
|
-
handle_exception(e, level: :error, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path)
|
|
58
|
-
raise if e.is_a?(AuthError)
|
|
59
|
-
|
|
60
|
-
raise AuthError, "Vault JWT auth failed: #{e.message}"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Authenticate and set the Vault client token for subsequent operations.
|
|
64
|
-
# This replaces the current Vault token with the JWT-authenticated one.
|
|
65
|
-
#
|
|
66
|
-
# @return [Hash] Same as login
|
|
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
|
-
result = login(jwt: jwt, role: role, auth_path: auth_path)
|
|
72
|
-
Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'jwt', generation: generation)
|
|
73
|
-
log.info "[crypt:vault_jwt] authenticated via JWT auth, policies=#{result[:policies].join(',')}"
|
|
74
|
-
result
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Issue a Legion JWT and use it to authenticate to Vault in one step.
|
|
78
|
-
# Convenience method for workers that need Vault access.
|
|
79
|
-
#
|
|
80
|
-
# @param worker_id [String] Digital worker ID
|
|
81
|
-
# @param owner_msid [String] Worker's owner MSID
|
|
82
|
-
# @param role [String] Vault JWT auth role name
|
|
83
|
-
# @return [Hash] Same as login
|
|
84
|
-
def self.worker_login(worker_id:, owner_msid:, role: DEFAULT_ROLE)
|
|
85
|
-
log.info "[crypt:vault_jwt] worker login requested role=#{role} worker_id=#{worker_id}"
|
|
86
|
-
jwt = Legion::Crypt::JWT.issue(
|
|
87
|
-
{ worker_id: worker_id, sub: owner_msid, scope: 'vault', aud: 'legion' },
|
|
88
|
-
signing_key: Legion::Crypt.cluster_secret,
|
|
89
|
-
ttl: 300,
|
|
90
|
-
issuer: 'legion'
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
login(jwt: jwt, role: role)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/logging/helper'
|
|
4
|
-
require 'legion/crypt/vault_client'
|
|
5
|
-
|
|
6
|
-
module Legion
|
|
7
|
-
module Crypt
|
|
8
|
-
module VaultKerberosAuth
|
|
9
|
-
DEFAULT_AUTH_PATH = 'auth/kerberos/login'
|
|
10
|
-
|
|
11
|
-
class AuthError < StandardError; end
|
|
12
|
-
|
|
13
|
-
extend Legion::Logging::Helper
|
|
14
|
-
|
|
15
|
-
def self.login(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
|
|
16
|
-
raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
|
|
17
|
-
|
|
18
|
-
log.info "[crypt:vault_kerberos] login requested auth_path=#{auth_path}"
|
|
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)
|
|
23
|
-
raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
token: response.auth.client_token,
|
|
27
|
-
lease_duration: response.auth.lease_duration,
|
|
28
|
-
renewable: response.auth.renewable?,
|
|
29
|
-
policies: response.auth.policies,
|
|
30
|
-
metadata: response.auth.metadata
|
|
31
|
-
}
|
|
32
|
-
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
33
|
-
handle_exception(e, level: :warn, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
|
|
34
|
-
raise AuthError, "Vault Kerberos auth failed: #{e.message}"
|
|
35
|
-
rescue StandardError => e
|
|
36
|
-
handle_exception(e, level: :error, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
|
|
37
|
-
raise
|
|
38
|
-
end
|
|
39
|
-
|
|
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]
|
|
44
|
-
result = login(spnego_token: spnego_token, auth_path: auth_path)
|
|
45
|
-
Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'kerberos', generation: generation)
|
|
46
|
-
log.info "[crypt:vault_kerberos] authenticated via Kerberos auth, policies=#{result[:policies].join(',')}"
|
|
47
|
-
result
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
|
File without changes
|