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.
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.12
4
+ version: 1.5.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: faraday
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: jwt
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -52,33 +66,33 @@ dependencies:
52
66
  - !ruby/object:Gem::Version
53
67
  version: '2.7'
54
68
  - !ruby/object:Gem::Dependency
55
- name: legion-logging
69
+ name: legion-json
56
70
  requirement: !ruby/object:Gem::Requirement
57
71
  requirements:
58
72
  - - ">="
59
73
  - !ruby/object:Gem::Version
60
- version: 1.5.0
74
+ version: 1.2.0
61
75
  type: :runtime
62
76
  prerelease: false
63
77
  version_requirements: !ruby/object:Gem::Requirement
64
78
  requirements:
65
79
  - - ">="
66
80
  - !ruby/object:Gem::Version
67
- version: 1.5.0
81
+ version: 1.2.0
68
82
  - !ruby/object:Gem::Dependency
69
- name: vault
83
+ name: legion-logging
70
84
  requirement: !ruby/object:Gem::Requirement
71
85
  requirements:
72
86
  - - ">="
73
87
  - !ruby/object:Gem::Version
74
- version: '0.17'
88
+ version: 1.5.0
75
89
  type: :runtime
76
90
  prerelease: false
77
91
  version_requirements: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - ">="
80
94
  - !ruby/object:Gem::Version
81
- version: '0.17'
95
+ version: 1.5.0
82
96
  description: A gem used by the LegionIO framework for encryption
83
97
  email:
84
98
  - matthewdiverson@gmail.com
@@ -127,14 +141,14 @@ files:
127
141
  - lib/legion/crypt/tls.rb
128
142
  - lib/legion/crypt/token_renewer.rb
129
143
  - lib/legion/crypt/vault.rb
144
+ - lib/legion/crypt/vault_client.rb
130
145
  - lib/legion/crypt/vault_cluster.rb
131
146
  - lib/legion/crypt/vault_entity.rb
132
147
  - lib/legion/crypt/vault_jwt_auth.rb
133
148
  - lib/legion/crypt/vault_kerberos_auth.rb
149
+ - lib/legion/crypt/vault_operation.rb
134
150
  - lib/legion/crypt/vault_renewer.rb
135
151
  - lib/legion/crypt/version.rb
136
- - lib/legion/logging.rb
137
- - lib/legion/logging/helper.rb
138
152
  - scripts/pre-commit-rubocop.sh
139
153
  - sonar-project.properties
140
154
  homepage: https://github.com/LegionIO/legion-crypt
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- begin
4
- helper_path = File.join(
5
- Gem::Specification.find_by_name('legion-logging').full_gem_path,
6
- 'lib/legion/logging/helper.rb'
7
- )
8
- require helper_path if File.exist?(helper_path)
9
- rescue Gem::LoadError => e
10
- require 'legion/logging'
11
- Legion::Logging.warn("legion-crypt logging helper fallback active: #{e.message}")
12
- end
13
-
14
- require 'legion/logging'
15
-
16
- module Legion
17
- module Logging
18
- module Helper
19
- unless const_defined?(:CompatLogger, false)
20
- CompatLogger = Class.new do
21
- %i[debug info warn error fatal unknown].each do |level|
22
- define_method(level) do |message = nil, &block|
23
- payload = block ? block.call : message
24
- return if payload.nil?
25
-
26
- if logging_supports?(level)
27
- Legion::Logging.public_send(level, payload)
28
- elsif %i[error fatal warn].include?(level)
29
- ::Kernel.warn(payload)
30
- else
31
- $stdout.puts(payload)
32
- end
33
- end
34
- end
35
-
36
- private
37
-
38
- def logging_supports?(level)
39
- return false unless Legion.const_defined?('Logging')
40
-
41
- Legion::Logging.respond_to?(level)
42
- rescue StandardError => e
43
- Legion::Logging.warn("legion-crypt logging support check failed: #{e.message}")
44
- false
45
- end
46
- end
47
- end
48
-
49
- def log
50
- @log ||= CompatLogger.new
51
- end
52
-
53
- def handle_exception(exception, task_id: nil, level: :error, handled: true, **opts) # rubocop:disable Lint/UnusedMethodArgument,Style/ArgumentsForwarding
54
- message = exception_log_message(exception, level: level, **opts) # rubocop:disable Style/ArgumentsForwarding
55
-
56
- if logging_supports?(:log_exception)
57
- Legion::Logging.log_exception(exception, level: level, lex: 'crypt', component_type: :helper)
58
- return
59
- end
60
- if logging_supports?(level)
61
- Legion::Logging.public_send(level, message)
62
- return
63
- end
64
- if logging_supports?(:error)
65
- Legion::Logging.error(message)
66
- return
67
- end
68
- if logging_supports?(:warn)
69
- Legion::Logging.warn(message)
70
- return
71
- end
72
-
73
- ::Kernel.warn(message)
74
- end
75
-
76
- private
77
-
78
- def logging_supports?(level)
79
- return false unless Legion.const_defined?('Logging')
80
-
81
- Legion::Logging.respond_to?(level)
82
- rescue StandardError => e
83
- Legion::Logging.warn("legion-crypt logging support check failed: #{e.message}")
84
- false
85
- end
86
-
87
- def exception_log_message(exception, level:, **opts)
88
- operation = opts[:operation] || opts['operation']
89
- prefix = operation ? "#{operation} failed: " : ''
90
- details = opts.reject { |key, _value| key.to_s == 'operation' }.map { |key, value| "#{key}=#{value}" }
91
- detail_suffix = details.empty? ? '' : " (#{details.join(' ')})"
92
- backtrace = Array(exception.backtrace).join("\n")
93
- base = "#{prefix}#{exception.class}: #{exception.message}#{detail_suffix}"
94
- return base if backtrace.empty? || level == :debug
95
- return base if backtrace.empty?
96
-
97
- "#{base}\n#{backtrace}"
98
- end
99
- end
100
- end
101
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'logger'
4
-
5
- begin
6
- gem_root = Gem::Specification.find_by_name('legion-logging').full_gem_path
7
- upstream_logging = File.join(gem_root, 'lib/legion/logging.rb')
8
- require upstream_logging if File.exist?(upstream_logging)
9
- rescue Gem::LoadError
10
- nil
11
- end
12
-
13
- module Legion
14
- module Logging
15
- class << self
16
- unless method_defined?(:setup)
17
- def setup(level: 'info', **_opts)
18
- logger.level = normalize_level(level)
19
- self
20
- end
21
-
22
- def logger
23
- @logger ||= Logger.new($stdout).tap do |instance|
24
- instance.progname = 'legion-crypt'
25
- end
26
- end
27
-
28
- def log_exception(exception, lex: nil, component_type: nil, **_opts)
29
- prefix = [lex, component_type].compact.join('.')
30
- payload = prefix.empty? ? exception.message : "#{prefix}: #{exception.message}"
31
- error(payload)
32
- end
33
-
34
- %i[debug info warn error fatal unknown].each do |level_name|
35
- define_method(level_name) do |message = nil, &block|
36
- payload = block ? block.call : message
37
- return if payload.nil?
38
-
39
- logger.public_send(level_name, payload)
40
- end
41
- end
42
-
43
- private
44
-
45
- def normalize_level(level)
46
- case level.to_s.downcase
47
- when 'debug' then Logger::DEBUG
48
- when 'info' then Logger::INFO
49
- when 'warn' then Logger::WARN
50
- when 'error' then Logger::ERROR
51
- when 'fatal' then Logger::FATAL
52
- else Logger::UNKNOWN
53
- end
54
- end
55
- end
56
- end
57
- end
58
- end