legion-crypt 1.5.0 → 1.5.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b7ffab5835f3a3ac600a9bfe87e1e015e362d07d5814c956c13cc7552d23775
4
- data.tar.gz: 18179a5915360c9f22151ec6a08ac813e580c2c2fb36dffd95b6f42aa9a4c242
3
+ metadata.gz: 01cec7ac57c7d5448b5fba4237fdccc9ac8099966a4a546e95dd4888d838604f
4
+ data.tar.gz: ac81d602c9fc34493aeabe3fdeadd12f01fb36f8c248ac44687166fd24d9a48d
5
5
  SHA512:
6
- metadata.gz: cce5f0d26e384c890c7117f0e710a9ff0f27da5794e8c2c9f05b3bdd81cacf3a551ffaaa9f31a75fea68e512918370b6deb8870c30cd010ea2da622a87c04611
7
- data.tar.gz: acac22b254e94ccfd8ead507e014c7a619b715f8aab8000454d2e428633096ea3b251e21ab51ba67b31c879d6b07733670eb4cff1bf657ec553892262f72d199
6
+ metadata.gz: dea3247dbaf00a49507ededcfdd72e9e3545485cbcec511657d2e2c602e03f2230c3c34e93108ae14ede71aa7a768a36573c3f852595444f2f6ed118a351bd8c
7
+ data.tar.gz: 011ac0f235a69f7655ab1237d2a87faf3d343de20ef1ffe1e9576b5a555e9c0e5d46dfc6d77dd137851835850e3877783c70f62a216c3dd1a4af9c3e080e9ed6
data/AGENTS.md CHANGED
@@ -32,6 +32,15 @@ bundle exec rubocop
32
32
  - Maintain compatibility for Kerberos, LDAP, and JWT Vault auth paths.
33
33
  - Cryptographic defaults and key lifecycle behavior are contract-sensitive; change only with test coverage.
34
34
 
35
+ ## Known Risks
36
+
37
+ - Vault-backed cluster secret sync is inconsistent today: config key mismatch, read/write path mismatch, and push happens before the new secret is stored.
38
+ - External JWKS verification currently accepts tokens without issuer/audience enforcement unless the caller passes both explicitly; fail closed when touching this path.
39
+ - Multi-cluster Vault behavior has correctness gaps around LDAP token propagation, default-cluster routing, and lease-manager client selection.
40
+ - SPIFFE X.509 fetch currently falls back to a self-signed SVID on Workload API failure; treat that path as security-sensitive and avoid expanding the fallback behavior.
41
+ - `Ed25519` and `Erasure` include helper paths that call `Legion::Crypt::Vault.read/write` directly; verify runtime behavior before relying on those helpers.
42
+ - Current specs pass, but some of the highest-risk paths above are under-covered or only covered with mocks that preserve the existing behavior.
43
+
35
44
  ## Validation
36
45
 
37
46
  - Run targeted specs for changed auth/crypto paths first.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.5.2] - 2026-04-03
4
+
5
+ ### Fixed
6
+ - LeaseManager `at_exit` hook now wraps shutdown in a 10s timeout to prevent process hang when Logger Monitor or network I/O is blocked during crash exit
7
+
8
+ ## [1.5.1] - 2026-04-03
9
+
10
+ ### Fixed
11
+ - Vault `read` method no longer prepends a `legion/` mount prefix to paths — the default `type` parameter changed from `'legion'` to `nil` to match the actual KV v2 mount path in the `legionio` namespace
12
+ - LeaseManager now registers an `at_exit` hook to revoke active Vault leases on unclean process exit, preventing orphaned dynamic credentials (RabbitMQ users, PostgreSQL roles, Redis creds)
13
+
3
14
  ## [1.5.0] - 2026-04-02
4
15
 
5
16
  ### Fixed
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'legion/logging/helper'
4
4
  require 'singleton'
5
+ require 'timeout'
5
6
 
