legion-crypt 1.5.13 → 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.
data/lib/legion/crypt.rb CHANGED
@@ -9,7 +9,9 @@ require 'legion/crypt/cipher'
9
9
  require 'legion/crypt/jwt'
10
10
  require 'legion/crypt/vault_jwt_auth'
11
11
  require 'legion/crypt/lease_manager'
12
+ require 'legion/crypt/vault_operation'
12
13
  require 'legion/crypt/vault_cluster'
14
+ require 'legion/crypt/vault'
13
15
  require 'legion/crypt/ldap_auth'
14
16
  require 'legion/crypt/token_renewer'
15
17
  require 'legion/crypt/helper'
@@ -32,15 +34,12 @@ module Legion
32
34
  }.freeze
33
35
 
34
36
  class << self
35
- attr_reader :sessions
37
+ attr_reader :sessions, :stopping
36
38
 
37
39
  include Legion::Logging::Helper
38
40
  include Legion::Crypt::Cipher
39
41
 
40
- unless Gem::Specification.find_by_name('vault').nil?
41
- require 'legion/crypt/vault'
42
- include Legion::Crypt::Vault
43
- end
42
+ include Legion::Crypt::Vault
44
43
 
45
44
  def vault_settings
46
45
  Legion::Settings[:crypt][:vault]
@@ -66,24 +65,32 @@ module Legion
66
65
 
67
66
  def start
68
67
  lifecycle_mutex.synchronize do
69
- if @started
70
- log.info 'Legion::Crypt start ignored because the lifecycle is already running'
68
+ if @started || @stopping
69
+ log.info 'Legion::Crypt start ignored because the lifecycle is running or stopping'
71
70
  return
72
71
  end
73
72
 
74
73
  log.info 'Legion::Crypt startup initiated'
75
74
  log.debug 'Legion::Crypt start requested'
76
- ::File.write('./legionio.key', private_key) if settings[:save_private_key]
75
+ if settings[:save_private_key]
76
+ key = private_key
77
+ ::File.open('./legionio.key', ::File::WRONLY | ::File::CREAT, 0o600) do |file|
78
+ file.chmod(0o600)
79
+ file.truncate(0)
80
+ file.write(key.to_pem)
81
+ end
82
+ end
77
83
  @token_renewers ||= []
84
+ start_session_tracking
78
85
 
79
86
  if vault_settings[:clusters]&.any?
80
87
  log.info "Legion::Crypt connecting #{vault_settings[:clusters].size} Vault cluster(s)"
81
88
  connect_all_clusters
82
- start_token_renewers
83
89
  else
84
90
  log.info 'Legion::Crypt connecting primary Vault client' unless settings[:vault][:token].nil?
85
91
  connect_vault unless settings[:vault][:token].nil?
86
92
  end
93
+ start_token_renewers
87
94
  start_lease_manager
88
95
  start_svid_rotation
89
96
  @started = true
@@ -91,6 +98,29 @@ module Legion
91
98
  end
92
99
  end
93
100
 
101
+ def vault_auth_generation
102
+ lifecycle_mutex.synchronize do
103
+ raise 'Crypt is shutting down; login must be retried' if @stopping
104
+
105
+ @token_generation || 0
106
+ end
107
+ end
108
+
109
+ def install_vault_grant(client:, config:, grant:, auth_method:, generation:, **)
110
+ issued_at = Time.now
111
+ lifecycle_mutex.synchronize do
112
+ raise 'Crypt stopped during login; login must be retried' if @stopping || generation != (@token_generation || 0)
113
+
114
+ cluster_name = clusters.find { |_name, candidate| candidate.equal?(config) }&.first
115
+ previous = (@token_renewers || []).find { |worker| worker.cluster_name == cluster_name }
116
+ previous&.stop
117
+ raise 'Previous token worker has not stopped; login must be retried' if previous&.running?
118
+
119
+ publish_vault_grant(client: client, config: config, grant: grant, auth_method: auth_method, issued_at: issued_at)
120
+ start_token_renewer(cluster_name: cluster_name, config: config, vault_client: client)
121
+ end
122
+ end
123
+
94
124
  def settings
95
125
  if Legion.const_defined?('Settings')
96
126
  Legion::Settings[:crypt]
@@ -112,7 +142,7 @@ module Legion
112
142
 
113
143
  Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default) if defined?(Legion::Transport::Settings)
114
144
 
115
- response = LeaseManager.instance.vault_logical.read('rabbitmq/creds/legionio-bootstrap')
145
+ response = LeaseManager.instance.vault_logical.read(path: 'rabbitmq/creds/legionio-bootstrap')
116
146
  return unless response&.data
117
147
 
118
148
  bootstrap_lease_ttl = Legion::Settings.dig(:crypt, :vault, :bootstrap_lease_ttl).to_i
@@ -147,10 +177,18 @@ module Legion
147
177
  return if mode == :lite
148
178
 
149
179
  role = RMQ_ROLE_MAP.fetch(mode, "legionio-#{mode}")
