legion-crypt 1.4.18 → 1.4.20
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 +21 -0
- data/lib/legion/crypt/kerberos_auth.rb +16 -0
- data/lib/legion/crypt/ldap_auth.rb +2 -1
- data/lib/legion/crypt/lease_manager.rb +10 -3
- data/lib/legion/crypt/vault.rb +42 -7
- data/lib/legion/crypt/vault_cluster.rb +29 -3
- data/lib/legion/crypt/vault_jwt_auth.rb +1 -1
- data/lib/legion/crypt/vault_kerberos_auth.rb +1 -1
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +7 -1
- 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: 6b78a171584f4ccb627b63cd35cad927a6773bd1802e0da432dd445dbceb85e4
|
|
4
|
+
data.tar.gz: b1806202e526aa259d1491a108f3d3b94b1c4564a05ea7b4afa702778069e715
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1cc01154f5fef732c05efe2d99213f1742aa486e2c703f7707709dbf29ab74484b49e5be4c3a1513b116ffdb658382b70e124067771549f9dcabd8cad1fd6f1
|
|
7
|
+
data.tar.gz: e8e674be70714e3d439330e8d1d03b323319dd60b0f2069c26087fa772ec27c61cbc3a1ca5ed47cdff30e5ee8a826e31f16b0695f35c479184be7a02fd34e2b7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Legion::Crypt
|
|
2
2
|
|
|
3
|
+
## [1.4.20] - 2026-03-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `Vault#read`: unwrap KV v2 response envelope — `logical.read` returns `{data: {keys}, metadata: {}}` for KV v2 mounts; the nested `:data` key is now auto-detected and unwrapped
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Debug logging throughout Vault auth, read, and cluster connection paths (`vault.rb`, `vault_cluster.rb`, `kerberos_auth.rb`, `lease_manager.rb`)
|
|
10
|
+
- `Vault#log_read_context`: logs path and namespace context for each Vault read
|
|
11
|
+
- `Vault#unwrap_kv_v2`: detects and unwraps KV v2 envelope pattern
|
|
12
|
+
- `VaultCluster`: debug logging for cluster connection, client build, and Kerberos auth flow
|
|
13
|
+
- `KerberosAuth`: debug logging for SPN, token exchange, policies, and renewal metadata
|
|
14
|
+
- `LeaseManager`: debug logging for lease fetch, renewal, and revocation
|
|
15
|
+
|
|
16
|
+
## [1.4.19] - 2026-03-26
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `LeaseManager`, `VaultJwtAuth`, `LdapAuth`, `VaultKerberosAuth`: use `renewable?` instead of `renewable` to match Vault gem API
|
|
20
|
+
- `LeaseManager#fetch`: handle string/symbol key mismatch between resolver (strings) and cache (symbols)
|
|
21
|
+
- `VaultCluster#connect_all_clusters`: set top-level `vault.connected` flag after any cluster connects via Kerberos/LDAP
|
|
22
|
+
- `Vault#add_session`: guard `@sessions` with lazy init to prevent nil error when using cluster-based auth
|
|
23
|
+
|
|
3
24
|
## [1.4.18] - 2026-03-26
|
|
4
25
|
|
|
5
26
|
### Fixed
|
|
@@ -17,10 +17,19 @@ module Legion
|
|
|
17
17
|
def self.login(vault_client:, service_principal:, auth_path: DEFAULT_AUTH_PATH)
|
|
18
18
|
raise GemMissingError, 'lex-kerberos gem is required for Kerberos auth' unless spnego_available?
|
|
19
19
|
|
|
20
|
+
log_debug("login: SPN=#{service_principal}, auth_path=#{auth_path}")
|
|
21
|
+
addr = vault_client.respond_to?(:address) ? vault_client.address : 'n/a'
|
|
22
|
+
ns = vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'
|
|
23
|
+
log_debug("login: vault_client.address=#{addr}, namespace=#{ns}")
|
|
24
|
+
|
|
20
25
|
@kerberos_principal = nil
|
|
21
26
|
token = obtain_token(service_principal)
|
|
27
|
+
log_debug("login: SPNEGO token obtained (#{token.length} chars)")
|
|
28
|
+
|
|
22
29
|
result = exchange_token(vault_client, token, auth_path)
|
|
23
30
|
@kerberos_principal = result[:metadata]&.dig('username') || result[:metadata]&.dig(:username)
|
|
31
|
+
log_debug("login: authenticated as #{@kerberos_principal.inspect}, policies=#{result[:policies].inspect}")
|
|
32
|
+
log_debug("login: renewable=#{result[:renewable]}, ttl=#{result[:lease_duration]}s")
|
|
24
33
|
result
|
|
25
34
|
end
|
|
26
35
|
|
|
@@ -41,6 +50,11 @@ module Legion
|
|
|
41
50
|
@kerberos_principal = nil
|
|
42
51
|
end
|
|
43
52
|
|
|
53
|
+
def self.log_debug(message)
|
|
54
|
+
Legion::Logging.debug("KerberosAuth: #{message}") if defined?(Legion::Logging)
|
|
55
|
+
end
|
|
56
|
+
private_class_method :log_debug
|
|
57
|
+
|
|
44
58
|
class << self
|
|
45
59
|
private
|
|
46
60
|
|
|
@@ -58,6 +72,7 @@ module Legion
|
|
|
58
72
|
|
|
59
73
|
# The Vault Kerberos plugin reads the SPNEGO token from the HTTP
|
|
60
74
|
# Authorization header, not the JSON body.
|
|
75
|
+
log_debug("exchange_token: PUT /v1/#{auth_path} (namespace=#{vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'})")
|
|
61
76
|
json = vault_client.put(
|
|
62
77
|
"/v1/#{auth_path}",
|
|
63
78
|
'{}',
|
|
@@ -75,6 +90,7 @@ module Legion
|
|
|
75
90
|
metadata: auth.metadata
|
|
76
91
|
}
|
|
77
92
|
rescue ::Vault::HTTPClientError => e
|
|
93
|
+
log_debug("exchange_token: HTTP error: #{e.message}")
|
|
78
94
|
raise AuthError, "Vault Kerberos auth failed: #{e.message}"
|
|
79
95
|
end
|
|
80
96
|
end
|
|
@@ -12,10 +12,11 @@ module Legion
|
|
|
12
12
|
|
|
13
13
|
clusters[cluster_name][:token] = token
|
|
14
14
|
clusters[cluster_name][:connected] = true
|
|
15
|
+
mark_vault_connected
|
|
15
16
|
|
|
16
17
|
Legion::Logging.info "LDAP login success: user=#{username}, cluster=#{cluster_name}" if defined?(Legion::Logging)
|
|
17
18
|
{ token: token, lease_duration: auth.lease_duration,
|
|
18
|
-
renewable: auth.renewable
|
|
19
|
+
renewable: auth.renewable?, policies: auth.policies }
|
|
19
20
|
rescue StandardError => e
|
|
20
21
|
Legion::Logging.warn "LDAP login failed: user=#{username}, cluster=#{cluster_name}: #{e.message}" if defined?(Legion::Logging)
|
|
21
22
|
raise
|
|
@@ -27,13 +27,16 @@ module Legion
|
|
|
27
27
|
|
|
28
28
|
begin
|
|
29
29
|
response = logical.read(path)
|
|
30
|
-
|
|
30
|
+
unless response
|
|
31
|
+
log_warn("LeaseManager: no data at '#{name}' (#{path}) — path may not exist or role not configured")
|
|
32
|
+
next
|
|
33
|
+
end
|
|
31
34
|
|
|
32
35
|
@lease_cache[name] = response.data || {}
|
|
33
36
|
@active_leases[name] = {
|
|
34
37
|
lease_id: response.lease_id,
|
|
35
38
|
lease_duration: response.lease_duration,
|
|
36
|
-
renewable: response.renewable
|
|
39
|
+
renewable: response.renewable?,
|
|
37
40
|
expires_at: Time.now + (response.lease_duration || 0),
|
|
38
41
|
fetched_at: Time.now
|
|
39
42
|
}
|
|
@@ -44,8 +47,12 @@ module Legion
|
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
def fetched_count
|
|
51
|
+
@active_leases.size
|
|
52
|
+
end
|
|
53
|
+
|
|
47
54
|
def fetch(name, key)
|
|
48
|
-
data = @lease_cache[name]
|
|
55
|
+
data = @lease_cache[name.to_sym] || @lease_cache[name.to_s]
|
|
49
56
|
return nil unless data
|
|
50
57
|
|
|
51
58
|
data[key.to_sym] || data[key.to_s]
|
data/lib/legion/crypt/vault.rb
CHANGED
|
@@ -44,23 +44,34 @@ module Legion
|
|
|
44
44
|
|
|
45
45
|
def read(path, type = 'legion')
|
|
46
46
|
full_path = type.nil? || type.empty? ? "#{type}/#{path}" : path
|
|
47
|
-
|
|
47
|
+
log_read_context(full_path)
|
|
48
48
|
lease = logical_client.read(full_path)
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
if lease.nil?
|
|
50
|
+
log_vault_debug("Vault read: #{full_path} returned nil")
|
|
51
|
+
return nil
|
|
52
|
+
end
|
|
53
|
+
add_session(path: lease.lease_id) if lease.respond_to?(:lease_id) && lease.lease_id && !lease.lease_id.empty?
|
|
54
|
+
|
|
55
|
+
data = lease.data
|
|
56
|
+
log_vault_debug("Vault read: #{full_path} returned keys=#{data&.keys&.inspect}")
|
|
57
|
+
unwrap_kv_v2(data, full_path)
|
|
51
58
|
rescue StandardError => e
|
|
52
|
-
Legion::Logging.warn "Vault read failed at #{full_path}: #{e.message}" if defined?(Legion::Logging)
|
|
59
|
+
Legion::Logging.warn "Vault read failed at #{full_path}: #{e.class}=#{e.message}" if defined?(Legion::Logging)
|
|
53
60
|
raise
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
def get(path)
|
|
57
|
-
Legion::Logging.debug "Vault kv get:
|
|
64
|
+
Legion::Logging.debug "Vault kv get: path=#{path}" if defined?(Legion::Logging)
|
|
58
65
|
result = kv_client.read(path)
|
|
59
|
-
|
|
66
|
+
if result.nil?
|
|
67
|
+
Legion::Logging.debug "Vault kv get: #{path} returned nil" if defined?(Legion::Logging)
|
|
68
|
+
return nil
|
|
69
|
+
end
|
|
60
70
|
|
|
71
|
+
Legion::Logging.debug "Vault kv get: #{path} returned keys=#{result.data&.keys&.inspect}" if defined?(Legion::Logging)
|
|
61
72
|
result.data
|
|
62
73
|
rescue StandardError => e
|
|
63
|
-
Legion::Logging.warn "Vault kv get failed at #{path}: #{e.message}" if defined?(Legion::Logging)
|
|
74
|
+
Legion::Logging.warn "Vault kv get failed at #{path}: #{e.class}=#{e.message}" if defined?(Legion::Logging)
|
|
64
75
|
raise
|
|
65
76
|
end
|
|
66
77
|
|
|
@@ -85,6 +96,7 @@ module Legion
|
|
|
85
96
|
end
|
|
86
97
|
|
|
87
98
|
def add_session(path:)
|
|
99
|
+
@sessions ||= []
|
|
88
100
|
@sessions.push(path)
|
|
89
101
|
end
|
|
90
102
|
|
|
@@ -158,6 +170,29 @@ module Legion
|
|
|
158
170
|
::Vault.logical
|
|
159
171
|
end
|
|
160
172
|
end
|
|
173
|
+
|
|
174
|
+
def log_read_context(full_path)
|
|
175
|
+
return unless defined?(Legion::Logging)
|
|
176
|
+
|
|
177
|
+
namespace = if respond_to?(:connected_clusters) && connected_clusters.any?
|
|
178
|
+
client = vault_client
|
|
179
|
+
client.respond_to?(:namespace) ? client.namespace : 'n/a'
|
|
180
|
+
else
|
|
181
|
+
'n/a (global client)'
|
|
182
|
+
end
|
|
183
|
+
Legion::Logging.debug "Vault read: path=#{full_path}, namespace=#{namespace}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def unwrap_kv_v2(data, full_path)
|
|
187
|
+
return data unless data.is_a?(Hash) && data.key?(:data) && data[:data].is_a?(Hash) && data.key?(:metadata)
|
|
188
|
+
|
|
189
|
+
log_vault_debug("Vault read: #{full_path} detected KV v2 envelope, unwrapping :data key")
|
|
190
|
+
data[:data]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def log_vault_debug(message)
|
|
194
|
+
Legion::Logging.debug(message) if defined?(Legion::Logging)
|
|
195
|
+
end
|
|
161
196
|
end
|
|
162
197
|
end
|
|
163
198
|
end
|
|
@@ -40,8 +40,10 @@ module Legion
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def connect_all_clusters
|
|
43
|
+
log_vault_debug("connect_all_clusters: #{clusters.size} cluster(s) configured")
|
|
43
44
|
results = {}
|
|
44
45
|
clusters.each do |name, config|
|
|
46
|
+
log_vault_debug("connect_all_clusters: #{name} (auth_method=#{config[:auth_method].inspect})")
|
|
45
47
|
case config[:auth_method]&.to_s
|
|
46
48
|
when 'kerberos'
|
|
47
49
|
results[name] = connect_kerberos_cluster(name, config)
|
|
@@ -60,11 +62,21 @@ module Legion
|
|
|
60
62
|
results[name] = false
|
|
61
63
|
log_vault_error(name, e)
|
|
62
64
|
end
|
|
65
|
+
|
|
66
|
+
connected = results.select { |_, v| v }
|
|
67
|
+
log_vault_debug("connect_all_clusters: #{connected.size}/#{results.size} connected")
|
|
68
|
+
mark_vault_connected if connected.any?
|
|
63
69
|
results
|
|
64
70
|
end
|
|
65
71
|
|
|
66
72
|
private
|
|
67
73
|
|
|
74
|
+
def mark_vault_connected
|
|
75
|
+
return unless defined?(Legion::Settings)
|
|
76
|
+
|
|
77
|
+
Legion::Settings[:crypt][:vault][:connected] = true
|
|
78
|
+
end
|
|
79
|
+
|
|
68
80
|
def resolve_cluster_name(name)
|
|
69
81
|
return name.to_sym if name
|
|
70
82
|
|
|
@@ -74,8 +86,10 @@ module Legion
|
|
|
74
86
|
def build_vault_client(config)
|
|
75
87
|
return nil unless config.is_a?(Hash)
|
|
76
88
|
|
|
89
|
+
addr = "#{config[:protocol]}://#{config[:address]}:#{config[:port]}"
|
|
90
|
+
log_vault_debug("build_vault_client: address=#{addr}")
|
|
77
91
|
client = ::Vault::Client.new(
|
|
78
|
-
address:
|
|
92
|
+
address: addr,
|
|
79
93
|
token: config[:token]
|
|
80
94
|
)
|
|
81
95
|
namespace =
|
|
@@ -86,6 +100,7 @@ module Legion
|
|
|
86
100
|
crypt_settings.respond_to?(:dig) ? crypt_settings.dig(:vault, :vault_namespace) : nil
|
|
87
101
|
end
|
|
88
102
|
client.namespace = namespace if namespace
|
|
103
|
+
log_vault_debug("build_vault_client: namespace=#{namespace.inspect}")
|
|
89
104
|
client
|
|
90
105
|
end
|
|
91
106
|
|
|
@@ -100,6 +115,9 @@ module Legion
|
|
|
100
115
|
def connect_kerberos_cluster(name, config)
|
|
101
116
|
krb_config = config[:kerberos] || {}
|
|
102
117
|
spn = krb_config[:service_principal]
|
|
118
|
+
auth_path = krb_config[:auth_path] || Legion::Crypt::KerberosAuth::DEFAULT_AUTH_PATH
|
|
119
|
+
|
|
120
|
+
log_vault_debug("connect_kerberos_cluster[#{name}]: SPN=#{spn}, auth_path=#{auth_path}, namespace=#{config[:namespace].inspect}")
|
|
103
121
|
|
|
104
122
|
unless spn
|
|
105
123
|
log_vault_warn(name, 'Kerberos auth missing service_principal, skipping')
|
|
@@ -108,10 +126,13 @@ module Legion
|
|
|
108
126
|
end
|
|
109
127
|
|
|
110
128
|
require 'legion/crypt/kerberos_auth'
|
|
129
|
+
client = vault_client(name)
|
|
130
|
+
log_vault_debug("connect_kerberos_cluster[#{name}]: client.namespace=#{client.respond_to?(:namespace) ? client.namespace.inspect : 'n/a'}")
|
|
131
|
+
|
|
111
132
|
result = Legion::Crypt::KerberosAuth.login(
|
|
112
|
-
vault_client:
|
|
133
|
+
vault_client: client,
|
|
113
134
|
service_principal: spn,
|
|
114
|
-
auth_path:
|
|
135
|
+
auth_path: auth_path
|
|
115
136
|
)
|
|
116
137
|
|
|
117
138
|
config[:token] = result[:token]
|
|
@@ -119,6 +140,7 @@ module Legion
|
|
|
119
140
|
config[:renewable] = result[:renewable]
|
|
120
141
|
config[:connected] = true
|
|
121
142
|
vault_client(name).token = result[:token]
|
|
143
|
+
log_vault_debug("connect_kerberos_cluster[#{name}]: policies=#{result[:policies].inspect}")
|
|
122
144
|
log_cluster_connected(name, config)
|
|
123
145
|
true
|
|
124
146
|
rescue Legion::Crypt::KerberosAuth::GemMissingError => e
|
|
@@ -142,6 +164,10 @@ module Legion
|
|
|
142
164
|
warn("Vault cluster #{name}: #{message}")
|
|
143
165
|
end
|
|
144
166
|
end
|
|
167
|
+
|
|
168
|
+
def log_vault_debug(message)
|
|
169
|
+
Legion::Logging.debug(message) if defined?(Legion::Logging)
|
|
170
|
+
end
|
|
145
171
|
end
|
|
146
172
|
end
|
|
147
173
|
end
|
|
@@ -39,7 +39,7 @@ module Legion
|
|
|
39
39
|
{
|
|
40
40
|
token: response.auth.client_token,
|
|
41
41
|
lease_duration: response.auth.lease_duration,
|
|
42
|
-
renewable: response.auth.renewable
|
|
42
|
+
renewable: response.auth.renewable?,
|
|
43
43
|
policies: response.auth.policies,
|
|
44
44
|
metadata: response.auth.metadata
|
|
45
45
|
}
|
|
@@ -16,7 +16,7 @@ module Legion
|
|
|
16
16
|
{
|
|
17
17
|
token: response.auth.client_token,
|
|
18
18
|
lease_duration: response.auth.lease_duration,
|
|
19
|
-
renewable: response.auth.renewable
|
|
19
|
+
renewable: response.auth.renewable?,
|
|
20
20
|
policies: response.auth.policies,
|
|
21
21
|
metadata: response.auth.metadata
|
|
22
22
|
}
|
data/lib/legion/crypt/version.rb
CHANGED
data/lib/legion/crypt.rb
CHANGED
|
@@ -123,7 +123,13 @@ module Legion
|
|
|
123
123
|
lease_manager = Legion::Crypt::LeaseManager.instance
|
|
124
124
|
lease_manager.start(leases, vault_client: client)
|
|
125
125
|
lease_manager.start_renewal_thread
|
|
126
|
-
|
|
126
|
+
fetched = lease_manager.fetched_count
|
|
127
|
+
defined = leases.size
|
|
128
|
+
if fetched == defined
|
|
129
|
+
Legion::Logging.info "LeaseManager: #{fetched} lease(s) initialized"
|
|
130
|
+
else
|
|
131
|
+
Legion::Logging.warn "LeaseManager: #{fetched}/#{defined} lease(s) initialized (#{defined - fetched} failed)"
|
|
132
|
+
end
|
|
127
133
|
rescue StandardError => e
|
|
128
134
|
Legion::Logging.warn "LeaseManager startup failed: #{e.message}"
|
|
129
135
|
end
|