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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -3
  3. data/.rspec +2 -0
  4. data/AGENTS.md +2 -0
  5. data/CHANGELOG.md +28 -0
  6. data/CLAUDE.md +64 -192
  7. data/CODEOWNERS +4 -4
  8. data/Gemfile +4 -1
  9. data/README.md +60 -16
  10. data/legion-crypt.gemspec +1 -0
  11. data/lib/legion/crypt/cert_rotation.rb +7 -21
  12. data/lib/legion/crypt/cluster_secret.rb +3 -14
  13. data/lib/legion/crypt/ed25519.rb +2 -10
  14. data/lib/legion/crypt/erasure.rb +3 -13
  15. data/lib/legion/crypt/jwks_client.rb +4 -4
  16. data/lib/legion/crypt/kerberos_auth.rb +2 -2
  17. data/lib/legion/crypt/ldap_auth.rb +2 -2
  18. data/lib/legion/crypt/lease_manager.rb +7 -7
  19. data/lib/legion/crypt/mtls.rb +8 -46
  20. data/lib/legion/crypt/partition_keys.rb +2 -6
  21. data/lib/legion/crypt/settings.rb +24 -21
  22. data/lib/legion/crypt/spiffe/svid_rotation.rb +6 -9
  23. data/lib/legion/crypt/spiffe.rb +14 -39
  24. data/lib/legion/crypt/token_renewer.rb +6 -6
  25. data/lib/legion/crypt/vault/client.rb +355 -0
  26. data/lib/legion/crypt/vault/cluster.rb +330 -0
  27. data/lib/legion/crypt/vault/entity.rb +80 -0
  28. data/lib/legion/crypt/vault/jwt_auth.rb +99 -0
  29. data/lib/legion/crypt/vault/kerberos_auth.rb +53 -0
  30. data/lib/legion/crypt/vault/operation.rb +29 -0
  31. data/lib/legion/crypt/vault.rb +6 -11
  32. data/lib/legion/crypt/version.rb +1 -1
  33. data/lib/legion/crypt.rb +69 -80
  34. metadata +23 -8
  35. data/lib/legion/crypt/vault_client.rb +0 -355
  36. data/lib/legion/crypt/vault_cluster.rb +0 -331
  37. data/lib/legion/crypt/vault_entity.rb +0 -78
  38. data/lib/legion/crypt/vault_jwt_auth.rb +0 -97
  39. data/lib/legion/crypt/vault_kerberos_auth.rb +0 -51
  40. data/lib/legion/crypt/vault_operation.rb +0 -27
  41. /data/lib/legion/crypt/{vault_renewer.rb → vault/renewer.rb} +0 -0