6
7
  module Legion
7
8
  module Crypt
@@ -24,6 +25,8 @@ module Legion
24
25
  @state_mutex.synchronize { @vault_client = vault_client }
25
26
  return if definitions.nil? || definitions.empty?
26
27
 
28
+ register_at_exit_hook
29
+
27
30
  log.info "LeaseManager start requested definitions=#{definitions.size}"
28
31
  definitions.each do |name, opts|
29
32
  path = opts['path'] || opts[:path]
@@ -43,16 +46,8 @@ module Legion
43
46
  next
44
47
  end
45
48
 
46
- @state_mutex.synchronize do
47
- @lease_cache[name] = response.data || {}
48
- @active_leases[name] = {
49
- lease_id: response.lease_id,
50
- lease_duration: response.lease_duration,
51
- renewable: response.renewable?,
52
- expires_at: Time.now + (response.lease_duration || 0),
53
- fetched_at: Time.now
54
- }
55
- end
49
+ log_lease_response(name, response)
50
+ cache_lease(name, response)
56
51
  log.info("LeaseManager: fetched lease for '#{name}' from #{path}")
57
52
  rescue StandardError => e
58
53
  handle_exception(e, level: :warn, operation: 'crypt.lease_manager.start', lease_name: name, path: path)
@@ -156,6 +151,48 @@ module Legion
156
151
 
157
152
  private
158
153
 
154
+ def register_at_exit_hook
155
+ return if @at_exit_registered
156
+
157
+ at_exit do
158
+ next if @state_mutex.synchronize { @active_leases.empty? }
159
+
160
+ Timeout.timeout(10) { shutdown }
161
+ rescue Timeout::Error
162
+ warn '[LeaseManager] at_exit shutdown timed out after 10s'
163
+ rescue StandardError # best effort on crash
164
+ nil
165
+ end
166
+ @at_exit_registered = true
167
+ end
168
+
169
+ def cache_lease(name, response)
170
+ @state_mutex.synchronize do
171
+ @lease_cache[name] = response.data || {}
172
+ @active_leases[name] = {
173
+ lease_id: response.lease_id,
174
+ lease_duration: response.lease_duration,
175
+ renewable: response.renewable?,
176
+ expires_at: Time.now + (response.lease_duration || 0),
177
+ fetched_at: Time.now
178
+ }
179
+ end
180
+ end
181
+
182
+ def log_lease_response(name, response)
183
+ data_keys = response.data&.keys&.map(&:to_s) || []
184
+ log.debug("LeaseManager[#{name}]: lease_id=#{response.lease_id}, " \
185
+ "lease_duration=#{response.lease_duration}s, " \
186
+ "renewable=#{response.renewable?}, " \
187
+ "data_keys=#{data_keys.inspect}")
188
+ return unless response.data&.key?(:username)
189
+
190
+ log.debug("LeaseManager[#{name}]: username=#{response.data[:username]}, " \
191
+ "password_length=#{response.data[:password]&.length || 0}, " \
192
+ "vhost=#{response.data[:vhost] || 'N/A'}, " \
193
+ "tags=#{response.data[:tags] || 'N/A'}")
194
+ end
195
+
159
196
  def logical
160
197
  client = @state_mutex.synchronize { @vault_client }
161
198
  client ? client.logical : ::Vault.logical
@@ -47,7 +47,7 @@ module Legion
47
47
  raise
48
48
  end
49
49
 
50
- def read(path, type = 'legion', cluster_name: nil)
50
+ def read(path, type = nil, cluster_name: nil)
51
51
  full_path = type.nil? || type.empty? ? path : "#{type}/#{path}"
52
52
  log_read_context(full_path, cluster_name: cluster_name)
53
53
  lease = logical_client(cluster_name: cluster_name).read(full_path)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.5.0'
5
+ VERSION = '1.5.2'
6
6
  end
7
7
  end
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.0
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity