legion-crypt 1.5.12 → 1.5.37
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +146 -1
- data/README.md +68 -14
- data/legion-crypt.gemspec +2 -1
- data/lib/legion/crypt/cert_rotation.rb +83 -65
- data/lib/legion/crypt/cipher.rb +5 -15
- data/lib/legion/crypt/cluster_secret.rb +71 -19
- data/lib/legion/crypt/erasure.rb +3 -10
- data/lib/legion/crypt/jwks_client.rb +43 -31
- data/lib/legion/crypt/jwt.rb +8 -7
- data/lib/legion/crypt/kerberos_auth.rb +18 -8
- data/lib/legion/crypt/ldap_auth.rb +14 -13
- data/lib/legion/crypt/lease_manager.rb +485 -335
- data/lib/legion/crypt/mtls.rb +3 -6
- data/lib/legion/crypt/partition_keys.rb +2 -0
- data/lib/legion/crypt/settings.rb +2 -1
- data/lib/legion/crypt/spiffe/workload_api_client.rb +83 -71
- data/lib/legion/crypt/token_renewer.rb +212 -82
- data/lib/legion/crypt/vault.rb +113 -83
- data/lib/legion/crypt/vault_client.rb +355 -0
- data/lib/legion/crypt/vault_cluster.rb +161 -39
- data/lib/legion/crypt/vault_entity.rb +21 -34
- data/lib/legion/crypt/vault_jwt_auth.rb +9 -20
- data/lib/legion/crypt/vault_kerberos_auth.rb +11 -14
- data/lib/legion/crypt/vault_operation.rb +27 -0
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +116 -63
- metadata +23 -9
- data/lib/legion/logging/helper.rb +0 -101
- data/lib/legion/logging.rb +0 -58
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'timeout'
|
|
4
6
|
require 'legion/logging/helper'
|
|
5
7
|
|
|
6
8
|
module Legion
|
|
7
9
|
module Crypt
|
|
8
10
|
module ClusterSecret
|
|
11
|
+
class InitializationError < StandardError; end
|
|
12
|
+
|
|
9
13
|
include Legion::Logging::Helper
|
|
10
14
|
|
|
11
15
|
def find_cluster_secret
|
|
12
|
-
|
|
16
|
+
secret_length
|
|
17
|
+
cluster_secret_timeout
|
|
18
|
+
raise InitializationError, 'Cluster secret initialization cancelled during shutdown' if Legion::Settings[:client]&.dig(:shutting_down)
|
|
19
|
+
|
|
20
|
+
%i[from_settings from_vault from_transport].each do |method|
|
|
21
|
+
break if method == :from_transport && only_member? == true
|
|
22
|
+
|
|
13
23
|
result = send(method)
|
|
14
24
|
next if result.nil?
|
|
15
25
|
|
|
16
26
|
unless validate_hex(result)
|
|
27
|
+
raise InitializationError, 'Configured cluster secret is not valid hexadecimal key material' if method == :from_settings
|
|
28
|
+
|
|
17
29
|
log.warn("Legion::Crypt.#{method} gave a value but it isn't a valid hex")
|
|
18
30
|
next
|
|
19
31
|
end
|
|
@@ -21,7 +33,7 @@ module Legion
|
|
|
21
33
|
set_cluster_secret(result, method != :from_vault)
|
|
22
34
|
return result
|
|
23
35
|
end
|
|
24
|
-
return unless only_member?
|
|
36
|
+
return unless only_member? == true
|
|
25
37
|
|
|
26
38
|
key = generate_secure_random
|
|
27
39
|
set_cluster_secret(key)
|
|
@@ -50,26 +62,36 @@ module Legion
|
|
|
50
62
|
alias cluster_secret from_settings
|
|
51
63
|
|
|
52
64
|
def from_transport
|
|
53
|
-
return nil unless Legion::Settings[:transport]
|
|
65
|
+
return nil unless Legion::Settings[:transport]&.dig(:connected)
|
|
66
|
+
return nil if defined?(Legion::Transport::TYPE) && Legion::Transport::TYPE == 'local'
|
|
54
67
|
|
|
55
68
|
require 'legion/transport/messages/request_cluster_secret'
|
|
56
69
|
log.info 'Requesting cluster secret via public key'
|
|
57
70
|
log.warn 'cluster_secret already set but we are requesting a new value' unless from_settings.nil?
|
|
58
|
-
start =
|
|
71
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
72
|
+
deadline = start + cluster_secret_timeout
|
|
59
73
|
Legion::Transport::Messages::RequestClusterSecret.new.publish
|
|
60
74
|
sleep_time = 0.001
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
while from_settings.nil?
|
|
76
|
+
raise InitializationError, 'Cluster secret initialization cancelled during shutdown' if Legion::Settings[:client]&.dig(:shutting_down)
|
|
77
|
+
|
|
78
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
79
|
+
break unless remaining.positive?
|
|
80
|
+
|
|
81
|
+
sleep([sleep_time, remaining].min)
|
|
82
|
+
sleep_time = [sleep_time * 2, 0.5].min
|
|
64
83
|
end
|
|
65
84
|
|
|
66
85
|
unless from_settings.nil?
|
|
67
|
-
|
|
86
|
+
elapsed = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
|
|
87
|
+
log.info "Received cluster secret in #{(elapsed * 1000.0).round}ms"
|
|
68
88
|
return from_settings
|
|
69
89
|
end
|
|
70
90
|
|
|
71
91
|
log.error 'Cluster secret is still unknown!'
|
|
72
92
|
nil
|
|
93
|
+
rescue InitializationError
|
|
94
|
+
raise
|
|
73
95
|
rescue StandardError => e
|
|
74
96
|
handle_exception(e, level: :error, operation: 'crypt.cluster_secret.from_transport')
|
|
75
97
|
nil
|
|
@@ -87,7 +109,16 @@ module Legion
|
|
|
87
109
|
end
|
|
88
110
|
|
|
89
111
|
def only_member?
|
|
90
|
-
Legion::Transport::
|
|
112
|
+
return true if defined?(Legion::Transport::TYPE) && Legion::Transport::TYPE == 'local'
|
|
113
|
+
|
|
114
|
+
transport = Legion::Settings[:transport]
|
|
115
|
+
return Legion::Transport::Queue.new('node.crypt', passive: true).consumer_count.zero? if transport&.dig(:connected)
|
|
116
|
+
|
|
117
|
+
return true unless defined?(Legion::Transport)
|
|
118
|
+
return true if transport&.dig(:enabled) == false
|
|
119
|
+
return true if defined?(Legion::Mode) ? Legion::Mode.lite? : ENV['LEGION_MODE'] == 'lite'
|
|
120
|
+
|
|
121
|
+
nil
|
|
91
122
|
rescue StandardError => e
|
|
92
123
|
handle_exception(e, level: :warn, operation: 'crypt.cluster_secret.only_member?')
|
|
93
124
|
nil
|
|
@@ -96,8 +127,8 @@ module Legion
|
|
|
96
127
|
def set_cluster_secret(value, push_to_vault = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
97
128
|
raise TypeError unless validate_hex(value)
|
|
98
129
|
|
|
99
|
-
Legion::Settings[:crypt][:cluster_secret] = value
|
|
100
|
-
@
|
|
130
|
+
Legion::Settings[:crypt][:cluster_secret] = value.dup.freeze
|
|
131
|
+
@cluster_secret_cache = nil
|
|
101
132
|
Legion::Settings[:crypt][:cs_encrypt_ready] = true
|
|
102
133
|
push_cs_to_vault if push_to_vault && settings_push_vault
|
|
103
134
|
log.info "Cluster secret loaded into settings push_to_vault=#{push_to_vault}"
|
|
@@ -114,11 +145,19 @@ module Legion
|
|
|
114
145
|
end
|
|
115
146
|
|
|
116
147
|
def cluster_secret_timeout
|
|
117
|
-
Legion::Settings[:crypt]
|
|
148
|
+
timeout = Legion::Settings[:crypt].fetch(:cluster_secret_timeout, 5)
|
|
149
|
+
unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
150
|
+
raise InitializationError, 'cluster_secret_timeout must be a finite positive number'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
timeout
|
|
118
154
|
end
|
|
119
155
|
|
|
120
156
|
def secret_length
|
|
121
|
-
Legion::Settings[:crypt][:cluster_length] || Legion::Settings[:crypt][:cluster_lenth] || 32
|
|
157
|
+
length = Legion::Settings[:crypt][:cluster_length] || Legion::Settings[:crypt][:cluster_lenth] || 32
|
|
158
|
+
raise InitializationError, 'cluster_length must be a positive integer' unless length.is_a?(Integer) && length.positive?
|
|
159
|
+
|
|
160
|
+
length
|
|
122
161
|
end
|
|
123
162
|
|
|
124
163
|
def generate_secure_random(length = secret_length)
|
|
@@ -126,10 +165,26 @@ module Legion
|
|
|
126
165
|
end
|
|
127
166
|
|
|
128
167
|
def cs
|
|
129
|
-
|
|
168
|
+
secret_length
|
|
169
|
+
raise InitializationError, 'Cluster secret initialization cancelled during shutdown' if Legion::Settings[:client]&.dig(:shutting_down)
|
|
170
|
+
|
|
171
|
+
source = from_settings || Timeout.timeout(cluster_secret_timeout) { find_cluster_secret }
|
|
172
|
+
raise InitializationError, 'No validated cluster secret is available' unless validate_hex(source)
|
|
173
|
+
|
|
174
|
+
Legion::Settings[:crypt][:cs_encrypt_ready] = true
|
|
175
|
+
cached = @cluster_secret_cache
|
|
176
|
+
return cached[:key] if cached && cached[:secret] == source
|
|
177
|
+
|
|
178
|
+
source = source.dup.freeze
|
|
179
|
+
key = Digest::SHA256.digest(source)
|
|
180
|
+
@cluster_secret_cache = { secret: source, key: key }.freeze
|
|
181
|
+
key
|
|
130
182
|
rescue StandardError => e
|
|
183
|
+
Legion::Settings[:crypt][:cs_encrypt_ready] = false
|
|
131
184
|
handle_exception(e, level: :error, operation: 'crypt.cluster_secret.cs')
|
|
132
|
-
|
|
185
|
+
raise if e.is_a?(InitializationError)
|
|
186
|
+
|
|
187
|
+
raise InitializationError, 'Cluster secret initialization failed'
|
|
133
188
|
end
|
|
134
189
|
|
|
135
190
|
def validate_hex(value, length = secret_length)
|
|
@@ -137,10 +192,7 @@ module Legion
|
|
|
137
192
|
return false if value.empty?
|
|
138
193
|
return false unless value.match?(/\A\h+\z/)
|
|
139
194
|
|
|
140
|
-
|
|
141
|
-
return true if expected_length.zero?
|
|
142
|
-
|
|
143
|
-
value.length == expected_length
|
|
195
|
+
length.is_a?(Integer) && length.positive? && value.length == length * 2
|
|
144
196
|
end
|
|
145
197
|
|
|
146
198
|
def cluster_secret_vault_path
|
data/lib/legion/crypt/erasure.rb
CHANGED
|
@@ -12,11 +12,9 @@ module Legion
|
|
|
12
12
|
key_path = "#{tenant_prefix}/#{tenant_id}/master_key"
|
|
13
13
|
|
|
14
14
|
log.info "[crypt] Erasing tenant #{tenant_id}"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
delete_vault_key(key_path)
|
|
19
|
-
end
|
|
15
|
+
result = Legion::Crypt.delete(key_path)
|
|
16
|
+
raise(result[:error] || 'Vault key deletion failed') unless result[:success]
|
|
17
|
+
|
|
20
18
|
Legion::Events.emit('crypt.tenant_erased', { tenant_id: tenant_id, erased_at: Time.now.utc }) if defined?(Legion::Events)
|
|
21
19
|
log.warn "[crypt] Tenant #{tenant_id} cryptographically erased"
|
|
22
20
|
|
|
@@ -28,7 +26,6 @@ module Legion
|
|
|
28
26
|
|
|
29
27
|
def verify_erasure(tenant_id:)
|
|
30
28
|
key_path = "#{tenant_prefix}/#{tenant_id}/master_key"
|
|
31
|
-
raise 'Legion::Crypt.read is unavailable' unless Legion::Crypt.respond_to?(:read)
|
|
32
29
|
|
|
33
30
|
data = Legion::Crypt.read(key_path, nil)
|
|
34
31
|
erased = data.nil?
|
|
@@ -41,10 +38,6 @@ module Legion
|
|
|
41
38
|
|
|
42
39
|
private
|
|
43
40
|
|
|
44
|
-
def delete_vault_key(path)
|
|
45
|
-
::Vault.logical.delete(path)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
41
|
def tenant_prefix
|
|
49
42
|
begin
|
|
50
43
|
Legion::Settings[:crypt][:partition_keys][:vault_tenant_prefix]
|
|
@@ -12,6 +12,7 @@ module Legion
|
|
|
12
12
|
module Crypt
|
|
13
13
|
module JwksClient
|
|
14
14
|
CACHE_TTL = 3600
|
|
15
|
+
MISS_REFRESH_COOLDOWN = 30
|
|
15
16
|
|
|
16
17
|
@cache = {}
|
|
17
18
|
@cache_mutex = Mutex.new
|
|
@@ -22,39 +23,30 @@ module Legion
|
|
|
22
23
|
include Legion::Logging::Helper
|
|
23
24
|
|
|
24
25
|
def fetch_keys(jwks_url)
|
|
25
|
-
with_url_lock(jwks_url)
|
|
26
|
-
log.debug "JWKS fetch: #{jwks_url}"
|
|
27
|
-
response = http_get(jwks_url)
|
|
28
|
-
jwks_data = parse_response(response)
|
|
29
|
-
keys = parse_jwks(jwks_data)
|
|
30
|
-
|
|
31
|
-
cache_write(jwks_url, keys)
|
|
32
|
-
log.info "JWKS fetched url=#{jwks_url} keys=#{keys.size}"
|
|
33
|
-
keys
|
|
34
|
-
end
|
|
35
|
-
rescue StandardError => e
|
|
36
|
-
handle_exception(e, level: :warn, operation: 'crypt.jwks.fetch_keys', jwks_url: jwks_url)
|
|
37
|
-
raise
|
|
26
|
+
with_url_lock(jwks_url) { refresh_keys(jwks_url: jwks_url) }
|
|
38
27
|
end
|
|
39
28
|
|
|
40
29
|
def find_key(jwks_url, kid)
|
|
41
30
|
cached = cache_read(jwks_url)
|
|
31
|
+
return cached[:keys][kid] if cached && !expired?(cached[:fetched_at]) && cached[:keys][kid]
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return key
|
|
48
|
-
end
|
|
33
|
+
with_url_lock(jwks_url) do
|
|
34
|
+
cached = cache_read(jwks_url)
|
|
35
|
+
fresh = cached && !expired?(cached[:fetched_at])
|
|
36
|
+
return cached[:keys][kid] if fresh && cached[:keys][kid]
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
if cached && cached[:refresh_after] && (fresh || cached[:error]) &&
|
|
39
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC) < cached[:refresh_after]
|
|
40
|
+
raise cached[:error] if cached[:error]
|
|
41
|
+
|
|
42
|
+
raise Legion::Crypt::JWT::InvalidTokenError, "signing key not found: #{kid}"
|
|
43
|
+
end
|
|
52
44
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return key if key
|
|
45
|
+
keys = refresh_keys(jwks_url: jwks_url, requested_kid: kid)
|
|
46
|
+
return keys[kid] if keys[kid]
|
|
56
47
|
|
|
57
|
-
|
|
48
|
+
raise Legion::Crypt::JWT::InvalidTokenError, "signing key not found: #{kid}"
|
|
49
|
+
end
|
|
58
50
|
end
|
|
59
51
|
|
|
60
52
|
def prefetch!(jwks_url)
|
|
@@ -87,24 +79,44 @@ module Legion
|
|
|
87
79
|
def clear_cache
|
|
88
80
|
stop_background_refresh!
|
|
89
81
|
@cache_mutex.synchronize { @cache = {} }
|
|
90
|
-
@locks_mutex.synchronize { @locks = {} }
|
|
91
82
|
log.info 'JWKS cache cleared'
|
|
92
83
|
end
|
|
93
84
|
|
|
94
85
|
private
|
|
95
86
|
|
|
87
|
+
def refresh_keys(jwks_url:, requested_kid: nil, **)
|
|
88
|
+
previous = cache_read(jwks_url)
|
|
89
|
+
refreshing_miss = requested_kid && previous && !expired?(previous[:fetched_at])
|
|
90
|
+
log.debug "JWKS fetch: #{jwks_url}"
|
|
91
|
+
keys = parse_jwks(parse_response(http_get(jwks_url)))
|
|
92
|
+
entry = { keys: keys, fetched_at: Time.now }
|
|
93
|
+
if requested_kid && (refreshing_miss || !keys.key?(requested_kid))
|
|
94
|
+
entry[:refresh_after] = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + MISS_REFRESH_COOLDOWN
|
|
95
|
+
end
|
|
96
|
+
cache_write(jwks_url, entry)
|
|
97
|
+
log.info "JWKS fetched url=#{jwks_url} keys=#{keys.size}"
|
|
98
|
+
keys
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
if requested_kid
|
|
101
|
+
entry = (previous || { keys: {}, fetched_at: nil }).merge(
|
|
102
|
+
error: e, refresh_after: ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + MISS_REFRESH_COOLDOWN
|
|
103
|
+
)
|
|
104
|
+
cache_write(jwks_url, entry)
|
|
105
|
+
end
|
|
106
|
+
handle_exception(e, level: :warn, operation: 'crypt.jwks.fetch_keys', jwks_url: jwks_url)
|
|
107
|
+
raise
|
|
108
|
+
end
|
|
109
|
+
|
|
96
110
|
def cache_read(jwks_url)
|
|
97
111
|
@cache_mutex.synchronize { @cache[jwks_url] }
|
|
98
112
|
end
|
|
99
113
|
|
|
100
|
-
def cache_write(jwks_url,
|
|
101
|
-
@cache_mutex.synchronize
|
|
102
|
-
@cache[jwks_url] = { keys: keys, fetched_at: Time.now }
|
|
103
|
-
end
|
|
114
|
+
def cache_write(jwks_url, entry)
|
|
115
|
+
@cache_mutex.synchronize { @cache[jwks_url] = entry }
|
|
104
116
|
end
|
|
105
117
|
|
|
106
118
|
def expired?(fetched_at)
|
|
107
|
-
Time.now - fetched_at > CACHE_TTL
|
|
119
|
+
fetched_at.nil? || Time.now - fetched_at > CACHE_TTL
|
|
108
120
|
end
|
|
109
121
|
|
|
110
122
|
def http_get(url)
|
data/lib/legion/crypt/jwt.rb
CHANGED
|
@@ -109,18 +109,16 @@ module Legion
|
|
|
109
109
|
def self.verify_with_jwks(token, jwks_url:, **opts)
|
|
110
110
|
header = decode_header(token)
|
|
111
111
|
kid = header['kid']
|
|
112
|
-
algorithm = header['alg']
|
|
112
|
+
algorithm = header['alg']
|
|
113
113
|
|
|
114
|
-
raise InvalidTokenError, 'token header missing kid' unless kid
|
|
115
|
-
|
|
116
|
-
validate_algorithm!(algorithm)
|
|
117
|
-
|
|
118
|
-
public_key = Legion::Crypt::JwksClient.find_key(jwks_url, kid)
|
|
114
|
+
raise InvalidTokenError, 'token header missing or invalid kid' unless kid.is_a?(String) && !kid.empty?
|
|
115
|
+
raise InvalidTokenError, 'unsupported token algorithm' unless SUPPORTED_ALGORITHMS.include?(algorithm)
|
|
119
116
|
|
|
120
117
|
verify_expiration = opts.fetch(:verify_expiration, true)
|
|
121
118
|
issuers = opts[:issuers]
|
|
122
119
|
audience = opts[:audience]
|
|
123
120
|
validate_external_requirements!(issuers: issuers, audience: audience)
|
|
121
|
+
public_key = Legion::Crypt::JwksClient.find_key(jwks_url, kid)
|
|
124
122
|
|
|
125
123
|
decode_opts = {
|
|
126
124
|
algorithm: algorithm,
|
|
@@ -165,7 +163,10 @@ module Legion
|
|
|
165
163
|
raise DecodeError, 'invalid token format' unless parts.size == 3
|
|
166
164
|
|
|
167
165
|
header_json = Base64.urlsafe_decode64(parts[0])
|
|
168
|
-
::JSON.parse(header_json)
|
|
166
|
+
header = ::JSON.parse(header_json)
|
|
167
|
+
raise DecodeError, 'JWT header must be an object' unless header.is_a?(Hash)
|
|
168
|
+
|
|
169
|
+
header
|
|
169
170
|
rescue ::JSON::ParserError, ArgumentError => e
|
|
170
171
|
handle_exception(e, level: :warn, operation: 'crypt.jwt.decode_header')
|
|
171
172
|
raise DecodeError, "failed to decode token header: #{e.message}"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault_client'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
@@ -17,7 +18,7 @@ module Legion
|
|
|
17
18
|
attr_reader :kerberos_principal
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
def self.login(vault_client:, service_principal:, auth_path: DEFAULT_AUTH_PATH)
|
|
21
|
+
def self.login(vault_client:, service_principal:, auth_path: DEFAULT_AUTH_PATH, principal_guard: nil)
|
|
21
22
|
raise GemMissingError, 'lex-kerberos gem is required for Kerberos auth' unless spnego_available?
|
|
22
23
|
|
|
23
24
|
log.info "KerberosAuth login requested auth_path=#{auth_path}"
|
|
@@ -26,12 +27,21 @@ module Legion
|
|
|
26
27
|
ns = vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'
|
|
27
28
|
log.debug("KerberosAuth: login: vault_client.address=#{addr}, namespace=#{ns}")
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
if principal_guard
|
|
31
|
+
principal_guard.call { @kerberos_principal = nil }
|
|
32
|
+
else
|
|
33
|
+
@kerberos_principal = nil
|
|
34
|
+
end
|
|
30
35
|
token = obtain_token(service_principal)
|
|
31
36
|
log.debug("KerberosAuth: login: SPNEGO token obtained (#{token.length} chars)")
|
|
32
37
|
|
|
33
38
|
result = exchange_token(vault_client, token, auth_path)
|
|
34
|
-
|
|
39
|
+
principal = result[:metadata]&.dig('username') || result[:metadata]&.dig(:username)
|
|
40
|
+
if principal_guard
|
|
41
|
+
principal_guard.call { @kerberos_principal = principal }
|
|
42
|
+
else
|
|
43
|
+
@kerberos_principal = principal
|
|
44
|
+
end
|
|
35
45
|
log.debug("KerberosAuth: login: authenticated as #{@kerberos_principal.inspect}, policies=#{result[:policies].inspect}")
|
|
36
46
|
log.debug("KerberosAuth: login: renewable=#{result[:renewable]}, ttl=#{result[:lease_duration]}s")
|
|
37
47
|
log.info "KerberosAuth login success principal=#{@kerberos_principal || 'unknown'} auth_path=#{auth_path}"
|
|
@@ -80,11 +90,11 @@ module Legion
|
|
|
80
90
|
namespace = vault_client.respond_to?(:namespace) ? vault_client.namespace.inspect : 'n/a'
|
|
81
91
|
log.debug("KerberosAuth: exchange_token: PUT /v1/#{auth_path} (namespace=#{namespace})")
|
|
82
92
|
json = vault_client.put(
|
|
83
|
-
"/v1/#{auth_path}",
|
|
84
|
-
'{}',
|
|
85
|
-
'Authorization' => "Negotiate #{spnego_token}"
|
|
93
|
+
path: "/v1/#{auth_path}",
|
|
94
|
+
body: '{}',
|
|
95
|
+
headers: { 'Authorization' => "Negotiate #{spnego_token}" }
|
|
86
96
|
)
|
|
87
|
-
response = ::
|
|
97
|
+
response = Legion::Crypt::VaultClient::Secret.decode(data: json)
|
|
88
98
|
raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
|
|
89
99
|
|
|
90
100
|
auth = response.auth
|
|
@@ -95,7 +105,7 @@ module Legion
|
|
|
95
105
|
policies: auth.policies,
|
|
96
106
|
metadata: auth.metadata
|
|
97
107
|
}
|
|
98
|
-
rescue ::Vault::HTTPClientError => e
|
|
108
|
+
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
99
109
|
handle_exception(e, level: :warn, operation: 'crypt.kerberos_auth.exchange_token', auth_path: auth_path)
|
|
100
110
|
log.debug("KerberosAuth: exchange_token: HTTP error: #{e.message}")
|
|
101
111
|
raise AuthError, "Vault Kerberos auth failed: #{e.message}"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault_operation'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
@@ -9,35 +10,35 @@ module Legion
|
|
|
9
10
|
|
|
10
11
|
def ldap_login(cluster_name:, username:, password:)
|
|
11
12
|
cluster_name = cluster_name.to_sym
|
|
13
|
+
generation = vault_auth_generation
|
|
14
|
+
config = clusters.fetch(cluster_name)
|
|
12
15
|
log.info "LDAP login requested user=#{username} cluster=#{cluster_name}"
|
|
13
16
|
client = vault_client(cluster_name)
|
|
14
|
-
secret =
|
|
17
|
+
secret = Legion::Crypt::VaultOperation.with_timeout do
|
|
18
|
+
client.logical.write(path: "auth/ldap/login/#{username}", data: { password: password })
|
|
19
|
+
end
|
|
15
20
|
auth = secret.auth
|
|
16
|
-
token = auth.client_token
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
client.token = token if client.respond_to?(:token=)
|
|
22
|
+
grant = { token: auth.client_token, lease_duration: auth.lease_duration, renewable: auth.renewable?, policies: auth.policies }
|
|
23
|
+
install_vault_grant(client: client, config: config, grant: grant, auth_method: 'ldap', generation: generation)
|
|
21
24
|
|
|
22
25
|
log.info "LDAP login success: user=#{username}, cluster=#{cluster_name}"
|
|
23
|
-
|
|
24
|
-
renewable: auth.renewable?, policies: auth.policies }
|
|
26
|
+
grant
|
|
25
27
|
rescue StandardError => e
|
|
26
|
-
|
|
27
|
-
log.error "LDAP login failed: user=#{username}, cluster=#{cluster_name}: #{e.message}"
|
|
28
|
+
log.error "LDAP login failed: user=#{username}, cluster=#{cluster_name}, error=#{e.class}"
|
|
28
29
|
raise
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def ldap_login_all(username:, password:)
|
|
32
33
|
results = {}
|
|
33
34
|
clusters.each do |name, config|
|
|
34
|
-
next unless config[:auth_method] == 'ldap'
|
|
35
|
+
next unless config[:auth_method].to_s == 'ldap'
|
|
35
36
|
|
|
36
37
|
results[name] = ldap_login(cluster_name: name, username: username, password: password)
|
|
37
38
|
rescue StandardError => e
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
results[name] = { error:
|
|
39
|
+
log.warn("Legion::Crypt::LdapAuth#ldap_login_all cluster=#{name} failed: #{e.class}")
|
|
40
|
+
message = password.to_s.empty? ? e.message : e.message.gsub(password.to_s, '[REDACTED]')
|
|
41
|
+
results[name] = { error: message }
|
|
41
42
|
end
|
|
42
43
|
log.info "LDAP login_all complete successes=#{results.count { |_, result| result.is_a?(Hash) && !result.key?(:error) }} attempted=#{results.size}"
|
|
43
44
|
results
|