@@ -0,0 +1,80 @@
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 Vault
10
+ module Entity
11
+ extend Legion::Logging::Helper
12
+
13
+ # Create or lookup a Vault entity for a Legion principal.
14
+ # Returns the Vault entity ID string, or nil on failure.
15
+ def self.ensure_entity(principal_id:, canonical_name:, metadata: {})
16
+ existing = find_by_name(canonical_name: canonical_name)
17
+ return existing if existing
18
+
19
+ response = Legion::Crypt::LeaseManager.instance.vault_logical.write(
20
+ path: 'identity/entity',
21
+ data: {
22
+ name: "legion-#{canonical_name}",
23
+ metadata: metadata.merge(
24
+ legion_principal_id: principal_id,
25
+ legion_canonical_name: canonical_name,
26
+ managed_by: 'legion'
27
+ )
28
+ }
29
+ )
30
+ extract_id(response)
31
+ rescue StandardError => e
32
+ handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_entity', canonical_name: canonical_name)
33
+ nil
34
+ end
35
+
36
+ # Create an alias linking an auth method mount to the entity.
37
+ # Idempotent — swallows "already exists" 4xx errors.
38
+ def self.ensure_alias(entity_id:, mount_accessor:, alias_name:)
39
+ Legion::Crypt::LeaseManager.instance.vault_logical.write(
40
+ path: 'identity/entity-alias',
41
+ data: { name: alias_name, canonical_id: entity_id, mount_accessor: mount_accessor }
42
+ )
43
+ rescue Legion::Crypt::Vault::HTTPClientError => e
44
+ if e.message.include?('already exists')
45
+ log.debug 'Vault entity alias already exists (idempotent)'
46
+ else
47
+ handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_alias', alias_name: alias_name)
48
+ end
49
+ nil
50
+ rescue StandardError => e
51
+ handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_alias', alias_name: alias_name)
52
+ nil
53
+ end
54
+
55
+ # Look up a Vault entity by its Legion canonical name.
56
+ # Returns the Vault entity ID string, or nil if not found.
57
+ def self.find_by_name(canonical_name:)
58
+ response = Legion::Crypt::LeaseManager.instance.vault_logical.read(path: "identity/entity/name/legion-#{canonical_name}")
59
+ extract_id(response)
60
+ rescue StandardError => e
61
+ handle_exception(e, level: :warn, operation: 'crypt.vault_entity.find_by_name', canonical_name: canonical_name)
62
+ nil
63
+ end
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Private helpers
67
+ # ---------------------------------------------------------------------------
68
+
69
+ # Extract the entity ID from the client's symbol-keyed response data.
70
+ def self.extract_id(response)
71
+ data = response&.data
72
+ return nil unless data
73
+
74
+ data[:id]
75
+ end
76
+ private_class_method :extract_id
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,99 @@
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 Vault
19
+ module JwtAuth
20
+ DEFAULT_AUTH_PATH = 'auth/jwt/login'
21
+ DEFAULT_ROLE = 'legion-worker'
22
+
23
+ class AuthError < StandardError; end
24
+
25
+ extend Legion::Logging::Helper
26
+
27
+ # Authenticate to Vault using a JWT token.
28
+ # Returns a Vault token string on success.
29
+ #
30
+ # @param jwt [String] Signed JWT token (issued by Legion or Entra ID)
31
+ # @param role [String] Vault JWT auth role name (default: 'legion-worker')
32
+ # @param auth_path [String] Vault auth mount path (default: 'auth/jwt/login')
33
+ # @return [Hash] { token:, lease_duration:, policies:, metadata: }
34
+ def self.login(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
35
+ raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
36
+
37
+ log.info "[crypt:vault_jwt] authenticating role=#{role} auth_path=#{auth_path}"
38
+ response = Legion::Crypt.vault_client.logical.write(path: auth_path, data: { role: role, jwt: jwt })
39
+
40
+ raise AuthError, 'Vault JWT auth returned no auth data' unless response&.auth
41
+
42
+ {
43
+ token: response.auth.client_token,
44
+ lease_duration: response.auth.lease_duration,
45
+ renewable: response.auth.renewable?,
46
+ policies: response.auth.policies,
47
+ metadata: response.auth.metadata
48
+ }
49
+ rescue Legion::Crypt::Vault::HTTPClientError => e
50
+ handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
51
+ category: 'client_error')
52
+ raise AuthError, "Vault JWT auth failed: #{e.message}"
53
+ rescue Legion::Crypt::Vault::HTTPServerError => e
54
+ handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
55
+ category: 'server_error')
56
+ raise AuthError, "Vault server error during JWT auth: #{e.message}"
57
+ rescue StandardError => e
58
+ handle_exception(e, level: :error, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path)
59
+ raise if e.is_a?(AuthError)
60
+
61
+ raise AuthError, "Vault JWT auth failed: #{e.message}"
62
+ end
63
+
64
+ # Authenticate and set the Vault client token for subsequent operations.
65
+ # This replaces the current Vault token with the JWT-authenticated one.
66
+ #
67
+ # @return [Hash] Same as login
68
+ def self.login!(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
69
+ generation = Legion::Crypt.vault_auth_generation
70
+ client = Legion::Crypt.vault_client
71
+ config = Legion::Crypt.cluster || Legion::Settings[:crypt][:vault]
72
+ result = login(jwt: jwt, role: role, auth_path: auth_path)
73
+ Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'jwt', generation: generation)
74
+ log.info "[crypt:vault_jwt] authenticated via JWT auth, policies=#{result[:policies].join(',')}"
75
+ result
76
+ end
77
+
78
+ # Issue a Legion JWT and use it to authenticate to Vault in one step.
79
+ # Convenience method for workers that need Vault access.
80
+ #
81
+ # @param worker_id [String] Digital worker ID
82
+ # @param owner_msid [String] Worker's owner MSID
83
+ # @param role [String] Vault JWT auth role name
84
+ # @return [Hash] Same as login
85
+ def self.worker_login(worker_id:, owner_msid:, role: DEFAULT_ROLE)
86
+ log.info "[crypt:vault_jwt] worker login requested role=#{role} worker_id=#{worker_id}"
87
+ jwt = Legion::Crypt::JWT.issue(
88
+ { worker_id: worker_id, sub: owner_msid, scope: 'vault', aud: 'legion' },
89
+ signing_key: Legion::Crypt.cluster_secret,
90
+ ttl: 300,
91
+ issuer: 'legion'
92
+ )
93
+
94
+ login(jwt: jwt, role: role)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,53 @@
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 Vault
9
+ module KerberosAuth
10
+ DEFAULT_AUTH_PATH = 'auth/kerberos/login'
11
+
12
+ class AuthError < StandardError; end
13
+
14
+ extend Legion::Logging::Helper
15
+
16
+ def self.login(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
17
+ raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
18
+
19
+ log.info "[crypt:vault_kerberos] login requested auth_path=#{auth_path}"
20
+ data = Legion::Crypt.vault_client.put(
21
+ path: auth_path, body: '{}', headers: { 'Authorization' => "Negotiate #{spnego_token}" }
22
+ )
23
+ response = Legion::Crypt::Vault::Client::Secret.decode(data: data)
24
+ raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
25
+
26
+ {
27
+ token: response.auth.client_token,
28
+ lease_duration: response.auth.lease_duration,
29
+ renewable: response.auth.renewable?,
30
+ policies: response.auth.policies,
31
+ metadata: response.auth.metadata
32
+ }
33
+ rescue Legion::Crypt::Vault::HTTPClientError => e
34
+ handle_exception(e, level: :warn, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
35
+ raise AuthError, "Vault Kerberos auth failed: #{e.message}"
36
+ rescue StandardError => e
37
+ handle_exception(e, level: :error, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
38
+ raise
39
+ end
40
+
41
+ def self.login!(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
42
+ generation = Legion::Crypt.vault_auth_generation
43
+ client = Legion::Crypt.vault_client
44
+ config = Legion::Crypt.cluster || Legion::Settings[:crypt][:vault]
45
+ result = login(spnego_token: spnego_token, auth_path: auth_path)
46
+ Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'kerberos', generation: generation)
47
+ log.info "[crypt:vault_kerberos] authenticated via Kerberos auth, policies=#{result[:policies].join(',')}"
48
+ result
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+ require 'legion/crypt/settings'
5
+
6
+ module Legion
7
+ module Crypt
8
+ module Vault
9
+ module Operation
10
+ DEFAULT_TIMEOUT = 5
11
+ SHUTDOWN_TIMEOUT = 10
12
+
13
+ def self.with_timeout(deadline: nil, **)
14
+ timeout = Legion::Settings[:crypt][:vault][:operation_timeout]
15
+ raise ArgumentError, 'Vault operation_timeout must be a finite positive number' unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
16
+
17
+ if deadline
18
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
19
+ raise Timeout::Error, 'Vault operation deadline expired' unless remaining.positive?
20
+
21
+ timeout = [timeout, remaining].min
22
+ end
23
+ operation_deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
24
+ Timeout.timeout(timeout) { yield(operation_deadline) }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/logging/helper'
4
- require 'legion/crypt/vault_operation'
5
- require 'legion/crypt/vault_client'
4
+ require 'legion/crypt/vault/operation'
5
+ require 'legion/crypt/vault/client'
6
6
 
7
7
  module Legion
8
8
  module Crypt
@@ -11,15 +11,10 @@ module Legion
11
11
 
12
12
  attr_accessor :sessions
13
13
 
14
- def settings
15
- Legion::Settings[:crypt][:vault]
16
- end
17
-
18
14
  def connect_vault
19
15
  return unless start_session_tracking
20
16
 
21
17
  configuration = Legion::Settings[:crypt][:vault]
22
- configuration[:token] = ENV['VAULT_DEV_ROOT_TOKEN_ID'] if ENV.key? 'VAULT_DEV_ROOT_TOKEN_ID'
23
18
  return nil if configuration[:token].nil?
24
19
 
25
20
  client = vault_client
@@ -119,7 +114,7 @@ module Legion
119
114
  end
120
115
 
121
116
  def close_sessions(deadline: nil)
122
- deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + VaultOperation::SHUTDOWN_TIMEOUT
117
+ deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::Vault::Operation::SHUTDOWN_TIMEOUT
123
118
  pending = session_mutex.synchronize do
124
119
  @sessions_stopped = true
125
120
  @session_generation = (@session_generation || 0) + 1
@@ -132,7 +127,7 @@ module Legion
132
127
  pending.each do |session|
133
128
  break if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) >= deadline
134
129
 
135
- VaultOperation.with_timeout(deadline: deadline) { close_session(session: session) }
130
+ Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { close_session(session: session) }
136
131
  closed << session
137
132
  rescue StandardError => e
138
133
  handle_exception(e, level: :error, operation: 'crypt.vault.close_session')
@@ -175,7 +170,7 @@ module Legion
175
170
  pending.each do |session|
176
171
  break unless session_maintenance_active?(generation: generation)
177
172
 
178
- VaultOperation.with_timeout { renew_session(session: session) }
173
+ Legion::Crypt::Vault::Operation.with_timeout { renew_session(session: session) }
179
174
  renewed << session
180
175
  rescue StandardError => e
181
176
  handle_exception(e, level: :error, operation: 'crypt.vault.renew_session')
@@ -192,7 +187,7 @@ module Legion
192
187
  connected_clusters.each_key do |name|
193
188
  break unless session_maintenance_active?(generation: generation)
194
189
 
195
- VaultOperation.with_timeout { vault_client(name).auth_token.renew_self }
190
+ Legion::Crypt::Vault::Operation.with_timeout { vault_client(name).auth_token.renew_self }
196
191
  renewed << name
197
192
  log.info "Vault token renewed for cluster #{name}"
198
193
  rescue StandardError => e
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.5.38'
5
+ VERSION = '1.5.42'
6
6
  end
7
7
  end
data/lib/legion/crypt.rb CHANGED
@@ -3,14 +3,16 @@
3
3
  require 'openssl'
4
4
  require 'base64'
5
5
  require 'legion/logging/helper'
6
+ require 'legion/settings'
7
+
6
8
  require 'legion/crypt/version'
7
9
  require 'legion/crypt/settings'
8
10
  require 'legion/crypt/cipher'
9
11
  require 'legion/crypt/jwt'
10
- require 'legion/crypt/vault_jwt_auth'
12
+ require 'legion/crypt/vault/jwt_auth'
11
13
  require 'legion/crypt/lease_manager'
12
- require 'legion/crypt/vault_operation'
13
- require 'legion/crypt/vault_cluster'
14
+ require 'legion/crypt/vault/operation'
15
+ require 'legion/crypt/vault/cluster'
14
16
  require 'legion/crypt/vault'
15
17
  require 'legion/crypt/ldap_auth'
16
18
  require 'legion/crypt/token_renewer'
@@ -18,13 +20,10 @@ require 'legion/crypt/helper'
18
20
  require 'legion/crypt/mtls'
19
21
  require 'legion/crypt/cert_rotation'
20
22
  require 'legion/crypt/spiffe'
21
- require 'legion/crypt/spiffe/workload_api_client'
22
- require 'legion/crypt/spiffe/svid_rotation'
23
- require 'legion/crypt/spiffe/identity_helpers'
24
23
 
25
24
  module Legion
26
25
  module Crypt
27
- extend Legion::Crypt::VaultCluster
26
+ extend Legion::Crypt::Vault::Cluster
28
27
  extend Legion::Crypt::LdapAuth
29
28
 
30
29
  RMQ_ROLE_MAP = {
@@ -33,6 +32,10 @@ module Legion
33
32
  infra: 'legionio-infra'
34
33
  }.freeze
35
34
 
35
+ POSTGRES_ROLE_MAP = {
36
+ infra: 'infra'
37
+ }.freeze
38
+
36
39
  class << self
37
40
  attr_reader :sessions, :stopping
38
41
 
@@ -41,29 +44,25 @@ module Legion
41
44
 
42
45
  include Legion::Crypt::Vault
43
46
 
44
- def vault_settings
45
- Legion::Settings[:crypt][:vault]
46
- end
47
-
48
- def kerberos_principal
49
- KerberosAuth.kerberos_principal
47
+ def kerberos_principal(**)
48
+ Legion::Crypt::KerberosAuth.kerberos_principal
50
49
  end
51
50
 
52
51
  def spiffe_svid
53
52
  @svid_rotation&.current_svid
54
53
  end
55
54
 
56
- def fetch_svid
57
- @workload_client ||= Spiffe::WorkloadApiClient.new
55
+ def fetch_svid(**)
56
+ @workload_client ||= Legion::Crypt::Spiffe::WorkloadApiClient.new
58
57
  @workload_client.fetch_x509_svid
59
58
  end
60
59
 
61
- def fetch_jwt_svid(audience:)
62
- @workload_client ||= Spiffe::WorkloadApiClient.new
60
+ def fetch_jwt_svid(audience:, **)
61
+ @workload_client ||= Legion::Crypt::Spiffe::WorkloadApiClient.new
63
62
  @workload_client.fetch_jwt_svid(audience: audience)
64
63
  end
65
64
 
66
- def start
65
+ def start(mode: nil, **)
67
66
  lifecycle_mutex.synchronize do
68
67
  if @started || @stopping
69
68
  log.info 'Legion::Crypt start ignored because the lifecycle is running or stopping'
@@ -72,7 +71,7 @@ module Legion
72
71
 
73
72
  log.info 'Legion::Crypt startup initiated'
74
73
  log.debug 'Legion::Crypt start requested'
75
- if settings[:save_private_key]
74
+ if Legion::Settings[:crypt][:save_private_key]
76
75
  key = private_key
77
76
  ::File.open('./legionio.key', ::File::WRONLY | ::File::CREAT, 0o600) do |file|
78
77
  file.chmod(0o600)
@@ -83,15 +82,15 @@ module Legion
83
82
  @token_renewers ||= []
84
83
  start_session_tracking
85
84
 
86
- if vault_settings[:clusters]&.any?
87
- log.info "Legion::Crypt connecting #{vault_settings[:clusters].size} Vault cluster(s)"
85
+ if Legion::Settings[:crypt][:vault][:clusters].any?
86
+ log.info "Legion::Crypt connecting #{Legion::Settings[:crypt][:vault][:clusters].size} Vault cluster(s)"
88
87
  connect_all_clusters
89
88
  else
90
- log.info 'Legion::Crypt connecting primary Vault client' unless settings[:vault][:token].nil?
91
- connect_vault unless settings[:vault][:token].nil?
89
+ log.info 'Legion::Crypt connecting primary Vault client' unless Legion::Settings[:crypt][:vault][:token].nil?
90
+ connect_vault unless Legion::Settings[:crypt][:vault][:token].nil?
92
91
  end
93
92
  start_token_renewers
94
- start_lease_manager
93
+ start_lease_manager(mode: mode)
95
94
  start_svid_rotation
96
95
  @started = true
97
96
  log.info 'Legion::Crypt startup completed'
@@ -121,31 +120,19 @@ module Legion
121
120
  end
122
121
  end
123
122
 
124
- def settings
125
- if Legion.const_defined?('Settings')
126
- Legion::Settings[:crypt]
127
- else
128
- Legion::Crypt::Settings.default
129
- end
130
- end
131
-
132
- def jwt_settings
133
- settings[:jwt] || Legion::Crypt::Settings.jwt
134
- end
135
-
136
123
  def dynamic_rmq_creds?
137
- Legion::Settings.dig(:crypt, :vault, :dynamic_rmq_creds) == true
124
+ Legion::Settings[:crypt][:vault][:dynamic_rmq_creds] == true
138
125
  end
139
126
 
140
- def fetch_bootstrap_rmq_creds
127
+ def fetch_bootstrap_rmq_creds(**)
141
128
  return unless vault_connected? && dynamic_rmq_creds?
142
129
 
143
130
  Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default) if defined?(Legion::Transport::Settings)
144
131
 
145
- response = LeaseManager.instance.vault_logical.read(path: 'rabbitmq/creds/legionio-bootstrap')
132
+ response = Legion::Crypt::LeaseManager.instance.vault_logical.read(path: 'rabbitmq/creds/legionio-bootstrap')
146
133
  return unless response&.data
147
134
 
148
- bootstrap_lease_ttl = Legion::Settings.dig(:crypt, :vault, :bootstrap_lease_ttl).to_i
135
+ bootstrap_lease_ttl = Legion::Settings[:crypt][:vault][:bootstrap_lease_ttl].to_i
149
136
  bootstrap_lease_ttl = 300 if bootstrap_lease_ttl <= 0
150
137
 
151
138
  @bootstrap_lease_id = response.lease_id
@@ -167,18 +154,17 @@ module Legion
167
154
  conn[:password] = password
168
155
 
169
156
  log.info "Bootstrap RMQ credentials acquired (lease: #{@bootstrap_lease_id[0..7]}...)"
170
- rescue StandardError => e
157
+ rescue IOError, SystemCallError, Timeout::Error, Faraday::Error, Legion::Crypt::Vault::HTTPError => e
171
158
  handle_exception(e, level: :warn, operation: 'crypt.fetch_bootstrap_rmq_creds')
172
- log.warn "Bootstrap RMQ credential fetch failed: #{e.message}"
173
159
  end
174
160
 
175
- def swap_to_identity_creds(mode:)
161
+ def swap_to_identity_creds(mode:, **)
176
162
  return unless vault_connected? && dynamic_rmq_creds?
177
163
  return if mode == :lite
178
164
 
179
165
  role = RMQ_ROLE_MAP.fetch(mode, "legionio-#{mode}")
180
- manager = LeaseManager.instance
181
- response = Legion::Crypt::VaultOperation.with_timeout { manager.vault_logical.read(path: "rabbitmq/creds/#{role}") }
166
+ manager = Legion::Crypt::LeaseManager.instance
167
+ response = Legion::Crypt::Vault::Operation.with_timeout { manager.vault_logical.read(path: "rabbitmq/creds/#{role}") }
182
168
  raise "Failed to fetch identity-scoped RMQ creds for role #{role}" unless response
183
169
 
184
170
  settings = Legion::Settings.loader.settings
@@ -199,56 +185,47 @@ module Legion
199
185
  )
