legion-crypt 1.5.4 → 1.5.5
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 +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/legion/crypt/lease_manager.rb +68 -2
- data/lib/legion/crypt/settings.rb +3 -1
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +110 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85275c53d61c6172fcabf74c69df85324c94d828372894e7d29e2c550501fc88
|
|
4
|
+
data.tar.gz: 61a939f7fb79750ca314840c2c16766e7d325eb956e2d9e3d6196174bde1f0ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d7d33ed75be9d7263b32e9cb3830bff352dddd68fff8a8f060c26a7856a1962a8be31fa2955cabb76b6c961a7891890fae22b94c001e718b91ecdb5a2df3286
|
|
7
|
+
data.tar.gz: c6df0c72440d9ac6cb134bbd1498d68f51a582431d974ee42a7084e9077170125047e0d55293623109c8c14562f34b86577482d100e2d82ad06125a09ade17d1
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.5.5] - 2026-04-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `RMQ_ROLE_MAP` constant mapping `:agent`/`:infra` → `'legionio-infra'` and `:worker` → `'legionio-worker'` for Vault RabbitMQ role selection (Phase 5 credential scoping)
|
|
9
|
+
- `dynamic_rmq_creds?` helper reads `Settings[:crypt][:vault][:dynamic_rmq_creds]` flag
|
|
10
|
+
- `fetch_bootstrap_rmq_creds` — fetches short-lived bootstrap RabbitMQ credentials from `rabbitmq/creds/legionio-bootstrap` and writes them to `Settings[:transport][:connection]`; gated on `vault_connected? && dynamic_rmq_creds?`; stores `@bootstrap_lease_id` for later revocation; rescue-safe
|
|
11
|
+
- `swap_to_identity_creds(mode:)` — fetches identity-scoped RabbitMQ credentials from the role matching `mode`, registers them with `LeaseManager` for renewal, updates transport settings, calls `Transport::Connection.force_reconnect`, and revokes the bootstrap lease; raises if reconnect fails (before revoking bootstrap)
|
|
12
|
+
- `revoke_bootstrap_lease` — revokes `@bootstrap_lease_id` via `LeaseManager#vault_sys`; non-fatal on failure; idempotent
|
|
13
|
+
- `LeaseManager#register_dynamic_lease` — registers a dynamically-fetched Vault lease into the cache and active lease tracking with mutex, stores `path` for `reissue_lease`, registers settings refs for rotation push-back
|
|
14
|
+
- `LeaseManager#reissue_lease(name)` — performs a full re-read (`logical.read(path)`) at credential rotation time, updates cache + active_leases in mutex, calls `push_to_settings`, triggers `Transport::Connection.force_reconnect` for `:rabbitmq` leases
|
|
15
|
+
- `LeaseManager#vault_logical` and `LeaseManager#vault_sys` — public delegators to the private `logical`/`sys` methods for use by `Crypt` bootstrap/swap operations
|
|
16
|
+
- `dynamic_rmq_creds: false` and `dynamic_pg_creds: false` defaults added to vault settings
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `start_lease_manager` now starts the renewal thread when `dynamic_rmq_creds: true` even if no static leases are configured, ensuring the renewal loop is running before identity-scoped leases are registered post-boot
|
|
20
|
+
|
|
5
21
|
## [1.5.4] - 2026-04-06
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -97,6 +97,67 @@ module Legion
|
|
|
97
97
|
log.info("Lease '#{name}' rotated — updated #{refs.size} settings reference(s)")
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# Public Vault client accessors used by Crypt for bootstrap/swap operations.
|
|
101
|
+
# Delegates to the configured vault_client or falls back to ::Vault.
|
|
102
|
+
def vault_logical
|
|
103
|
+
logical
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def vault_sys
|
|
107
|
+
sys
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def register_dynamic_lease(name:, path:, response:, settings_refs:)
|
|
111
|
+
register_at_exit_hook
|
|
112
|
+
|
|
113
|
+
@state_mutex.synchronize do
|
|
114
|
+
@lease_cache[name] = response.data || {}
|
|
115
|
+
@active_leases[name] = {
|
|
116
|
+
lease_id: response.lease_id,
|
|
117
|
+
lease_duration: response.lease_duration,
|
|
118
|
+
expires_at: Time.now + (response.lease_duration || 0),
|
|
119
|
+
fetched_at: Time.now,
|
|
120
|
+
renewable: response.renewable?,
|
|
121
|
+
path: path
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
settings_refs.each do |ref|
|
|
125
|
+
register_ref(name, ref[:key], ref[:path])
|
|
126
|
+
end
|
|
127
|
+
log.info("LeaseManager: registered dynamic lease '#{name}' (path: #{path})")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def reissue_lease(name)
|
|
131
|
+
lease = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
132
|
+
return unless lease && lease[:path]
|
|
133
|
+
|
|
134
|
+
response = logical.read(lease[:path])
|
|
135
|
+
return unless response&.data
|
|
136
|
+
|
|
137
|
+
@state_mutex.synchronize do
|
|
138
|
+
active_lease = @active_leases[name]
|
|
139
|
+
next unless active_lease
|
|
140
|
+
|
|
141
|
+
@lease_cache[name] = response.data
|
|
142
|
+
active_lease.merge!(
|
|
143
|
+
lease_id: response.lease_id,
|
|
144
|
+
lease_duration: response.lease_duration,
|
|
145
|
+
expires_at: Time.now + (response.lease_duration || 0),
|
|
146
|
+
fetched_at: Time.now,
|
|
147
|
+
renewable: response.renewable?
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
push_to_settings(name)
|
|
151
|
+
|
|
152
|
+
return unless name == :rabbitmq && defined?(Legion::Transport::Connection)
|
|
153
|
+
|
|
154
|
+
Legion::Transport::Connection.force_reconnect
|
|
155
|
+
log.info("LeaseManager: reissued lease '#{name}' and triggered transport reconnect")
|
|
156
|
+
rescue StandardError => e
|
|
157
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.reissue_lease', lease_name: name)
|
|
158
|
+
log.warn("LeaseManager: failed to reissue lease '#{name}': #{e.message}")
|
|
159
|
+
end
|
|
160
|
+
|
|
100
161
|
def start_renewal_thread
|
|
101
162
|
@state_mutex.synchronize do
|
|
102
163
|
return if @renewal_thread&.alive?
|
|
@@ -240,10 +301,14 @@ module Legion
|
|
|
240
301
|
leases.each do |name|
|
|
241
302
|
lease = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
242
303
|
next unless lease
|
|
243
|
-
next unless lease[:renewable]
|
|
244
304
|
next unless approaching_expiry?(lease)
|
|
245
305
|
|
|
246
|
-
|
|
306
|
+
if lease[:renewable]
|
|
307
|
+
renew_lease(name, lease)
|
|
308
|
+
elsif lease[:path]
|
|
309
|
+
log.info("LeaseManager: lease '#{name}' is non-renewable and approaching expiry — re-issuing")
|
|
310
|
+
reissue_lease(name)
|
|
311
|
+
end
|
|
247
312
|
end
|
|
248
313
|
end
|
|
249
314
|
|
|
@@ -267,6 +332,7 @@ module Legion
|
|
|
267
332
|
rescue StandardError => e
|
|
268
333
|
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.renew_lease', lease_name: name)
|
|
269
334
|
log.warn("LeaseManager: failed to renew lease '#{name}': #{e.message}")
|
|
335
|
+
reissue_lease(name) if lease[:path]
|
|
270
336
|
end
|
|
271
337
|
|
|
272
338
|
def lease_valid?(name)
|
data/lib/legion/crypt/version.rb
CHANGED
data/lib/legion/crypt.rb
CHANGED
|
@@ -25,6 +25,12 @@ module Legion
|
|
|
25
25
|
extend Legion::Crypt::VaultCluster
|
|
26
26
|
extend Legion::Crypt::LdapAuth
|
|
27
27
|
|
|
28
|
+
RMQ_ROLE_MAP = {
|
|
29
|
+
agent: 'legionio-infra',
|
|
30
|
+
worker: 'legionio-worker',
|
|
31
|
+
infra: 'legionio-infra'
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
28
34
|
class << self
|
|
29
35
|
attr_reader :sessions
|
|
30
36
|
|
|
@@ -97,6 +103,96 @@ module Legion
|
|
|
97
103
|
settings[:jwt] || Legion::Crypt::Settings.jwt
|
|
98
104
|
end
|
|
99
105
|
|
|
106
|
+
def dynamic_rmq_creds?
|
|
107
|
+
Legion::Settings.dig(:crypt, :vault, :dynamic_rmq_creds) == true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def fetch_bootstrap_rmq_creds
|
|
111
|
+
return unless vault_connected? && dynamic_rmq_creds?
|
|
112
|
+
|
|
113
|
+
Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default) if defined?(Legion::Transport::Settings)
|
|
114
|
+
|
|
115
|
+
response = LeaseManager.instance.vault_logical.read('rabbitmq/creds/legionio-bootstrap')
|
|
116
|
+
return unless response&.data
|
|
117
|
+
|
|
118
|
+
bootstrap_lease_ttl = Legion::Settings.dig(:crypt, :vault, :bootstrap_lease_ttl).to_i
|
|
119
|
+
bootstrap_lease_ttl = 300 if bootstrap_lease_ttl <= 0
|
|
120
|
+
|
|
121
|
+
@bootstrap_lease_id = response.lease_id
|
|
122
|
+
@bootstrap_lease_expires = Time.now + [response.lease_duration, bootstrap_lease_ttl].min
|
|
123
|
+
|
|
124
|
+
settings = Legion::Settings.loader.settings
|
|
125
|
+
settings[:transport] ||= {}
|
|
126
|
+
settings[:transport][:connection] ||= {}
|
|
127
|
+
conn = settings[:transport][:connection]
|
|
128
|
+
username = response.data[:username] || response.data['username']
|
|
129
|
+
password = response.data[:password] || response.data['password']
|
|
130
|
+
|
|
131
|
+
unless username && password
|
|
132
|
+
log.warn 'Bootstrap RMQ credential fetch returned nil username or password — skipping settings update'
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
conn[:user] = username
|
|
137
|
+
conn[:password] = password
|
|
138
|
+
|
|
139
|
+
log.info "Bootstrap RMQ credentials acquired (lease: #{@bootstrap_lease_id[0..7]}...)"
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
log.warn "Bootstrap RMQ credential fetch failed: #{e.message}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def swap_to_identity_creds(mode:)
|
|
145
|
+
return unless vault_connected? && dynamic_rmq_creds?
|
|
146
|
+
return if mode == :lite
|
|
147
|
+
|
|
148
|
+
role = RMQ_ROLE_MAP.fetch(mode, "legionio-#{mode}")
|
|
149
|
+
response = LeaseManager.instance.vault_logical.read("rabbitmq/creds/#{role}")
|
|
150
|
+
raise "Failed to fetch identity-scoped RMQ creds for role #{role}" unless response&.data
|
|
151
|
+
|
|
152
|
+
LeaseManager.instance.register_dynamic_lease(
|
|
153
|
+
name: :rabbitmq,
|
|
154
|
+
path: "rabbitmq/creds/#{role}",
|
|
155
|
+
response: response,
|
|
156
|
+
settings_refs: [
|
|
157
|
+
{ path: %i[transport connection user], key: :username },
|
|
158
|
+
{ path: %i[transport connection password], key: :password }
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
settings = Legion::Settings.loader.settings
|
|
163
|
+
settings[:transport] ||= {}
|
|
164
|
+
settings[:transport][:connection] ||= {}
|
|
165
|
+
conn = settings[:transport][:connection]
|
|
166
|
+
username = response.data[:username] || response.data['username']
|
|
167
|
+
password = response.data[:password] || response.data['password']
|
|
168
|
+
raise "Identity-scoped RMQ creds for role #{role} missing username or password" unless username && password
|
|
169
|
+
|
|
170
|
+
conn[:user] = username
|
|
171
|
+
conn[:password] = password
|
|
172
|
+
|
|
173
|
+
if defined?(Legion::Transport::Connection)
|
|
174
|
+
Legion::Transport::Connection.force_reconnect
|
|
175
|
+
raise 'Transport reconnect failed after credential swap — bootstrap lease NOT revoked' unless Legion::Transport::Connection.session_open?
|
|
176
|
+
|
|
177
|
+
log.info "Transport reconnected with identity-scoped creds (role: #{role})"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
revoke_bootstrap_lease
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def revoke_bootstrap_lease
|
|
184
|
+
return unless @bootstrap_lease_id
|
|
185
|
+
|
|
186
|
+
LeaseManager.instance.vault_sys.revoke(@bootstrap_lease_id)
|
|
187
|
+
log.info "Bootstrap RMQ lease revoked (#{@bootstrap_lease_id[0..7]}...)"
|
|
188
|
+
@bootstrap_lease_id = nil
|
|
189
|
+
@bootstrap_lease_expires = nil
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
log.warn "Bootstrap lease revocation failed: #{e.message} — lease will expire naturally"
|
|
192
|
+
@bootstrap_lease_id = nil
|
|
193
|
+
@bootstrap_lease_expires = nil
|
|
194
|
+
end
|
|
195
|
+
|
|
100
196
|
def vault_connected?
|
|
101
197
|
return true if settings.dig(:vault, :connected) == true
|
|
102
198
|
return true if respond_to?(:connected_clusters) && connected_clusters.any?
|
|
@@ -156,8 +252,10 @@ module Legion
|
|
|
156
252
|
|
|
157
253
|
def start_lease_manager
|
|
158
254
|
leases = settings.dig(:vault, :leases) || {}
|
|
159
|
-
|
|
160
|
-
|
|
255
|
+
vault_ok = connected_clusters.any? || settings.dig(:vault, :connected)
|
|
256
|
+
|
|
257
|
+
return if leases.empty? && !dynamic_rmq_creds?
|
|
258
|
+
return unless vault_ok
|
|
161
259
|
|
|
162
260
|
client = nil
|
|
163
261
|
|
|
@@ -167,15 +265,19 @@ module Legion
|
|
|
167
265
|
elsif settings.dig(:vault, :connected)
|
|
168
266
|
client = vault_client
|
|
169
267
|
end
|
|
268
|
+
|
|
170
269
|
lease_manager = Legion::Crypt::LeaseManager.instance
|
|
171
270
|
lease_manager.start(leases, vault_client: client)
|
|
172
271
|
lease_manager.start_renewal_thread
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
272
|
+
|
|
273
|
+
unless leases.empty?
|
|
274
|
+
fetched = lease_manager.fetched_count
|
|
275
|
+
defined = leases.size
|
|
276
|
+
if fetched == defined
|
|
277
|
+
log.info "LeaseManager: #{fetched} lease(s) initialized"
|
|
278
|
+
else
|
|
279
|
+
log.warn "LeaseManager: #{fetched}/#{defined} lease(s) initialized (#{defined - fetched} failed)"
|
|
280
|
+
end
|
|
179
281
|
end
|
|
180
282
|
rescue StandardError => e
|
|
181
283
|
handle_exception(e, level: :warn, operation: 'crypt.start_lease_manager')
|