Skip to content

Commit 403a7ac

Browse files
authored
Merge pull request #30 from LegionIO/fix/vault-lease-token-cascade-revocation
fix vault lease cascade revocation — handle non-renewable tokens and reissue on reauth
2 parents 731ee17 + d8b4801 commit 403a7ac

5 files changed

Lines changed: 77 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
## [Unreleased]
44

5+
## [1.5.9] - 2026-04-10
6+
7+
### Fixed
8+
- Vault lease cascade revocation: all three service credentials (RabbitMQ, PostgreSQL, Redis) died at exactly 2 hours when the Vault Kerberos auth token expired — Vault cascade-revokes all child leases when the parent token dies, regardless of individual lease TTLs (closes #29)
9+
- `TokenRenewer` now detects non-renewable tokens (`renewable=false`) and skips `renew_self` (which always fails for non-renewable tokens), going straight to `reauth_kerberos` before the token expires
10+
- `TokenRenewer#reauth_kerberos` now triggers `LeaseManager.reissue_all` after obtaining a new token, re-issuing all active leases under the new token so they are not orphaned when the old token expires
11+
- `LeaseManager#push_to_settings` symbol/string key mismatch: `resolve_secrets!` registers refs with string keys (`"rabbitmq"`) via `lease://` URI parsing, but `cache_lease` stores leases with symbol keys (`:rabbitmq` from `Legion::JSON.load`) — now tries both key types
12+
- `LeaseManager#trigger_reconnect` for `:postgresql` — uses surgical Sequel pool `disconnect` + `test_connection` instead of `Data.shutdown + Data.setup` which tore down unrelated connections (Apollo SQLite, Local cache)
13+
- `LeaseManager#trigger_reconnect` for `:redis` — uses `Cache.restart` (the actual method) instead of `Cache.reconnect` (which does not exist)
14+
15+
### Added
16+
- `LeaseManager#reissue_all` — re-issues all active leases under the current vault client token; called by `TokenRenewer` after successful Kerberos re-authentication to prevent cascade revocation of orphaned leases
17+
518
## [1.5.8] - 2026-04-09
619

720
### Added

lib/legion/crypt/lease_manager.rb

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def register_ref(name, key, path)
8686

8787
def push_to_settings(name)
8888
refs, data = @state_mutex.synchronize do
89-
[@refs[name]&.dup, @lease_cache[name]&.dup]
89+
r = @refs[name] || @refs[name.to_s] || @refs[name.to_sym]
90+
d = @lease_cache[name] || @lease_cache[name.to_s] || @lease_cache[name.to_sym]
91+
[r&.dup, d&.dup]
9092
end
9193
return if refs.nil? || refs.empty?
9294
return unless data
@@ -109,6 +111,19 @@ def vault_sys
109111
sys
110112
end
111113

114+
def reissue_all
115+
log.info('LeaseManager: reissue_all — re-issuing all active leases under new token')
116+
lease_names = @state_mutex.synchronize { @active_leases.keys.dup }
117+
118+
lease_names.each do |name|
119+
lease = @state_mutex.synchronize { @active_leases[name]&.dup }
120+
next unless lease && lease[:path]
121+
122+
reissue_lease(name)
123+
end
124+
log.info('LeaseManager: reissue_all complete')
125+
end
126+
112127
def register_dynamic_lease(name:, path:, response:, settings_refs:)
113128
register_at_exit_hook
114129

@@ -451,14 +466,19 @@ def trigger_reconnect(name)
451466
Legion::Transport::Connection.force_reconnect
452467
log.info("LeaseManager: triggered transport reconnect after '#{name}' reissue")
453468
when :postgresql
454-
return unless defined?(Legion::Data) && Legion::Data.respond_to?(:reconnect)
469+
return unless defined?(Legion::Data::Connection) && Legion::Data::Connection.sequel
455470

456-
Legion::Data.reconnect
457-
log.info("LeaseManager: triggered data reconnect after '#{name}' reissue")
471+
Legion::Data::Connection.sequel.disconnect
472+
Legion::Data::Connection.sequel.test_connection
473+
log.info("LeaseManager: triggered data pool reconnect after '#{name}' reissue")
458474
when :redis
459-
return unless defined?(Legion::Cache) && Legion::Cache.respond_to?(:reconnect)
475+
return unless defined?(Legion::Cache)
460476

461-
Legion::Cache.reconnect
477+
if Legion::Cache.respond_to?(:restart)
478+
Legion::Cache.restart
479+
elsif Legion::Cache.respond_to?(:reconnect)
480+
Legion::Cache.reconnect
481+
end
462482
log.info("LeaseManager: triggered cache reconnect after '#{name}' reissue")
463483
end
464484
rescue StandardError => e

lib/legion/crypt/token_renewer.rb

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ def reauth_kerberos
7272
@config[:renewable] = result[:renewable]
7373
@config[:connected] = true
7474
@vault_client.token = result[:token]
75-
log.info("TokenRenewer[#{@cluster_name}]: re-authenticated via Kerberos")
75+
log.info("TokenRenewer[#{@cluster_name}]: re-authenticated via Kerberos, ttl=#{result[:lease_duration]}s")
76+
77+
reissue_all_leases
7678
true
7779
rescue StandardError => e
7880
handle_exception(e, level: :warn, operation: 'crypt.token_renewer.reauth_kerberos', cluster_name: @cluster_name)
@@ -104,10 +106,18 @@ def renewal_loop
104106
interruptible_sleep(sleep_duration)
105107

106108
until @stop
107-
if renew_token || reauth_kerberos
108-
on_renewal_success
109+
if @config[:renewable]
110+
if renew_token || reauth_kerberos
111+
on_renewal_success
112+
else
113+
on_renewal_failure
114+
end
109115
else
110-
on_renewal_failure
116+
if reauth_kerberos # rubocop:disable Style/IfInsideElse
117+
on_renewal_success
118+
else
119+
on_renewal_failure
120+
end
111121
end
112122
end
113123
rescue StandardError => e
@@ -128,6 +138,15 @@ def on_renewal_failure
128138
interruptible_sleep(delay)
129139
end
130140

141+
def reissue_all_leases
142+
return unless defined?(Legion::Crypt::LeaseManager)
143+
144+
Legion::Crypt::LeaseManager.instance.reissue_all
145+
rescue StandardError => e
146+
handle_exception(e, level: :warn, operation: 'crypt.token_renewer.reissue_all_leases', cluster_name: @cluster_name)
147+
log.warn("TokenRenewer[#{@cluster_name}]: failed to reissue leases after reauth: #{e.message}")
148+
end
149+
131150
def interruptible_sleep(seconds)
132151
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + seconds
133152
loop do
@@ -150,7 +169,7 @@ def stop_thread_and_revoke
150169
else
151170
@thread = nil
152171
revoke_token
153-
log.debug("TokenRenewer[#{@cluster_name}]: token renewal thread stopped")
172+
log.info("TokenRenewer[#{@cluster_name}]: token renewal thread stopped")
154173
end
155174
end
156175

lib/legion/crypt/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module Legion
44
module Crypt
5-
VERSION = '1.5.8'
5+
VERSION = '1.5.9'
66
end
77
end

spec/legion/lease_manager_spec.rb

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -611,29 +611,28 @@
611611
end
612612

613613
context 'when name is :postgresql' do
614-
it 'calls Data.reconnect when available' do
615-
data_mod = double('Legion::Data')
616-
stub_const('Legion::Data', data_mod)
617-
allow(data_mod).to receive(:respond_to?).with(:reconnect).and_return(true)
618-
expect(data_mod).to receive(:reconnect)
614+
it 'disconnects and reconnects the Sequel pool' do
615+
sequel_db = double('Sequel::Database')
616+
connection_mod = double('Legion::Data::Connection', sequel: sequel_db)
617+
stub_const('Legion::Data::Connection', connection_mod)
618+
expect(sequel_db).to receive(:disconnect)
619+
expect(sequel_db).to receive(:test_connection)
619620
manager.send(:trigger_reconnect, :postgresql)
620621
end
621622

622-
it 'skips when Data does not respond to reconnect' do
623-
data_mod = double('Legion::Data')
624-
stub_const('Legion::Data', data_mod)
625-
allow(data_mod).to receive(:respond_to?).with(:reconnect).and_return(false)
626-
expect(data_mod).not_to receive(:reconnect)
627-
manager.send(:trigger_reconnect, :postgresql)
623+
it 'skips when Data::Connection.sequel is nil' do
624+
connection_mod = double('Legion::Data::Connection', sequel: nil)
625+
stub_const('Legion::Data::Connection', connection_mod)
626+
expect { manager.send(:trigger_reconnect, :postgresql) }.not_to raise_error
628627
end
629628
end
630629

631630
context 'when name is :redis' do
632-
it 'calls Cache.reconnect when available' do
631+
it 'calls Cache.restart when available' do
633632
cache_mod = double('Legion::Cache')
634633
stub_const('Legion::Cache', cache_mod)
635-
allow(cache_mod).to receive(:respond_to?).with(:reconnect).and_return(true)
636-
expect(cache_mod).to receive(:reconnect)
634+
allow(cache_mod).to receive(:respond_to?).with(:restart).and_return(true)
635+
expect(cache_mod).to receive(:restart)
637636
manager.send(:trigger_reconnect, :redis)
638637
end
639638
end

0 commit comments

Comments
 (0)