200
186
  raise 'Identity credential handoff failed — bootstrap lease NOT revoked' unless installed
201
187
 
202
- Legion::Crypt::VaultOperation.with_timeout { revoke_bootstrap_lease }
188
+ Legion::Crypt::Vault::Operation.with_timeout { revoke_bootstrap_lease }
203
189
  end
204
190
 
205
- def revoke_bootstrap_lease
191
+ def revoke_bootstrap_lease(**)
206
192
  return unless @bootstrap_lease_id
207
193
 
208
- LeaseManager.instance.vault_sys.revoke(lease_id: @bootstrap_lease_id)
194
+ Legion::Crypt::LeaseManager.instance.vault_sys.revoke(lease_id: @bootstrap_lease_id)
209
195
  log.info "Bootstrap RMQ lease revoked (#{@bootstrap_lease_id[0..7]}...)"
210
196
  @bootstrap_lease_id = nil
211
197
  @bootstrap_lease_expires = nil
212
- rescue StandardError => e
198
+ rescue IOError, SystemCallError, Timeout::Error, Faraday::Error, Legion::Crypt::Vault::HTTPError => e
213
199
  handle_exception(e, level: :warn, operation: 'crypt.revoke_bootstrap_lease')
214
- log.warn "Bootstrap lease revocation failed: #{e.message} — lease will expire naturally"
215
200
  @bootstrap_lease_id = nil
216
201
  @bootstrap_lease_expires = nil
217
202
  end
218
203
 
219
- def vault_connected?
220
- return connected_clusters.any? if settings.dig(:vault, :clusters)&.any?
204
+ def vault_connected?(**)
205
+ return connected_clusters.any? if Legion::Settings[:crypt][:vault][:clusters].any?
221
206
 
222
- return true if settings.dig(:vault, :connected) == true
223
- return true if respond_to?(:connected_clusters) && connected_clusters.any?
224
-
225
- false
226
- rescue StandardError => e
227
- handle_exception(e, level: :debug, operation: 'crypt.vault_connected?')
228
- false
207
+ Legion::Settings[:crypt][:vault][:connected] == true
229
208
  end
230
209
 
231
210
  def issue_token(payload = {}, ttl: nil, algorithm: nil)
232
- jwt = jwt_settings
211
+ jwt = Legion::Settings[:crypt][:jwt]
233
212
  algo = algorithm || jwt[:default_algorithm]
234
213
  token_ttl = ttl || jwt[:default_ttl]
214
+ signing_key = algo == 'RS256' ? private_key : Legion::Settings[:crypt][:cluster_secret]
235
215
 
236
- signing_key = algo == 'RS256' ? private_key : settings[:cluster_secret]
237
-
238
- Legion::Crypt::JWT.issue(payload, signing_key: signing_key, algorithm: algo, ttl: token_ttl,
239
- issuer: jwt[:issuer])
216
+ Legion::Crypt::JWT.issue(payload, signing_key: signing_key, algorithm: algo, ttl: token_ttl, issuer: jwt[:issuer])
240
217
  end