150
- response = LeaseManager.instance.vault_logical.read("rabbitmq/creds/#{role}")
151
- raise "Failed to fetch identity-scoped RMQ creds for role #{role}" unless response&.data
180
+ manager = LeaseManager.instance
181
+ response = Legion::Crypt::VaultOperation.with_timeout { manager.vault_logical.read(path: "rabbitmq/creds/#{role}") }
182
+ raise "Failed to fetch identity-scoped RMQ creds for role #{role}" unless response
152
183
 
153
- LeaseManager.instance.register_dynamic_lease(
184
+ settings = Legion::Settings.loader.settings
185
+ settings[:transport] ||= {}
186
+ settings[:transport][:connection] ||= {}
187
+ conn = settings[:transport][:connection]
188
+ conn[:user] = nil unless conn.key?(:user)
189
+ conn[:password] = nil unless conn.key?(:password)
190
+
191
+ installed = manager.register_dynamic_lease(
154
192
  name: :rabbitmq,
155
193
  path: "rabbitmq/creds/#{role}",
156
194
  response: response,
@@ -159,32 +197,15 @@ module Legion
159
197
  { path: %i[transport connection password], key: :password }
160
198
  ]
161
199
  )
200
+ raise 'Identity credential handoff failed — bootstrap lease NOT revoked' unless installed
162
201
 
163
- settings = Legion::Settings.loader.settings
164
- settings[:transport] ||= {}
165
- settings[:transport][:connection] ||= {}
166
- conn = settings[:transport][:connection]
167
- username = response.data[:username] || response.data['username']
168
- password = response.data[:password] || response.data['password']
169
- raise "Identity-scoped RMQ creds for role #{role} missing username or password" unless username && password
170
-
171
- conn[:user] = username
172
- conn[:password] = password
173
-
174
- if defined?(Legion::Transport::Connection)
175
- Legion::Transport::Connection.force_reconnect
176
- raise 'Transport reconnect failed after credential swap — bootstrap lease NOT revoked' unless Legion::Transport::Connection.session_open?
177
-
178
- log.info "Transport reconnected with identity-scoped creds (role: #{role})"
179
- end
180
-
181
- revoke_bootstrap_lease
202
+ Legion::Crypt::VaultOperation.with_timeout { revoke_bootstrap_lease }
182
203
  end
183
204
 
184
205
  def revoke_bootstrap_lease
185
206
  return unless @bootstrap_lease_id
186
207
 
187
- LeaseManager.instance.vault_sys.revoke(@bootstrap_lease_id)
208
+ LeaseManager.instance.vault_sys.revoke(lease_id: @bootstrap_lease_id)
188
209
  log.info "Bootstrap RMQ lease revoked (#{@bootstrap_lease_id[0..7]}...)"
189
210
  @bootstrap_lease_id = nil
190
211
  @bootstrap_lease_expires = nil
@@ -196,6 +217,8 @@ module Legion
196
217
  end
197
218
 
198
219
  def vault_connected?
220
+ return connected_clusters.any? if settings.dig(:vault, :clusters)&.any?
221
+
199
222
  return true if settings.dig(:vault, :connected) == true
200
223
  return true if respond_to?(:connected_clusters) && connected_clusters.any?
201
224
 
@@ -233,20 +256,43 @@ module Legion
233
256
  end
234
257
 
235
258
  def shutdown
236
- lifecycle_mutex.synchronize do
237
- unless @started
238
- log.info 'Legion::Crypt shutdown ignored because the lifecycle is not running'
239
- return
240
- end
259
+ shutdown_started = lifecycle_mutex.synchronize do
260
+ return if @stopping
261
+ return unless @started || @vault_clients || @retired_vault_clients&.size&.positive?
241
262
 
242
- log.info 'Legion::Crypt shutdown initiated'
243
- Legion::Crypt::LeaseManager.instance.shutdown
244
- stop_token_renewers
245
- shutdown_renewer
246
- close_sessions
247
- stop_svid_rotation
263
+ @stopping = true
264
+ @token_generation = (@token_generation || 0) + 1
248
265
  @started = false
249
- log.info 'Legion::Crypt shutdown completed'
266
+ true
267
+ end
268
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::VaultOperation::SHUTDOWN_TIMEOUT
269
+ manager = Legion::Crypt::LeaseManager.instance
270
+ [manager, *(@token_renewers || [])].each do |worker|
271
+ worker.request_stop
272
+ rescue StandardError => e
273
+ handle_exception(e, level: :warn, operation: 'crypt.shutdown.request_stop')
274
+ end
275
+
276
+ components = [
277
+ [self, :stop_svid_rotation, { deadline: deadline }],
278
+ [self, :shutdown_renewer, {}],
279
+ [manager, :shutdown, { deadline: deadline }],
280
+ [self, :stop_token_renewers, { deadline: deadline }],
281
+ [self, :close_sessions, { deadline: deadline }],
282
+ [self, :clear_vault_clients, { deadline: deadline }]
283
+ ]
284
+ components.each do |owner, operation, options|
285
+ owner.send(operation, **options)
286
+ rescue StandardError => e
287
+ handle_exception(e, level: :warn, operation: "crypt.shutdown.#{operation}")
288
+ end
289
+ log.info 'Legion::Crypt shutdown completed'
290
+ ensure
291
+ if shutdown_started
292
+ lifecycle_mutex.synchronize do
293
+ @started = false
294
+ @stopping = false
295
+ end
250
296
  end
