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,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
3
4
|
require 'singleton'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
7
8
|
class LeaseManager
|
|
8
9
|
include Singleton
|
|
10
|
+
include Legion::Logging::Helper
|
|
9
11
|
|
|
10
12
|
RENEWAL_CHECK_INTERVAL = 5
|
|
11
13
|
|
|
@@ -15,18 +17,20 @@ module Legion
|
|
|
15
17
|
@refs = {}
|
|
16
18
|
@running = false
|
|
17
19
|
@renewal_thread = nil
|
|
20
|
+
@state_mutex = Mutex.new
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def start(definitions, vault_client: nil)
|
|
21
|
-
@vault_client = vault_client
|
|
24
|
+
@state_mutex.synchronize { @vault_client = vault_client }
|
|
22
25
|
return if definitions.nil? || definitions.empty?
|
|
23
26
|
|
|
27
|
+
log.info "LeaseManager start requested definitions=#{definitions.size}"
|
|
24
28
|
definitions.each do |name, opts|
|
|
25
29
|
path = opts['path'] || opts[:path]
|
|
26
30
|
next unless path
|
|
27
31
|
|
|
28
32
|
if lease_valid?(name)
|
|
29
|
-
|
|
33
|
+
log.debug("LeaseManager: reusing valid cached lease for '#{name}'")
|
|
30
34
|
next
|
|
31
35
|
end
|
|
32
36
|
|
|
@@ -35,52 +39,59 @@ module Legion
|
|
|
35
39
|
begin
|
|
36
40
|
response = logical.read(path)
|
|
37
41
|
unless response
|
|
38
|
-
|
|
42
|
+
log.warn("LeaseManager: no data at '#{name}' (#{path}) — path may not exist or role not configured")
|
|
39
43
|
next
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
@
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
@state_mutex.synchronize do
|
|
47
|
+
@lease_cache[name] = response.data || {}
|
|
48
|
+
@active_leases[name] = {
|
|
49
|
+
lease_id: response.lease_id,
|
|
50
|
+
lease_duration: response.lease_duration,
|
|
51
|
+
renewable: response.renewable?,
|
|
52
|
+
expires_at: Time.now + (response.lease_duration || 0),
|
|
53
|
+
fetched_at: Time.now
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
log.info("LeaseManager: fetched lease for '#{name}' from #{path}")
|
|
51
57
|
rescue StandardError => e
|
|
52
|
-
|
|
58
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.start', lease_name: name, path: path)
|
|
59
|
+
log.warn("LeaseManager: failed to fetch lease '#{name}' from #{path}: #{e.message}")
|
|
53
60
|
end
|
|
54
61
|
end
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
def fetched_count
|
|
58
|
-
@active_leases.size
|
|
65
|
+
@state_mutex.synchronize { @active_leases.size }
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
def fetch(name, key)
|
|
62
|
-
data = @
|
|
69
|
+
data = @state_mutex.synchronize do
|
|
70
|
+
@lease_cache[name.to_sym] || @lease_cache[name.to_s]
|
|
71
|
+
end
|
|
63
72
|
return nil unless data
|
|
64
73
|
|
|
65
74
|
data[key.to_sym] || data[key.to_s]
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
def lease_data(name)
|
|
69
|
-
@lease_cache[name]
|
|
78
|
+
@state_mutex.synchronize { @lease_cache[name] }
|
|
70
79
|
end
|
|
71
80
|
|
|
72
81
|
attr_reader :active_leases
|
|
73
82
|
|
|
74
83
|
def register_ref(name, key, path)
|
|
75
|
-
@
|
|
76
|
-
|
|
84
|
+
@state_mutex.synchronize do
|
|
85
|
+
@refs[name] ||= {}
|
|
86
|
+
@refs[name][key] = path
|
|
87
|
+
end
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
def push_to_settings(name)
|
|
80
|
-
refs = @
|
|
91
|
+
refs, data = @state_mutex.synchronize do
|
|
92
|
+
[@refs[name]&.dup, @lease_cache[name]&.dup]
|
|
93
|
+
end
|
|
81
94
|
return if refs.nil? || refs.empty?
|
|
82
|
-
|
|
83
|
-
data = @lease_cache[name]
|
|
84
95
|
return unless data
|
|
85
96
|
|
|
86
97
|
refs.each do |key, path|
|
|
@@ -88,80 +99,110 @@ module Legion
|
|
|
88
99
|
write_setting(path, value)
|
|
89
100
|
end
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
log.info("Lease '#{name}' rotated — updated #{refs.size} settings reference(s)")
|
|
92
103
|
end
|
|
93
104
|
|
|
94
105
|
def start_renewal_thread
|
|
95
|
-
|
|
106
|
+
@state_mutex.synchronize do
|
|
107
|
+
return if @renewal_thread&.alive?
|
|
96
108
|
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
@running = true
|
|
110
|
+
@renewal_thread = Thread.new { renewal_loop }
|
|
111
|
+
end
|
|
112
|
+
log.info 'LeaseManager renewal thread started'
|
|
99
113
|
end
|
|
100
114
|
|
|
101
115
|
def renewal_thread_alive?
|
|
102
|
-
@renewal_thread&.alive? || false
|
|
116
|
+
@state_mutex.synchronize { @renewal_thread&.alive? || false }
|
|
103
117
|
end
|
|
104
118
|
|
|
105
119
|
def shutdown
|
|
120
|
+
log.info 'LeaseManager shutdown requested'
|
|
106
121
|
stop_renewal_thread
|
|
107
122
|
|
|
108
|
-
@
|
|
123
|
+
leases = @state_mutex.synchronize { @active_leases.dup }
|
|
124
|
+
leases.each do |name, meta|
|
|
109
125
|
lease_id = meta[:lease_id]
|
|
110
126
|
next if lease_id.nil? || lease_id.empty?
|
|
111
127
|
|
|
112
128
|
begin
|
|
113
129
|
sys.revoke(lease_id)
|
|
114
|
-
|
|
130
|
+
log.debug("LeaseManager: revoked lease '#{name}' (#{lease_id})")
|
|
115
131
|
rescue StandardError => e
|
|
116
|
-
|
|
132
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.shutdown', lease_name: name)
|
|
133
|
+
log.warn("LeaseManager: failed to revoke lease '#{name}' (#{lease_id}): #{e.message}")
|
|
117
134
|
end
|
|
118
135
|
end
|
|
119
136
|
|
|
120
|
-
@
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
@state_mutex.synchronize do
|
|
138
|
+
@lease_cache.clear
|
|
139
|
+
@active_leases.clear
|
|
140
|
+
@refs.clear
|
|
141
|
+
@vault_client = nil
|
|
142
|
+
end
|
|
143
|
+
log.info 'LeaseManager shutdown complete'
|
|
124
144
|
end
|
|
125
145
|
|
|
126
146
|
def reset!
|
|
127
|
-
@
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
@state_mutex.synchronize do
|
|
148
|
+
@running = false
|
|
149
|
+
@lease_cache.clear
|
|
150
|
+
@active_leases.clear
|
|
151
|
+
@refs.clear
|
|
152
|
+
@vault_client = nil
|
|
153
|
+
@renewal_thread = nil
|
|
154
|
+
end
|
|
132
155
|
end
|
|
133
156
|
|
|
134
157
|
private
|
|
135
158
|
|
|
136
159
|
def logical
|
|
137
|
-
@
|
|
160
|
+
client = @state_mutex.synchronize { @vault_client }
|
|
161
|
+
client ? client.logical : ::Vault.logical
|
|
138
162
|
end
|
|
139
163
|
|
|
140
164
|
def sys
|
|
141
|
-
@
|
|
165
|
+
client = @state_mutex.synchronize { @vault_client }
|
|
166
|
+
client ? client.sys : ::Vault.sys
|
|
142
167
|
end
|
|
143
168
|
|
|
144
169
|
def stop_renewal_thread
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@renewal_thread
|
|
148
|
-
|
|
170
|
+
thread = @state_mutex.synchronize do
|
|
171
|
+
@running = false
|
|
172
|
+
@renewal_thread
|
|
173
|
+
end
|
|
174
|
+
return unless thread
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
thread.wakeup if thread.alive?
|
|
178
|
+
rescue ThreadError => e
|
|
179
|
+
handle_exception(e, level: :debug, operation: 'crypt.lease_manager.stop_renewal_thread')
|
|
180
|
+
end
|
|
181
|
+
thread.join(2)
|
|
182
|
+
if thread.alive?
|
|
183
|
+
log.warn 'LeaseManager renewal thread did not stop within timeout'
|
|
184
|
+
else
|
|
185
|
+
@state_mutex.synchronize { @renewal_thread = nil }
|
|
186
|
+
log.debug 'LeaseManager renewal thread stopped'
|
|
149
187
|
end
|
|
150
|
-
@renewal_thread = nil
|
|
151
188
|
end
|
|
152
189
|
|
|
153
190
|
def renewal_loop
|
|
154
|
-
while
|
|
155
|
-
|
|
156
|
-
renew_approaching_leases if
|
|
191
|
+
while running?
|
|
192
|
+
interruptible_sleep(RENEWAL_CHECK_INTERVAL)
|
|
193
|
+
renew_approaching_leases if running?
|
|
157
194
|
end
|
|
158
195
|
rescue StandardError => e
|
|
159
|
-
|
|
160
|
-
|
|
196
|
+
handle_exception(e, level: :error, operation: 'crypt.lease_manager.renewal_loop')
|
|
197
|
+
log.error("LeaseManager: renewal loop error: #{e.message}")
|
|
198
|
+
retry if running?
|
|
161
199
|
end
|
|
162
200
|
|
|
163
201
|
def renew_approaching_leases
|
|
164
|
-
@
|
|
202
|
+
leases = @state_mutex.synchronize { @active_leases.keys }
|
|
203
|
+
leases.each do |name|
|
|
204
|
+
lease = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
205
|
+
next unless lease
|
|
165
206
|
next unless lease[:renewable]
|
|
166
207
|
next unless approaching_expiry?(lease)
|
|
167
208
|
|
|
@@ -171,18 +212,28 @@ module Legion
|
|
|
171
212
|
|
|
172
213
|
def renew_lease(name, lease)
|
|
173
214
|
response = sys.renew(lease[:lease_id])
|
|
174
|
-
|
|
215
|
+
@state_mutex.synchronize do
|
|
216
|
+
current_lease = @active_leases[name]
|
|
217
|
+
next unless current_lease
|
|
218
|
+
|
|
219
|
+
current_lease[:lease_duration] = response.lease_duration if response.respond_to?(:lease_duration)
|
|
220
|
+
current_lease[:renewable] = response.renewable? if response.respond_to?(:renewable?)
|
|
221
|
+
current_lease[:expires_at] = Time.now + (response.lease_duration || 0)
|
|
222
|
+
end
|
|
223
|
+
log.info("LeaseManager: renewed lease '#{name}'")
|
|
175
224
|
|
|
176
|
-
|
|
177
|
-
|
|
225
|
+
cached_data = @state_mutex.synchronize { @lease_cache[name] }
|
|
226
|
+
if response.data && response.data != cached_data
|
|
227
|
+
@state_mutex.synchronize { @lease_cache[name] = response.data }
|
|
178
228
|
push_to_settings(name)
|
|
179
229
|
end
|
|
180
230
|
rescue StandardError => e
|
|
181
|
-
|
|
231
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.renew_lease', lease_name: name)
|
|
232
|
+
log.warn("LeaseManager: failed to renew lease '#{name}': #{e.message}")
|
|
182
233
|
end
|
|
183
234
|
|
|
184
235
|
def lease_valid?(name)
|
|
185
|
-
meta = @active_leases[name]
|
|
236
|
+
meta = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
186
237
|
return false unless meta
|
|
187
238
|
|
|
188
239
|
expires_at = meta[:expires_at]
|
|
@@ -192,7 +243,7 @@ module Legion
|
|
|
192
243
|
end
|
|
193
244
|
|
|
194
245
|
def revoke_expired_lease(name)
|
|
195
|
-
meta = @active_leases[name]
|
|
246
|
+
meta = @state_mutex.synchronize { @active_leases[name]&.dup }
|
|
196
247
|
return unless meta
|
|
197
248
|
|
|
198
249
|
lease_id = meta[:lease_id]
|
|
@@ -200,12 +251,15 @@ module Legion
|
|
|
200
251
|
|
|
201
252
|
begin
|
|
202
253
|
sys.revoke(lease_id)
|
|
203
|
-
|
|
254
|
+
log.debug("LeaseManager: revoked expired lease '#{name}' (#{lease_id}) before re-fetch")
|
|
204
255
|
rescue StandardError => e
|
|
205
|
-
|
|
256
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.revoke_expired_lease', lease_name: name)
|
|
257
|
+
log.warn("LeaseManager: failed to revoke expired lease '#{name}' (#{lease_id}): #{e.message}")
|
|
206
258
|
ensure
|
|
207
|
-
@
|
|
208
|
-
|
|
259
|
+
@state_mutex.synchronize do
|
|
260
|
+
@active_leases.delete(name)
|
|
261
|
+
@lease_cache.delete(name)
|
|
262
|
+
end
|
|
209
263
|
end
|
|
210
264
|
end
|
|
211
265
|
|
|
@@ -229,22 +283,21 @@ module Legion
|
|
|
229
283
|
end
|
|
230
284
|
target[path.last] = value if target.is_a?(Hash)
|
|
231
285
|
rescue StandardError => e
|
|
232
|
-
|
|
286
|
+
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.write_setting', path: path.join('.'))
|
|
287
|
+
log.warn("LeaseManager: failed to write setting at #{path.join('.')}: #{e.message}")
|
|
233
288
|
end
|
|
234
289
|
|
|
235
|
-
def
|
|
236
|
-
|
|
237
|
-
Legion::Logging.debug(message)
|
|
238
|
-
else
|
|
239
|
-
$stdout.puts("[DEBUG] #{message}")
|
|
240
|
-
end
|
|
290
|
+
def running?
|
|
291
|
+
@state_mutex.synchronize { @running }
|
|
241
292
|
end
|
|
242
293
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
294
|
+
def interruptible_sleep(seconds)
|
|
295
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + seconds
|
|
296
|
+
loop do
|
|
297
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
298
|
+
break if remaining <= 0 || !running?
|
|
299
|
+
|
|
300
|
+
sleep([remaining, 1.0].min)
|
|
248
301
|
end
|
|
249
302
|
end
|
|
250
303
|
end
|
data/lib/legion/crypt/mtls.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
3
4
|
require 'socket'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
@@ -7,6 +8,7 @@ module Legion
|
|
|
7
8
|
module Mtls
|
|
8
9
|
DEFAULT_PKI_PATH = 'pki/issue/legion-internal'
|
|
9
10
|
DEFAULT_TTL = '24h'
|
|
11
|
+
extend Legion::Logging::Helper
|
|
10
12
|
|
|
11
13
|
class << self
|
|
12
14
|
def enabled?
|
|
@@ -29,6 +31,7 @@ module Legion
|
|
|
29
31
|
|
|
30
32
|
def issue_cert(common_name:, ttl: nil)
|
|
31
33
|
resolved_ttl = ttl || cert_ttl_setting || DEFAULT_TTL
|
|
34
|
+
log.info "[mTLS] certificate issue requested common_name=#{common_name} ttl=#{resolved_ttl}"
|
|
32
35
|
|
|
33
36
|
response = ::Vault.logical.write(
|
|
34
37
|
pki_path,
|
|
@@ -49,10 +52,16 @@ module Legion
|
|
|
49
52
|
serial: data[:serial_number],
|
|
50
53
|
expiry: Time.at(data[:expiration].to_i)
|
|
51
54
|
}
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
handle_exception(e, level: :error, operation: 'crypt.mtls.issue_cert', common_name: common_name, ttl: resolved_ttl)
|
|
57
|
+
raise
|
|
52
58
|
end
|
|
53
59
|
|
|
54
60
|
def local_ip
|
|
55
61
|
Socket.ip_address_list.find { |a| a.ipv4? && !a.ipv4_loopback? }&.ip_address || '127.0.0.1'
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
handle_exception(e, level: :warn, operation: 'crypt.mtls.local_ip')
|
|
64
|
+
'127.0.0.1'
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
private
|
|
@@ -61,7 +70,8 @@ module Legion
|
|
|
61
70
|
return nil unless defined?(Legion::Settings)
|
|
62
71
|
|
|
63
72
|
Legion::Settings[:security]
|
|
64
|
-
rescue StandardError
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
handle_exception(e, level: :debug, operation: 'crypt.mtls.safe_security_settings')
|
|
65
75
|
nil
|
|
66
76
|
end
|
|
67
77
|
|
|
@@ -71,6 +81,9 @@ module Legion
|
|
|
71
81
|
|
|
72
82
|
mtls = security[:mtls] || security['mtls'] || {}
|
|
73
83
|
mtls[:cert_ttl] || mtls['cert_ttl']
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
handle_exception(e, level: :debug, operation: 'crypt.mtls.cert_ttl_setting')
|
|
86
|
+
nil
|
|
74
87
|
end
|
|
75
88
|
end
|
|
76
89
|
end
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'openssl'
|
|
4
|
+
require 'legion/logging/helper'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
7
8
|
module PartitionKeys
|
|
9
|
+
extend Legion::Logging::Helper
|
|
10
|
+
|
|
8
11
|
class << self
|
|
9
12
|
def derive_key(master_key:, tenant_id:, context: nil)
|
|
10
13
|
context ||= begin
|
|
11
14
|
Legion::Settings[:crypt][:partition_keys][:derivation_context]
|
|
12
|
-
rescue StandardError
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
handle_exception(e, level: :debug, operation: 'crypt.partition_keys.derivation_context', tenant_id: tenant_id)
|
|
13
17
|
nil
|
|
14
18
|
end || 'legion-partition'
|
|
15
|
-
|
|
19
|
+
log.debug "PartitionKeys deriving key for tenant #{tenant_id}"
|
|
16
20
|
salt = OpenSSL::Digest::SHA256.digest(tenant_id.to_s)
|
|
17
21
|
OpenSSL::KDF.hkdf(master_key, salt: salt, info: context, length: 32, hash: 'SHA256')
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
handle_exception(e, level: :error, operation: 'crypt.partition_keys.derive_key', tenant_id: tenant_id)
|
|
24
|
+
raise
|
|
18
25
|
end
|
|
19
26
|
|
|
20
27
|
def encrypt_for_tenant(plaintext:, tenant_id:, master_key:)
|
|
@@ -26,9 +33,10 @@ module Legion
|
|
|
26
33
|
ciphertext = cipher.update(plaintext) + cipher.final
|
|
27
34
|
auth_tag = cipher.auth_tag
|
|
28
35
|
|
|
36
|
+
log.debug "PartitionKeys encrypted payload for tenant #{tenant_id}"
|
|
29
37
|
{ ciphertext: ciphertext, iv: iv, auth_tag: auth_tag }
|
|
30
38
|
rescue StandardError => e
|
|
31
|
-
|
|
39
|
+
handle_exception(e, level: :error, operation: 'crypt.partition_keys.encrypt_for_tenant', tenant_id: tenant_id)
|
|
32
40
|
raise
|
|
33
41
|
end
|
|
34
42
|
|
|
@@ -39,9 +47,11 @@ module Legion
|
|
|
39
47
|
decipher.key = key
|
|
40
48
|
decipher.iv = init_vector
|
|
41
49
|
decipher.auth_tag = auth_tag
|
|
42
|
-
decipher.update(ciphertext) + decipher.final
|
|
50
|
+
plaintext = decipher.update(ciphertext) + decipher.final
|
|
51
|
+
log.debug "PartitionKeys decrypted payload for tenant #{tenant_id}"
|
|
52
|
+
plaintext
|
|
43
53
|
rescue StandardError => e
|
|
44
|
-
|
|
54
|
+
handle_exception(e, level: :error, operation: 'crypt.partition_keys.decrypt_for_tenant', tenant_id: tenant_id)
|
|
45
55
|
raise
|
|
46
56
|
end
|
|
47
57
|
end
|
|
@@ -1,8 +1,12 @@
|
|
|
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 Settings
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
def self.tls
|
|
7
11
|
{
|
|
8
12
|
enabled: false,
|
|
@@ -15,11 +19,12 @@ module Legion
|
|
|
15
19
|
|
|
16
20
|
def self.spiffe
|
|
17
21
|
{
|
|
18
|
-
enabled:
|
|
19
|
-
socket_path:
|
|
20
|
-
trust_domain:
|
|
21
|
-
workload_id:
|
|
22
|
-
renewal_window:
|
|
22
|
+
enabled: false,
|
|
23
|
+
socket_path: '/tmp/spire-agent/public/api.sock',
|
|
24
|
+
trust_domain: 'legion.internal',
|
|
25
|
+
workload_id: nil,
|
|
26
|
+
renewal_window: 0.5,
|
|
27
|
+
allow_x509_fallback: false
|
|
23
28
|
}
|
|
24
29
|
end
|
|
25
30
|
|
|
@@ -57,8 +62,8 @@ module Legion
|
|
|
57
62
|
connected: false,
|
|
58
63
|
renewer_time: 5,
|
|
59
64
|
renewer: true,
|
|
60
|
-
push_cluster_secret:
|
|
61
|
-
read_cluster_secret:
|
|
65
|
+
push_cluster_secret: false,
|
|
66
|
+
read_cluster_secret: false,
|
|
62
67
|
kv_path: ENV['LEGION_VAULT_KV_PATH'] || 'legion',
|
|
63
68
|
leases: {},
|
|
64
69
|
default: nil,
|
|
@@ -75,12 +80,13 @@ module Legion
|
|
|
75
80
|
end
|
|
76
81
|
|
|
77
82
|
begin
|
|
78
|
-
|
|
83
|
+
if Legion.const_defined?('Settings')
|
|
84
|
+
Legion::Settings.merge_settings('crypt', Legion::Crypt::Settings.default)
|
|
85
|
+
Legion::Crypt::Settings.log.info('Legion::Crypt settings defaults merged')
|
|
86
|
+
end
|
|
79
87
|
rescue StandardError => e
|
|
80
|
-
if Legion
|
|
81
|
-
Legion::
|
|
82
|
-
elsif Legion.const_defined?('Logging') && Legion::Logging.respond_to?(:fatal)
|
|
83
|
-
Legion::Logging.fatal("crypt settings merge error: #{e.class}: #{e.message}\n#{Array(e.backtrace).join("\n")}")
|
|
88
|
+
if Legion::Crypt::Settings.respond_to?(:handle_exception)
|
|
89
|
+
Legion::Crypt::Settings.handle_exception(e, level: :fatal, operation: 'crypt.settings.merge_defaults')
|
|
84
90
|
else
|
|
85
91
|
puts e.message
|
|
86
92
|
puts e.backtrace
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
3
4
|
require 'openssl'
|
|
4
5
|
require 'base64'
|
|
5
6
|
|
|
@@ -12,6 +13,8 @@ module Legion
|
|
|
12
13
|
# returned by WorkloadApiClient. No external gem is required —
|
|
13
14
|
# all operations use the Ruby stdlib OpenSSL bindings.
|
|
14
15
|
module IdentityHelpers
|
|
16
|
+
include Legion::Logging::Helper
|
|
17
|
+
|
|
15
18
|
# Sign arbitrary data with the private key from an X.509 SVID.
|
|
16
19
|
# Returns the signature as a Base64-encoded string.
|
|
17
20
|
#
|
|
@@ -26,7 +29,11 @@ module Legion
|
|
|
26
29
|
key = OpenSSL::PKey.read(svid.key_pem)
|
|
27
30
|
digest = OpenSSL::Digest.new('SHA256')
|
|
28
31
|
signature = key.sign(digest, data.b)
|
|
32
|
+
log.debug("SPIFFE signed payload with SVID id=#{svid.spiffe_id}")
|
|
29
33
|
Base64.strict_encode64(signature)
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
handle_exception(e, level: :error, operation: 'crypt.spiffe.sign_with_svid', spiffe_id: svid&.spiffe_id&.to_s)
|
|
36
|
+
raise
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
# Verify a Base64-encoded signature produced by sign_with_svid.
|
|
@@ -43,9 +50,12 @@ module Legion
|
|
|
43
50
|
cert = OpenSSL::X509::Certificate.new(svid.cert_pem)
|
|
44
51
|
digest = OpenSSL::Digest.new('SHA256')
|
|
45
52
|
signature = Base64.strict_decode64(signature_b64)
|
|
46
|
-
cert.public_key.verify(digest, signature, data.b)
|
|
53
|
+
result = cert.public_key.verify(digest, signature, data.b)
|
|
54
|
+
log.debug("SPIFFE signature verification completed id=#{svid.spiffe_id} valid=#{result}")
|
|
55
|
+
result
|
|
47
56
|
rescue OpenSSL::PKey::PKeyError, OpenSSL::X509::CertificateError, ArgumentError => e
|
|
48
|
-
|
|
57
|
+
handle_exception(e, level: :warn, operation: 'crypt.spiffe.verify_svid_signature', spiffe_id: svid&.spiffe_id&.to_s)
|
|
58
|
+
log.warn("[SPIFFE] SVID signature verification error: #{e.message}")
|
|
49
59
|
false
|
|
50
60
|
end
|
|
51
61
|
|
|
@@ -65,12 +75,14 @@ module Legion
|
|
|
65
75
|
|
|
66
76
|
uri = entry.sub('URI:', '')
|
|
67
77
|
return Legion::Crypt::Spiffe.parse_id(uri)
|
|
68
|
-
rescue InvalidSpiffeIdError
|
|
78
|
+
rescue InvalidSpiffeIdError => e
|
|
79
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.extract_spiffe_id_from_cert', san_entry: entry)
|
|
69
80
|
next
|
|
70
81
|
end
|
|
71
82
|
|
|
72
83
|
nil
|
|
73
|
-
rescue OpenSSL::X509::CertificateError
|
|
84
|
+
rescue OpenSSL::X509::CertificateError => e
|
|
85
|
+
handle_exception(e, level: :debug, operation: 'crypt.spiffe.extract_spiffe_id_from_cert')
|
|
74
86
|
nil
|
|
75
87
|
end
|
|
76
88
|
|
|
@@ -90,7 +102,8 @@ module Legion
|
|
|
90
102
|
|
|
91
103
|
leaf = OpenSSL::X509::Certificate.new(cert_pem)
|
|
92
104
|
store.verify(leaf)
|
|
93
|
-
rescue OpenSSL::X509::CertificateError, OpenSSL::X509::StoreError
|
|
105
|
+
rescue OpenSSL::X509::CertificateError, OpenSSL::X509::StoreError => e
|
|
106
|
+
handle_exception(e, level: :warn, operation: 'crypt.spiffe.trusted_cert?', spiffe_id: svid&.spiffe_id&.to_s)
|
|
94
107
|
false
|
|
95
108
|
end
|
|
96
109
|
|
|
@@ -118,12 +131,6 @@ module Legion
|
|
|
118
131
|
base
|
|
119
132
|
end
|
|
120
133
|
end
|
|
121
|
-
|
|
122
|
-
private
|
|
123
|
-
|
|
124
|
-
def log_spiffe_warn(message)
|
|
125
|
-
Legion::Logging.warn("[SPIFFE] #{message}") if defined?(Legion::Logging)
|
|
126
|
-
end
|
|
127
134
|
end
|
|
128
135
|
end
|
|
129
136
|
end
|