241
218
 
242
219
  def verify_token(token, algorithm: nil)
243
- jwt = jwt_settings
220
+ jwt = Legion::Settings[:crypt][:jwt]
244
221
  algo = algorithm || jwt[:default_algorithm]
245
222
 
246
- verification_key = algo == 'RS256' ? OpenSSL::PKey::RSA.new(public_key) : settings[:cluster_secret]
223
+ verification_key = algo == 'RS256' ? OpenSSL::PKey::RSA.new(public_key) : Legion::Settings[:crypt][:cluster_secret]
247
224
 
248
225
  Legion::Crypt::JWT.verify(token, verification_key: verification_key, algorithm: algo,
249
- verify_expiration: jwt[:verify_expiration],
250
- verify_issuer: jwt[:verify_issuer],
251
- issuer: jwt[:issuer])
226
+ verify_expiration: jwt[:verify_expiration],
227
+ verify_issuer: jwt[:verify_issuer],
228
+ issuer: jwt[:issuer])
252
229
  end
253
230
 
254
231
  def verify_external_token(token, jwks_url:, **)
@@ -265,7 +242,7 @@ module Legion
265
242
  @started = false
266
243
  true
267
244
  end
268
- deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::VaultOperation::SHUTDOWN_TIMEOUT
245
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::Vault::Operation::SHUTDOWN_TIMEOUT
269
246
  manager = Legion::Crypt::LeaseManager.instance