251
297
  end
252
298
 
@@ -287,27 +333,34 @@ module Legion
287
333
 
288
334
  def start_token_renewers
289
335
  started = 0
290
- clusters.each do |name, config|
291
- next unless config[:auth_method]&.to_s == 'kerberos' && config[:connected]
292
-
293
- renewer = Legion::Crypt::TokenRenewer.new(
294
- cluster_name: name,
295
- config: config,
296
- vault_client: vault_client(name)
297
- )
298
- renewer.start
299
- @token_renewers << renewer
336
+ configurations = clusters.empty? ? { nil => vault_settings } : clusters
337
+ configurations.each do |name, config|
338
+ next unless %w[kerberos ldap jwt].include?(config[:auth_method].to_s) && config[:connected]
339
+
340
+ start_token_renewer(cluster_name: name, config: config, vault_client: vault_client(name))
300
341
  started += 1
301
342
  end
302
343
  log.info "Legion::Crypt started #{started} token renewer(s)" if started.positive?
303
344
  end
304
345
 
305
- def stop_token_renewers
306
- return unless @token_renewers
346
+ def start_token_renewer(cluster_name:, config:, vault_client:, **)
347
+ @token_renewers ||= []
348
+ return if @token_renewers.any? { |worker| worker.cluster_name == cluster_name && worker.running? }
307
349
 
308
- @token_renewers.each(&:stop)
309
- log.info "Legion::Crypt stopped #{@token_renewers.size} token renewer(s)" if @token_renewers.any?
310
- @token_renewers.clear
350
+ @token_renewers.reject! { |worker| worker.cluster_name == cluster_name }
351
+ renewer = Legion::Crypt::TokenRenewer.new(cluster_name: cluster_name, config: config, vault_client: vault_client)
352
+ @token_renewers << renewer
353
+ renewer.start
354
+ end
355
+
356
+ def stop_token_renewers(deadline: nil)
357
+ renewers = @token_renewers || []
358
+ @token_renewers = []
359
+ renewers.each do |renewer|
360
+ renewer.stop(deadline: deadline)
361
+ rescue StandardError => e
362
+ handle_exception(e, level: :warn, operation: 'crypt.stop_token_renewers')
363
+ end
311
364
  end
312
365
 
313
366
  def start_svid_rotation
@@ -320,11 +373,11 @@ module Legion
320
373
  handle_exception(e, level: :warn, operation: 'crypt.start_svid_rotation')
321
374
  end
322
375
 
323
- def stop_svid_rotation
376
+ def stop_svid_rotation(deadline: nil)
324
377
  return unless @svid_rotation
325
378
 
326
- log.info 'Legion::Crypt stopping SPIFFE SVID rotation'
327
- @svid_rotation.stop
379
+ Legion::Crypt::VaultOperation.with_timeout(deadline: deadline) { @svid_rotation.stop }
380
+ ensure
328
381
  @svid_rotation = nil
329
382
  end
330
383
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-crypt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.13
4
+ version: 1.5.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -38,61 +38,61 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.3'
40
40
  - !ruby/object:Gem::Dependency
41
- name: jwt
41
+ name: faraday
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.7'
46
+ version: '2.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - ">="
51
+ - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '2.7'
53
+ version: '2.0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: legion-json
55
+ name: jwt
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 1.2.0
60
+ version: '2.7'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: 1.2.0
67
+ version: '2.7'
68
68
  - !ruby/object:Gem::Dependency
69
- name: legion-logging
69
+ name: legion-json
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 1.5.0
74
+ version: 1.2.0
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 1.5.0
81
+ version: 1.2.0
82
82
  - !ruby/object:Gem::Dependency
83
- name: vault
83
+ name: legion-logging
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: '0.17'
88
+ version: 1.5.0
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '0.17'
95
+ version: 1.5.0
96
96
  description: A gem used by the LegionIO framework for encryption
97
97
  email:
98
98
  - matthewdiverson@gmail.com
@@ -141,10 +141,12 @@ files:
141
141
  - lib/legion/crypt/tls.rb
142
142
  - lib/legion/crypt/token_renewer.rb
143
143
  - lib/legion/crypt/vault.rb
144
+ - lib/legion/crypt/vault_client.rb
144
145
  - lib/legion/crypt/vault_cluster.rb
145
146
  - lib/legion/crypt/vault_entity.rb
146
147
  - lib/legion/crypt/vault_jwt_auth.rb
147
148
  - lib/legion/crypt/vault_kerberos_auth.rb
149
+ - lib/legion/crypt/vault_operation.rb
148
150
  - lib/legion/crypt/vault_renewer.rb
149
151
  - lib/legion/crypt/version.rb
150
152
  - scripts/pre-commit-rubocop.sh