legion-crypt 1.4.28 → 1.5.0
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 +29 -0
- data/Gemfile +1 -0
- data/legion-crypt.gemspec +1 -0
- data/lib/legion/crypt/attestation.rb +18 -8
- data/lib/legion/crypt/cert_rotation.rb +54 -42
- data/lib/legion/crypt/cipher.rb +106 -15
- data/lib/legion/crypt/cluster_secret.rb +41 -39
- data/lib/legion/crypt/ed25519.rb +58 -18
- data/lib/legion/crypt/erasure.rb +21 -8
- data/lib/legion/crypt/jwks_client.rb +37 -9
- data/lib/legion/crypt/jwt.rb +75 -31
- data/lib/legion/crypt/kerberos_auth.rb +23 -13
- data/lib/legion/crypt/ldap_auth.rb +12 -4
- data/lib/legion/crypt/lease_manager.rb +126 -73
- data/lib/legion/crypt/mtls.rb +14 -1
- data/lib/legion/crypt/partition_keys.rb +15 -5
- data/lib/legion/crypt/settings.rb +18 -12
- data/lib/legion/crypt/spiffe/identity_helpers.rb +18 -11
- data/lib/legion/crypt/spiffe/svid_rotation.rb +23 -33
- data/lib/legion/crypt/spiffe/workload_api_client.rb +61 -17
- data/lib/legion/crypt/spiffe.rb +18 -4
- data/lib/legion/crypt/tls.rb +14 -10
- data/lib/legion/crypt/token_renewer.rb +29 -26
- data/lib/legion/crypt/vault.rb +57 -45
- data/lib/legion/crypt/vault_cluster.rb +35 -17
- data/lib/legion/crypt/vault_jwt_auth.rb +17 -4
- data/lib/legion/crypt/vault_kerberos_auth.rb +11 -1
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +69 -32
- data/lib/legion/logging/helper.rb +98 -0
- data/lib/legion/logging.rb +58 -0
- metadata +17 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Crypt
|
|
5
7
|
module Spiffe
|
|
@@ -10,6 +12,8 @@ module Legion
|
|
|
10
12
|
# defaults to 60 seconds; renewal fires when the SVID is past 50%
|
|
11
13
|
# of its lifetime (configurable via security.spiffe.renewal_window).
|
|
12
14
|
class SvidRotation
|
|
15
|
+
include Legion::Logging::Helper
|
|
16
|
+
|
|
13
17
|
DEFAULT_CHECK_INTERVAL = 60
|
|
14
18
|
|
|
15
19
|
attr_reader :check_interval, :current_svid
|
|
@@ -31,19 +35,24 @@ module Legion
|
|
|
31
35
|
@running = true
|
|
32
36
|
@thread = Thread.new { rotation_loop }
|
|
33
37
|
@thread.name = 'spiffe-svid-rotation'
|
|
34
|
-
|
|
38
|
+
log.info '[SPIFFE] SvidRotation started'
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def stop
|
|
38
42
|
@running = false
|
|
39
43
|
begin
|
|
40
44
|
@thread&.wakeup
|
|
41
|
-
rescue ThreadError
|
|
45
|
+
rescue ThreadError => e
|
|
46
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.svid_rotation.stop')
|
|
42
47
|
nil
|
|
43
48
|
end
|
|
44
49
|
@thread&.join(3)
|
|
45
|
-
@thread
|
|
46
|
-
|
|
50
|
+
if @thread&.alive?
|
|
51
|
+
log.warn '[SPIFFE] SvidRotation thread did not stop within timeout'
|
|
52
|
+
else
|
|
53
|
+
@thread = nil
|
|
54
|
+
end
|
|
55
|
+
log.info '[SPIFFE] SvidRotation stopped'
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
def running?
|
|
@@ -56,7 +65,7 @@ module Legion
|
|
|
56
65
|
@current_svid = svid
|
|
57
66
|
@issued_at = Time.now
|
|
58
67
|
end
|
|
59
|
-
|
|
68
|
+
log.info("[SPIFFE] SVID rotated: id=#{svid.spiffe_id} expiry=#{svid.expiry}")
|
|
60
69
|
svid
|
|
61
70
|
end
|
|
62
71
|
|
|
@@ -85,7 +94,8 @@ module Legion
|
|
|
85
94
|
begin
|
|
86
95
|
rotate!
|
|
87
96
|
rescue StandardError => e
|
|
88
|
-
|
|
97
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.svid_rotation.rotation_loop')
|
|
98
|
+
log.error("[SPIFFE] Initial SVID fetch failed: #{e.message}")
|
|
89
99
|
end
|
|
90
100
|
loop_check
|
|
91
101
|
end
|
|
@@ -96,13 +106,16 @@ module Legion
|
|
|
96
106
|
next unless @running && needs_renewal?
|
|
97
107
|
|
|
98
108
|
begin
|
|
109
|
+
log.info('[SPIFFE] SVID renewal window reached, rotating current SVID')
|
|
99
110
|
rotate!
|
|
100
111
|
rescue StandardError => e
|
|
101
|
-
|
|
112
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.svid_rotation.loop_check')
|
|
113
|
+
log.error("[SPIFFE] SVID rotation failed: #{e.message}")
|
|
102
114
|
end
|
|
103
115
|
end
|
|
104
116
|
rescue StandardError => e
|
|
105
|
-
|
|
117
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.svid_rotation.loop_check')
|
|
118
|
+
log.error("[SPIFFE] SvidRotation loop error: #{e.message}")
|
|
106
119
|
retry if @running
|
|
107
120
|
end
|
|
108
121
|
|
|
@@ -124,33 +137,10 @@ module Legion
|
|
|
124
137
|
|
|
125
138
|
spiffe = security[:spiffe] || security['spiffe'] || {}
|
|
126
139
|
spiffe[:renewal_window] || spiffe['renewal_window'] || SVID_RENEWAL_RATIO
|
|
127
|
-
rescue StandardError
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.svid_rotation.renewal_window')
|
|
128
142
|
SVID_RENEWAL_RATIO
|
|
129
143
|
end
|
|
130
|
-
|
|
131
|
-
def log_info(msg)
|
|
132
|
-
if defined?(Legion::Logging)
|
|
133
|
-
Legion::Logging.info(msg)
|
|
134
|
-
else
|
|
135
|
-
$stdout.puts(msg)
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def log_debug(msg)
|
|
140
|
-
if defined?(Legion::Logging)
|
|
141
|
-
Legion::Logging.debug(msg)
|
|
142
|
-
else
|
|
143
|
-
$stdout.puts("[DEBUG] #{msg}")
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def log_warn(msg)
|
|
148
|
-
if defined?(Legion::Logging)
|
|
149
|
-
Legion::Logging.warn(msg)
|
|
150
|
-
else
|
|
151
|
-
warn("[WARN] #{msg}")
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
144
|
end
|
|
155
145
|
end
|
|
156
146
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
3
4
|
require 'socket'
|
|
4
5
|
require 'openssl'
|
|
5
6
|
|
|
@@ -17,6 +18,8 @@ module Legion
|
|
|
17
18
|
# no socket present) the client returns a self-signed fallback SVID so
|
|
18
19
|
# that callers never have to special-case the nil case.
|
|
19
20
|
class WorkloadApiClient
|
|
21
|
+
include Legion::Logging::Helper
|
|
22
|
+
|
|
20
23
|
# gRPC content-type and method path for the Workload API FetchX509SVID RPC.
|
|
21
24
|
GRPC_CONTENT_TYPE = 'application/grpc'
|
|
22
25
|
FETCH_X509_METHOD = '/spiffe.workload.SpiffeWorkloadAPI/FetchX509SVID'
|
|
@@ -24,34 +27,56 @@ module Legion
|
|
|
24
27
|
|
|
25
28
|
# Handshake + settings frames required to open an HTTP/2 connection.
|
|
26
29
|
HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
|
27
|
-
HTTP2_SETTINGS_FRAME =
|
|
30
|
+
HTTP2_SETTINGS_FRAME = "\x00\x00\x00\x04\x00\x00\x00\x00\x00".b
|
|
28
31
|
|
|
29
32
|
CONNECT_TIMEOUT = 5
|
|
30
33
|
READ_TIMEOUT = 10
|
|
31
34
|
|
|
32
|
-
def initialize(socket_path: nil, trust_domain: nil)
|
|
33
|
-
@socket_path
|
|
34
|
-
@trust_domain
|
|
35
|
+
def initialize(socket_path: nil, trust_domain: nil, allow_x509_fallback: nil)
|
|
36
|
+
@socket_path = socket_path || Legion::Crypt::Spiffe.socket_path
|
|
37
|
+
@trust_domain = trust_domain || Legion::Crypt::Spiffe.trust_domain
|
|
38
|
+
@allow_x509_fallback = allow_x509_fallback.nil? ? Legion::Crypt::Spiffe.allow_x509_fallback? : allow_x509_fallback
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
# Fetch an X.509 SVID from the SPIRE Workload API.
|
|
38
42
|
# Returns a populated X509Svid struct.
|
|
39
43
|
# Falls back to a self-signed certificate when the Workload API is unavailable.
|
|
40
44
|
def fetch_x509_svid
|
|
45
|
+
log.info("[SPIFFE] Fetching X.509 SVID from Workload API socket=#{@socket_path}")
|
|
41
46
|
raw = call_workload_api(FETCH_X509_METHOD, '')
|
|
42
47
|
parse_x509_svid_response(raw)
|
|
43
48
|
rescue WorkloadApiError, IOError, Errno::ENOENT, Errno::ECONNREFUSED, Errno::EPIPE => e
|
|
44
|
-
|
|
49
|
+
handle_exception(e, level: :warn, operation: 'crypt.spiffe.workload_api_client.fetch_x509_svid',
|
|
50
|
+
socket_path: @socket_path, fallback: @allow_x509_fallback)
|
|
51
|
+
unless @allow_x509_fallback
|
|
52
|
+
log.error("[SPIFFE] Workload API unavailable (#{e.message}); X.509 fallback disabled")
|
|
53
|
+
raise SvidError, "Failed to fetch X.509 SVID: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
log.warn("[SPIFFE] Workload API unavailable (#{e.message}); using self-signed fallback")
|
|
45
57
|
self_signed_fallback
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.workload_api_client.fetch_x509_svid',
|
|
60
|
+
socket_path: @socket_path)
|
|
61
|
+
log.error("[SPIFFE] X.509 SVID fetch failed: #{e.message}")
|
|
62
|
+
raise
|
|
46
63
|
end
|
|
47
64
|
|
|
48
65
|
# Fetch a JWT SVID from the SPIRE Workload API for the given audience.
|
|
49
66
|
def fetch_jwt_svid(audience:)
|
|
67
|
+
log.info("[SPIFFE] Fetching JWT SVID from Workload API audience=#{audience}")
|
|
50
68
|
payload = encode_jwt_request(audience)
|
|
51
69
|
raw = call_workload_api(FETCH_JWT_METHOD, payload)
|
|
52
70
|
parse_jwt_svid_response(raw, audience)
|
|
53
71
|
rescue WorkloadApiError, IOError, Errno::ENOENT, Errno::ECONNREFUSED, Errno::EPIPE => e
|
|
54
|
-
|
|
72
|
+
handle_exception(e, level: :warn, operation: 'crypt.spiffe.workload_api_client.fetch_jwt_svid',
|
|
73
|
+
socket_path: @socket_path, audience: audience)
|
|
74
|
+
log.warn("[SPIFFE] JWT SVID fetch failed (#{e.message})")
|
|
75
|
+
raise SvidError, "Failed to fetch JWT SVID for audience '#{audience}': #{e.message}"
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.workload_api_client.fetch_jwt_svid',
|
|
78
|
+
socket_path: @socket_path, audience: audience)
|
|
79
|
+
log.error("[SPIFFE] JWT SVID fetch failed: #{e.message}")
|
|
55
80
|
raise SvidError, "Failed to fetch JWT SVID for audience '#{audience}': #{e.message}"
|
|
56
81
|
end
|
|
57
82
|
|
|
@@ -62,7 +87,9 @@ module Legion
|
|
|
62
87
|
sock = UNIXSocket.new(@socket_path)
|
|
63
88
|
sock.close
|
|
64
89
|
true
|
|
65
|
-
rescue StandardError
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.workload_api_client.available',
|
|
92
|
+
socket_path: @socket_path)
|
|
66
93
|
false
|
|
67
94
|
end
|
|
68
95
|
|
|
@@ -71,6 +98,7 @@ module Legion
|
|
|
71
98
|
# Minimal HTTP/2 + gRPC unary call over a Unix domain socket.
|
|
72
99
|
# This is intentionally simple: one request frame, one response frame.
|
|
73
100
|
def call_workload_api(method_path, request_body)
|
|
101
|
+
log.debug("[SPIFFE] Calling Workload API method=#{method_path}")
|
|
74
102
|
sock = connect_socket
|
|
75
103
|
begin
|
|
76
104
|
send_grpc_request(sock, method_path, request_body)
|
|
@@ -89,9 +117,13 @@ module Legion
|
|
|
89
117
|
sock.write(HTTP2_SETTINGS_FRAME)
|
|
90
118
|
sock.flush
|
|
91
119
|
sock
|
|
92
|
-
rescue Errno::ENOENT
|
|
120
|
+
rescue Errno::ENOENT => e
|
|
121
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.workload_api_client.connect_socket',
|
|
122
|
+
socket_path: @socket_path)
|
|
93
123
|
raise WorkloadApiError, "SPIRE agent socket not found at '#{@socket_path}'"
|
|
94
124
|
rescue Errno::ECONNREFUSED, Errno::EACCES => e
|
|
125
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.workload_api_client.connect_socket',
|
|
126
|
+
socket_path: @socket_path)
|
|
95
127
|
raise WorkloadApiError, "Cannot connect to SPIRE agent socket: #{e.message}"
|
|
96
128
|
end
|
|
97
129
|
|
|
@@ -250,9 +282,11 @@ module Legion
|
|
|
250
282
|
cert_pem: cert.to_pem,
|
|
251
283
|
key_pem: key.private_to_pem,
|
|
252
284
|
bundle_pem: bundle_pem,
|
|
253
|
-
expiry: cert.not_after
|
|
285
|
+
expiry: cert.not_after,
|
|
286
|
+
source: :spire
|
|
254
287
|
)
|
|
255
288
|
rescue OpenSSL::X509::CertificateError, OpenSSL::PKey::PKeyError => e
|
|
289
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.workload_api_client.parse_x509_svid_response')
|
|
256
290
|
raise SvidError, "Failed to parse X.509 SVID: #{e.message}"
|
|
257
291
|
end
|
|
258
292
|
|
|
@@ -277,14 +311,20 @@ module Legion
|
|
|
277
311
|
spiffe_id: spiffe_id,
|
|
278
312
|
token: token,
|
|
279
313
|
audience: audience,
|
|
280
|
-
expiry: expiry
|
|
314
|
+
expiry: expiry,
|
|
315
|
+
source: :spire
|
|
281
316
|
)
|
|
317
|
+
rescue StandardError => e
|
|
318
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.workload_api_client.parse_jwt_svid_response',
|
|
319
|
+
audience: audience)
|
|
320
|
+
raise
|
|
282
321
|
end
|
|
283
322
|
|
|
284
323
|
# Build a self-signed X.509 SVID for use when SPIRE is not available.
|
|
285
324
|
# The SPIFFE ID is placed in the SAN URI extension per the SPIFFE spec.
|
|
286
325
|
# The Subject CN is a plain workload name (no URI) so OpenSSL parses cleanly.
|
|
287
326
|
def self_signed_fallback
|
|
327
|
+
log.info("[SPIFFE] Generating self-signed fallback SVID trust_domain=#{@trust_domain}")
|
|
288
328
|
key = OpenSSL::PKey::EC.generate('prime256v1')
|
|
289
329
|
cert = OpenSSL::X509::Certificate.new
|
|
290
330
|
cert.version = 2
|
|
@@ -308,8 +348,14 @@ module Legion
|
|
|
308
348
|
cert_pem: cert.to_pem,
|
|
309
349
|
key_pem: key.private_to_pem,
|
|
310
350
|
bundle_pem: nil,
|
|
311
|
-
expiry: cert.not_after
|
|
351
|
+
expiry: cert.not_after,
|
|
352
|
+
source: :fallback
|
|
312
353
|
)
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.workload_api_client.self_signed_fallback',
|
|
356
|
+
trust_domain: @trust_domain)
|
|
357
|
+
log.error("[SPIFFE] Self-signed fallback generation failed: #{e.message}")
|
|
358
|
+
raise
|
|
313
359
|
end
|
|
314
360
|
|
|
315
361
|
# --- Minimal protobuf decoder ---
|
|
@@ -373,18 +419,16 @@ module Legion
|
|
|
373
419
|
payload_json = Base64.urlsafe_decode64("#{parts[1]}==")
|
|
374
420
|
claims = begin
|
|
375
421
|
Legion::JSON.parse(payload_json)
|
|
376
|
-
rescue StandardError
|
|
422
|
+
rescue StandardError => e
|
|
423
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.workload_api_client.extract_jwt_expiry')
|
|
377
424
|
{}
|
|
378
425
|
end
|
|
379
426
|
exp = claims['exp'] || claims[:exp]
|
|
380
427
|
exp ? Time.at(exp.to_i) : Time.now + 3600
|
|
381
|
-
rescue StandardError
|
|
428
|
+
rescue StandardError => e
|
|
429
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.workload_api_client.extract_jwt_expiry')
|
|
382
430
|
Time.now + 3600
|
|
383
431
|
end
|
|
384
|
-
|
|
385
|
-
def log_warn(message)
|
|
386
|
-
Legion::Logging.warn(message) if defined?(Legion::Logging)
|
|
387
|
-
end
|
|
388
432
|
end
|
|
389
433
|
end
|
|
390
434
|
end
|
data/lib/legion/crypt/spiffe.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
3
4
|
require 'uri'
|
|
4
5
|
require 'openssl'
|
|
5
6
|
|
|
@@ -29,7 +30,7 @@ module Legion
|
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# Parsed X.509 SVID (SPIFFE Verifiable Identity Document).
|
|
32
|
-
X509Svid = Struct.new(:spiffe_id, :cert_pem, :key_pem, :bundle_pem, :expiry) do
|
|
33
|
+
X509Svid = Struct.new(:spiffe_id, :cert_pem, :key_pem, :bundle_pem, :expiry, :source) do
|
|
33
34
|
def expired?
|
|
34
35
|
Time.now >= expiry
|
|
35
36
|
end
|
|
@@ -45,7 +46,7 @@ module Legion
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# Parsed JWT SVID.
|
|
48
|
-
JwtSvid = Struct.new(:spiffe_id, :token, :audience, :expiry) do
|
|
49
|
+
JwtSvid = Struct.new(:spiffe_id, :token, :audience, :expiry, :source) do
|
|
49
50
|
def expired?
|
|
50
51
|
Time.now >= expiry
|
|
51
52
|
end
|
|
@@ -56,6 +57,8 @@ module Legion
|
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
class << self
|
|
60
|
+
include Legion::Logging::Helper
|
|
61
|
+
|
|
59
62
|
# Parse a SPIFFE ID string into a SpiffeId struct.
|
|
60
63
|
# Raises InvalidSpiffeIdError on malformed input.
|
|
61
64
|
def parse_id(spiffe_id_string)
|
|
@@ -69,13 +72,15 @@ module Legion
|
|
|
69
72
|
path: uri.path.empty? ? '/' : uri.path
|
|
70
73
|
)
|
|
71
74
|
rescue URI::InvalidURIError => e
|
|
75
|
+
handle_exception(e, level: :warn, operation: 'crypt.spiffe.parse_id', spiffe_id: spiffe_id_string)
|
|
72
76
|
raise InvalidSpiffeIdError, "Invalid SPIFFE ID '#{spiffe_id_string}': #{e.message}"
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
def valid_id?(spiffe_id_string)
|
|
76
80
|
parse_id(spiffe_id_string)
|
|
77
81
|
true
|
|
78
|
-
rescue InvalidSpiffeIdError
|
|
82
|
+
rescue InvalidSpiffeIdError => e
|
|
83
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.valid_id', spiffe_id: spiffe_id_string)
|
|
79
84
|
false
|
|
80
85
|
end
|
|
81
86
|
|
|
@@ -113,6 +118,14 @@ module Legion
|
|
|
113
118
|
spiffe[:workload_id] || spiffe['workload_id']
|
|
114
119
|
end
|
|
115
120
|
|
|
121
|
+
def allow_x509_fallback?
|
|
122
|
+
security = safe_security_settings
|
|
123
|
+
return false if security.nil?
|
|
124
|
+
|
|
125
|
+
spiffe = security[:spiffe] || security['spiffe'] || {}
|
|
126
|
+
spiffe[:allow_x509_fallback] || spiffe['allow_x509_fallback'] || false
|
|
127
|
+
end
|
|
128
|
+
|
|
116
129
|
private
|
|
117
130
|
|
|
118
131
|
def validate_uri!(uri, raw)
|
|
@@ -130,7 +143,8 @@ module Legion
|
|
|
130
143
|
return nil unless defined?(Legion::Settings)
|
|
131
144
|
|
|
132
145
|
Legion::Settings[:security]
|
|
133
|
-
rescue StandardError
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.safe_security_settings')
|
|
134
148
|
nil
|
|
135
149
|
end
|
|
136
150
|
end
|
data/lib/legion/crypt/tls.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Crypt
|
|
5
7
|
module TLS
|
|
@@ -9,6 +11,8 @@ module Legion
|
|
|
9
11
|
11_207 => 'memcached'
|
|
10
12
|
}.freeze
|
|
11
13
|
|
|
14
|
+
extend Legion::Logging::Helper
|
|
15
|
+
|
|
12
16
|
class << self
|
|
13
17
|
def resolve(tls_config, port: nil)
|
|
14
18
|
config = symbolize_keys(migrate_legacy(tls_config || {}))
|
|
@@ -19,7 +23,7 @@ module Legion
|
|
|
19
23
|
if enabled.nil? && port && TLS_PORTS.key?(port.to_i)
|
|
20
24
|
enabled = true
|
|
21
25
|
auto_detected = true
|
|
22
|
-
|
|
26
|
+
log.warn("TLS auto-enabled for port #{port}")
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
enabled = false if enabled.nil?
|
|
@@ -30,10 +34,12 @@ module Legion
|
|
|
30
34
|
key = resolve_uri(config[:key])
|
|
31
35
|
|
|
32
36
|
if verify == :mutual && (cert.nil? || key.nil?)
|
|
33
|
-
|
|
37
|
+
log.warn('TLS mutual requested but cert or key missing, downgrading to peer')
|
|
34
38
|
verify = :peer
|
|
35
39
|
end
|
|
36
40
|
|
|
41
|
+
log.info "TLS resolved enabled=#{enabled} verify=#{verify} auto_detected=#{auto_detected}"
|
|
42
|
+
|
|
37
43
|
{
|
|
38
44
|
enabled: enabled,
|
|
39
45
|
verify: verify,
|
|
@@ -42,6 +48,9 @@ module Legion
|
|
|
42
48
|
key: key,
|
|
43
49
|
auto_detected: auto_detected
|
|
44
50
|
}
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
handle_exception(e, level: :error, operation: 'crypt.tls.resolve', port: port)
|
|
53
|
+
raise
|
|
45
54
|
end
|
|
46
55
|
|
|
47
56
|
def migrate_legacy(config)
|
|
@@ -79,14 +88,9 @@ module Legion
|
|
|
79
88
|
else
|
|
80
89
|
value
|
|
81
90
|
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if defined?(Legion::Logging)
|
|
86
|
-
Legion::Logging.warn(msg)
|
|
87
|
-
else
|
|
88
|
-
warn msg
|
|
89
|
-
end
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
handle_exception(e, level: :warn, operation: 'crypt.tls.resolve_uri')
|
|
93
|
+
raise
|
|
90
94
|
end
|
|
91
95
|
end
|
|
92
96
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
3
4
|
require 'legion/crypt/kerberos_auth'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
7
8
|
class TokenRenewer
|
|
9
|
+
include Legion::Logging::Helper
|
|
10
|
+
|
|
8
11
|
INITIAL_BACKOFF = 30
|
|
9
12
|
MAX_BACKOFF = 600
|
|
10
13
|
MIN_SLEEP = 30
|
|
@@ -22,16 +25,19 @@ module Legion
|
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def start
|
|
28
|
+
return if running?
|
|
29
|
+
|
|
25
30
|
@stop = false
|
|
26
31
|
@thread = Thread.new { renewal_loop }
|
|
27
32
|
@thread.name = "vault-renewer-#{@cluster_name}"
|
|
28
|
-
|
|
33
|
+
log.info("TokenRenewer[#{@cluster_name}]: token renewal thread started")
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
def stop
|
|
32
37
|
@stop = true
|
|
33
38
|
@thread&.wakeup
|
|
34
|
-
rescue ThreadError
|
|
39
|
+
rescue ThreadError => e
|
|
40
|
+
handle_exception(e, level: :debug, operation: 'crypt.token_renewer.stop', cluster_name: @cluster_name)
|
|
35
41
|
nil
|
|
36
42
|
ensure
|
|
37
43
|
stop_thread_and_revoke
|
|
@@ -44,10 +50,12 @@ module Legion
|
|
|
44
50
|
def renew_token
|
|
45
51
|
result = @vault_client.auth_token.renew_self
|
|
46
52
|
@config[:lease_duration] = result.auth.lease_duration
|
|
47
|
-
|
|
53
|
+
@config[:renewable] = result.auth.renewable? if result.auth.respond_to?(:renewable?)
|
|
54
|
+
log.info("TokenRenewer[#{@cluster_name}]: token renewed, ttl=#{result.auth.lease_duration}s")
|
|
48
55
|
true
|
|
49
56
|
rescue StandardError => e
|
|
50
|
-
|
|
57
|
+
handle_exception(e, level: :warn, operation: 'crypt.token_renewer.renew_token', cluster_name: @cluster_name)
|
|
58
|
+
log.warn("TokenRenewer[#{@cluster_name}]: token renewal failed: #{e.message}")
|
|
51
59
|
false
|
|
52
60
|
end
|
|
53
61
|
|
|
@@ -64,15 +72,19 @@ module Legion
|
|
|
64
72
|
@config[:renewable] = result[:renewable]
|
|
65
73
|
@config[:connected] = true
|
|
66
74
|
@vault_client.token = result[:token]
|
|
67
|
-
|
|
75
|
+
log.info("TokenRenewer[#{@cluster_name}]: re-authenticated via Kerberos")
|
|
68
76
|
true
|
|
69
77
|
rescue StandardError => e
|
|
70
|
-
|
|
78
|
+
handle_exception(e, level: :warn, operation: 'crypt.token_renewer.reauth_kerberos', cluster_name: @cluster_name)
|
|
79
|
+
log.warn("TokenRenewer[#{@cluster_name}]: Kerberos re-auth failed: #{e.message}")
|
|
71
80
|
false
|
|
72
81
|
end
|
|
73
82
|
|
|
74
83
|
def sleep_duration
|
|
75
|
-
|
|
84
|
+
lease_duration = @config[:lease_duration].to_i
|
|
85
|
+
duration = [(lease_duration * RENEWAL_RATIO).to_i, 1].max
|
|
86
|
+
return [duration, lease_duration - 1].min if lease_duration.positive? && lease_duration < MIN_SLEEP
|
|
87
|
+
|
|
76
88
|
[duration, MIN_SLEEP].max
|
|
77
89
|
end
|
|
78
90
|
|
|
@@ -99,7 +111,8 @@ module Legion
|
|
|
99
111
|
end
|
|
100
112
|
end
|
|
101
113
|
rescue StandardError => e
|
|
102
|
-
|
|
114
|
+
handle_exception(e, level: :warn, operation: 'crypt.token_renewer.renewal_loop', cluster_name: @cluster_name)
|
|
115
|
+
log.warn("TokenRenewer[#{@cluster_name}]: renewal loop error: #{e.message}")
|
|
103
116
|
retry unless @stop
|
|
104
117
|
end
|
|
105
118
|
|
|
@@ -111,7 +124,7 @@ module Legion
|
|
|
111
124
|
def on_renewal_failure
|
|
112
125
|
@config[:connected] = false
|
|
113
126
|
delay = next_backoff
|
|
114
|
-
|
|
127
|
+
log.warn("TokenRenewer[#{@cluster_name}]: backoff retry in #{delay}s")
|
|
115
128
|
interruptible_sleep(delay)
|
|
116
129
|
end
|
|
117
130
|
|
|
@@ -128,15 +141,16 @@ module Legion
|
|
|
128
141
|
def stop_thread_and_revoke
|
|
129
142
|
return unless @thread
|
|
130
143
|
|
|
144
|
+
log.info("TokenRenewer[#{@cluster_name}]: stopping token renewal thread")
|
|
131
145
|
@thread.join(5)
|
|
132
146
|
thread_still_running = @thread.alive?
|
|
133
|
-
@thread = nil
|
|
134
147
|
|
|
135
148
|
if thread_still_running
|
|
136
|
-
|
|
149
|
+
log.warn("TokenRenewer[#{@cluster_name}]: token renewal thread did not stop within timeout; skipping token revocation")
|
|
137
150
|
else
|
|
151
|
+
@thread = nil
|
|
138
152
|
revoke_token
|
|
139
|
-
|
|
153
|
+
log.debug("TokenRenewer[#{@cluster_name}]: token renewal thread stopped")
|
|
140
154
|
end
|
|
141
155
|
end
|
|
142
156
|
|
|
@@ -145,21 +159,10 @@ module Legion
|
|
|
145
159
|
return unless @config[:auth_method]&.to_s == 'kerberos'
|
|
146
160
|
|
|
147
161
|
@vault_client.auth_token.revoke_self
|
|
148
|
-
|
|
162
|
+
log.info("TokenRenewer[#{@cluster_name}]: Vault token revoked")
|
|
149
163
|
rescue StandardError => e
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def log_debug(message)
|
|
154
|
-
Legion::Logging.debug("TokenRenewer[#{@cluster_name}]: #{message}") if defined?(Legion::Logging)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def log_info(message)
|
|
158
|
-
Legion::Logging.info("TokenRenewer[#{@cluster_name}]: #{message}") if defined?(Legion::Logging)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def log_warn(message)
|
|
162
|
-
Legion::Logging.warn("TokenRenewer[#{@cluster_name}]: #{message}") if defined?(Legion::Logging)
|
|
164
|
+
handle_exception(e, level: :warn, operation: 'crypt.token_renewer.revoke_token', cluster_name: @cluster_name)
|
|
165
|
+
log.warn("TokenRenewer[#{@cluster_name}]: Vault token revoke failed: #{e.message}")
|
|
163
166
|
end
|
|
164
167
|
end
|
|
165
168
|
end
|