270
247
  [manager, *(@token_renewers || [])].each do |worker|
271
248
  worker.request_stop
@@ -298,9 +275,9 @@ module Legion
298
275
 
299
276
  private
300
277
 
301
- def start_lease_manager
302
- leases = settings.dig(:vault, :leases) || {}
303
- vault_ok = connected_clusters.any? || settings.dig(:vault, :connected)
278
+ def start_lease_manager(mode: nil)
279
+ leases = postgresql_leases_for_mode(leases: Legion::Settings[:crypt][:vault][:leases], mode: mode)
280
+ vault_ok = connected_clusters.any? || Legion::Settings[:crypt][:vault][:connected]
304
281
 
305
282
  return if leases.empty? && !dynamic_rmq_creds?
306
283
  return unless vault_ok
@@ -310,7 +287,7 @@ module Legion
310
287
  if connected_clusters.any?
311
288
  selected_cluster = selected_connected_cluster_name
312
289
  client = selected_cluster ? vault_client(selected_cluster) : nil
313
- elsif settings.dig(:vault, :connected)
290
+ elsif Legion::Settings[:crypt][:vault][:connected]
314
291
  client = vault_client
315
292
  end
316
293
 
@@ -327,13 +304,25 @@ module Legion
327
304
  log.warn "LeaseManager: #{fetched}/#{defined} lease(s) initialized (#{defined - fetched} failed)"
