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
@@ -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