328
305
  end
329
306
  end
330
- rescue StandardError => e
307
+ rescue IOError, SystemCallError, Timeout::Error, Faraday::Error, Legion::Crypt::Vault::HTTPError => e
331
308
  handle_exception(e, level: :warn, operation: 'crypt.start_lease_manager')
332
309
  end
333
310
 
311
+ def postgresql_leases_for_mode(leases:, mode:)
312
+ role = POSTGRES_ROLE_MAP[mode]
313
+ return leases unless role
314
+
315
+ postgresql_key = leases.key?(:postgresql) ? :postgresql : 'postgresql'
316
+ definition = leases[postgresql_key]
317
+ return leases unless definition.is_a?(Hash)
318
+
319
+ path_key = definition.key?(:path) ? :path : 'path'
320
+ leases.merge(postgresql_key => definition.merge(path_key => "postgresql/creds/#{role}"))
321
+ end
322
+
334
323
  def start_token_renewers
335
324
  started = 0
336
- configurations = clusters.empty? ? { nil => vault_settings } : clusters
325
+ configurations = clusters.empty? ? { nil => Legion::Settings[:crypt][:vault] } : clusters
337
326
  configurations.each do |name, config|
338
327
  next unless %w[kerberos ldap jwt].include?(config[:auth_method].to_s) && config[:connected]
339
328
 
@@ -364,19 +353,19 @@ module Legion
364
353
  end
365
354
 
366
355
  def start_svid_rotation
367
- return unless Spiffe.enabled?
356
+ return unless Legion::Crypt::Spiffe.enabled?
368
357
 
369
358
  log.info 'Legion::Crypt starting SPIFFE SVID rotation'
370
- @svid_rotation = Spiffe::SvidRotation.new
359
+ @svid_rotation = Legion::Crypt::Spiffe::SvidRotation.new
371
360
  @svid_rotation.start
372
- rescue StandardError => e
361
+ rescue IOError, SystemCallError, Timeout::Error, Legion::Crypt::Spiffe::Error => e
373
362
  handle_exception(e, level: :warn, operation: 'crypt.start_svid_rotation')
374
363
  end
375
364
 
376
365
  def stop_svid_rotation(deadline: nil)
377
366
  return unless @svid_rotation
378
367
 
379
- Legion::Crypt::VaultOperation.with_timeout(deadline: deadline) { @svid_rotation.stop }
368
+ Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { @svid_rotation.stop }
380
369
  ensure
381
370
  @svid_rotation = nil
382
371
  end