legion-crypt 1.5.38 → 1.5.42
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/.github/workflows/ci.yml +30 -3
- data/.rspec +2 -0
- data/AGENTS.md +2 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +64 -192
- data/CODEOWNERS +4 -4
- data/Gemfile +4 -1
- data/README.md +60 -16
- data/legion-crypt.gemspec +1 -0
- data/lib/legion/crypt/cert_rotation.rb +7 -21
- data/lib/legion/crypt/cluster_secret.rb +3 -14
- data/lib/legion/crypt/ed25519.rb +2 -10
- data/lib/legion/crypt/erasure.rb +3 -13
- data/lib/legion/crypt/jwks_client.rb +4 -4
- data/lib/legion/crypt/kerberos_auth.rb +2 -2
- data/lib/legion/crypt/ldap_auth.rb +2 -2
- data/lib/legion/crypt/lease_manager.rb +7 -7
- data/lib/legion/crypt/mtls.rb +8 -46
- data/lib/legion/crypt/partition_keys.rb +2 -6
- data/lib/legion/crypt/settings.rb +24 -21
- data/lib/legion/crypt/spiffe/svid_rotation.rb +6 -9
- data/lib/legion/crypt/spiffe.rb +14 -39
- data/lib/legion/crypt/token_renewer.rb +6 -6
- data/lib/legion/crypt/vault/client.rb +355 -0
- data/lib/legion/crypt/vault/cluster.rb +330 -0
- data/lib/legion/crypt/vault/entity.rb +80 -0
- data/lib/legion/crypt/vault/jwt_auth.rb +99 -0
- data/lib/legion/crypt/vault/kerberos_auth.rb +53 -0
- data/lib/legion/crypt/vault/operation.rb +29 -0
- data/lib/legion/crypt/vault.rb +6 -11
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +69 -80
- metadata +23 -8
- data/lib/legion/crypt/vault_client.rb +0 -355
- data/lib/legion/crypt/vault_cluster.rb +0 -331
- data/lib/legion/crypt/vault_entity.rb +0 -78
- data/lib/legion/crypt/vault_jwt_auth.rb +0 -97
- data/lib/legion/crypt/vault_kerberos_auth.rb +0 -51
- data/lib/legion/crypt/vault_operation.rb +0 -27
- /data/lib/legion/crypt/{vault_renewer.rb → vault/renewer.rb} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
-
require 'legion/crypt/
|
|
4
|
+
require 'legion/crypt/vault/operation'
|
|
5
5
|
|
|
6
6
|
module Legion
|
|
7
7
|
module Crypt
|
|
@@ -53,8 +53,8 @@ module Legion
|
|
|
53
53
|
@wake.broadcast
|
|
54
54
|
@thread
|
|
55
55
|
end
|
|
56
|
-
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::
|
|
57
|
-
Legion::Crypt::
|
|
56
|
+
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::Vault::Operation::SHUTDOWN_TIMEOUT
|
|
57
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { thread.join } if thread && thread != Thread.current
|
|
58
58
|
@mutex.synchronize { @thread = nil if @thread.equal?(thread) && !thread&.alive? }
|
|
59
59
|
log.info('[mTLS] CertRotation stopped')
|
|
60
60
|
rescue StandardError => e
|
|
@@ -67,7 +67,7 @@ module Legion
|
|
|
67
67
|
|
|
68
68
|
def rotate!(generation: nil)
|
|
69
69
|
generation ||= @mutex.synchronize { @generation }
|
|
70
|
-
new_cert = Legion::Crypt::
|
|
70
|
+
new_cert = Legion::Crypt::Vault::Operation.with_timeout { Legion::Crypt::Mtls.issue_cert(common_name: node_common_name) }
|
|
71
71
|
issued_at = Time.now
|
|
72
72
|
raise 'Vault PKI returned an expired certificate' unless new_cert[:expiry] > issued_at
|
|
73
73
|
|
|
@@ -133,31 +133,17 @@ module Legion
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def renewal_window
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
security = Legion::Settings[:security]
|
|
139
|
-
return 0.5 if security.nil?
|
|
140
|
-
|
|
141
|
-
mtls = security[:mtls] || security['mtls'] || {}
|
|
142
|
-
window = mtls[:renewal_window] || mtls['renewal_window'] || 0.5
|
|
136
|
+
window = Legion::Settings[:crypt][:mtls][:renewal_window]
|
|
143
137
|
unless window.is_a?(Numeric) && window.finite? && window >= 0 && window < 1
|
|
144
138
|
raise ArgumentError, 'Certificate renewal window must be finite and between zero and one (exclusive)'
|
|
145
139
|
end
|
|
146
140
|
|
|
147
141
|
window
|
|
148
|
-
rescue StandardError => e
|
|
149
|
-
handle_exception(e, level: :debug, operation: 'crypt.cert_rotation.renewal_window')
|
|
150
|
-
0.5
|
|
151
142
|
end
|
|
152
143
|
|
|
153
144
|
def node_common_name
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
name = Legion::Settings[:client]&.dig(:name) || Legion::Settings[:client]&.dig('name')
|
|
157
|
-
name || 'legion.internal'
|
|
158
|
-
rescue StandardError => e
|
|
159
|
-
handle_exception(e, level: :debug, operation: 'crypt.cert_rotation.node_common_name')
|
|
160
|
-
'legion.internal'
|
|
145
|
+
name = Legion::Settings.dig(:client, :name)
|
|
146
|
+
name.nil? ? 'legion.internal' : name
|
|
161
147
|
end
|
|
162
148
|
|
|
163
149
|
def emit_rotated_event(cert)
|
|
@@ -97,17 +97,6 @@ module Legion
|
|
|
97
97
|
nil
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
def force_cluster_secret
|
|
101
|
-
Legion::Settings[:crypt].fetch(:force_cluster_secret, false)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def settings_push_vault
|
|
105
|
-
vault_settings = Legion::Settings[:crypt][:vault]
|
|
106
|
-
return vault_settings[:push_cluster_secret] unless vault_settings[:push_cluster_secret].nil?
|
|
107
|
-
|
|
108
|
-
vault_settings.fetch(:push_cs_to_vault, false)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
100
|
def only_member?
|
|
112
101
|
return true if defined?(Legion::Transport::TYPE) && Legion::Transport::TYPE == 'local'
|
|
113
102
|
|
|
@@ -130,7 +119,7 @@ module Legion
|
|
|
130
119
|
Legion::Settings[:crypt][:cluster_secret] = value.dup.freeze
|
|
131
120
|
@cluster_secret_cache = nil
|
|
132
121
|
Legion::Settings[:crypt][:cs_encrypt_ready] = true
|
|
133
|
-
push_cs_to_vault if push_to_vault &&
|
|
122
|
+
push_cs_to_vault if push_to_vault && Legion::Settings[:crypt][:vault][:push_cluster_secret]
|
|
134
123
|
log.info "Cluster secret loaded into settings push_to_vault=#{push_to_vault}"
|
|
135
124
|
end
|
|
136
125
|
|
|
@@ -145,7 +134,7 @@ module Legion
|
|
|
145
134
|
end
|
|
146
135
|
|
|
147
136
|
def cluster_secret_timeout
|
|
148
|
-
timeout = Legion::Settings[:crypt]
|
|
137
|
+
timeout = Legion::Settings[:crypt][:cluster_secret_timeout]
|
|
149
138
|
unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
150
139
|
raise InitializationError, 'cluster_secret_timeout must be a finite positive number'
|
|
151
140
|
end
|
|
@@ -154,7 +143,7 @@ module Legion
|
|
|
154
143
|
end
|
|
155
144
|
|
|
156
145
|
def secret_length
|
|
157
|
-
length = Legion::Settings[:crypt][:cluster_length]
|
|
146
|
+
length = Legion::Settings[:crypt][:cluster_length]
|
|
158
147
|
raise InitializationError, 'cluster_length must be a positive integer' unless length.is_a?(Integer) && length.positive?
|
|
159
148
|
|
|
160
149
|
length
|
data/lib/legion/crypt/ed25519.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'ed25519'
|
|
4
4
|
require 'legion/logging/helper'
|
|
5
|
+
require 'legion/crypt/settings'
|
|
5
6
|
|
|
6
7
|
module Legion
|
|
7
8
|
module Crypt
|
|
@@ -83,17 +84,8 @@ module Legion
|
|
|
83
84
|
|
|
84
85
|
private
|
|
85
86
|
|
|
86
|
-
def key_prefix
|
|
87
|
-
begin
|
|
88
|
-
Legion::Settings[:crypt][:ed25519][:vault_key_prefix]
|
|
89
|
-
rescue StandardError => e
|
|
90
|
-
handle_exception(e, level: :debug, operation: 'crypt.ed25519.key_prefix')
|
|
91
|
-
nil
|
|
92
|
-
end || 'keys'
|
|
93
|
-
end
|
|
94
|
-
|
|
95
87
|
def vault_key_path(agent_id)
|
|
96
|
-
normalize_kv_path("#{
|
|
88
|
+
normalize_kv_path("#{Legion::Settings[:crypt][:ed25519][:vault_key_prefix]}/#{agent_id}")
|
|
97
89
|
end
|
|
98
90
|
|
|
99
91
|
def normalize_kv_path(path)
|
data/lib/legion/crypt/erasure.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/settings'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
@@ -9,7 +10,7 @@ module Legion
|
|
|
9
10
|
|
|
10
11
|
class << self
|
|
11
12
|
def erase_tenant(tenant_id:)
|
|
12
|
-
key_path = "#{
|
|
13
|
+
key_path = "#{Legion::Settings[:crypt][:partition_keys][:vault_tenant_prefix]}/#{tenant_id}/master_key"
|
|
13
14
|
|
|
14
15
|
log.info "[crypt] Erasing tenant #{tenant_id}"
|
|
15
16
|
result = Legion::Crypt.delete(key_path)
|
|
@@ -25,7 +26,7 @@ module Legion
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def verify_erasure(tenant_id:)
|
|
28
|
-
key_path = "#{
|
|
29
|
+
key_path = "#{Legion::Settings[:crypt][:partition_keys][:vault_tenant_prefix]}/#{tenant_id}/master_key"
|
|
29
30
|
|
|
30
31
|
data = Legion::Crypt.read(key_path, nil)
|
|
31
32
|
erased = data.nil?
|
|
@@ -35,17 +36,6 @@ module Legion
|
|
|
35
36
|
handle_exception(e, level: :warn, operation: 'crypt.erasure.verify_erasure', tenant_id: tenant_id)
|
|
36
37
|
{ erased: false, tenant_id: tenant_id, error: e.message }
|
|
37
38
|
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
def tenant_prefix
|
|
42
|
-
begin
|
|
43
|
-
Legion::Settings[:crypt][:partition_keys][:vault_tenant_prefix]
|
|
44
|
-
rescue StandardError => e
|
|
45
|
-
handle_exception(e, level: :debug, operation: 'crypt.erasure.tenant_prefix')
|
|
46
|
-
nil
|
|
47
|
-
end || 'secret/data/legion/tenants'
|
|
48
|
-
end
|
|
49
39
|
end
|
|
50
40
|
end
|
|
51
41
|
end
|
|
@@ -6,6 +6,7 @@ require 'json'
|
|
|
6
6
|
require 'openssl'
|
|
7
7
|
require 'jwt'
|
|
8
8
|
require 'legion/logging/helper'
|
|
9
|
+
require 'legion/crypt/settings'
|
|
9
10
|
require 'concurrent'
|
|
10
11
|
|
|
11
12
|
module Legion
|
|
@@ -172,12 +173,11 @@ module Legion
|
|
|
172
173
|
end
|
|
173
174
|
|
|
174
175
|
def jwks_ssl_verify_mode
|
|
175
|
-
return OpenSSL::SSL::VERIFY_PEER unless defined?(Legion::Settings)
|
|
176
|
-
|
|
177
176
|
verify = Legion::Settings[:crypt][:jwt][:jwks_tls_verify]&.to_s
|
|
178
177
|
verify == 'none' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
|
179
|
-
rescue StandardError
|
|
180
|
-
|
|
178
|
+
rescue StandardError => e
|
|
179
|
+
handle_exception(e, level: :warn, operation: 'crypt.jwks.ssl_verify_mode')
|
|
180
|
+
raise
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
def with_url_lock(jwks_url, &)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
-
require 'legion/crypt/
|
|
4
|
+
require 'legion/crypt/vault/client'
|
|
5
5
|
|
|
6
6
|
module Legion
|
|
7
7
|
module Crypt
|
|
@@ -94,7 +94,7 @@ module Legion
|
|
|
94
94
|
body: '{}',
|
|
95
95
|
headers: { 'Authorization' => "Negotiate #{spnego_token}" }
|
|
96
96
|
)
|
|
97
|
-
response = Legion::Crypt::
|
|
97
|
+
response = Legion::Crypt::Vault::Client::Secret.decode(data: json)
|
|
98
98
|
raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
|
|
99
99
|
|
|
100
100
|
auth = response.auth
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
-
require 'legion/crypt/
|
|
4
|
+
require 'legion/crypt/vault/operation'
|
|
5
5
|
|
|
6
6
|
module Legion
|
|
7
7
|
module Crypt
|
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
config = clusters.fetch(cluster_name)
|
|
15
15
|
log.info "LDAP login requested user=#{username} cluster=#{cluster_name}"
|
|
16
16
|
client = vault_client(cluster_name)
|
|
17
|
-
secret = Legion::Crypt::
|
|
17
|
+
secret = Legion::Crypt::Vault::Operation.with_timeout do
|
|
18
18
|
client.logical.write(path: "auth/ldap/login/#{username}", data: { password: password })
|
|
19
19
|
end
|
|
20
20
|
auth = secret.auth
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
4
|
require 'singleton'
|
|
5
5
|
require 'timeout'
|
|
6
|
-
require 'legion/crypt/
|
|
6
|
+
require 'legion/crypt/vault/operation'
|
|
7
7
|
|
|
8
8
|
module Legion
|
|
9
9
|
module Crypt
|
|
@@ -189,7 +189,7 @@ module Legion
|
|
|
189
189
|
|
|
190
190
|
def start_renewal_thread
|
|
191
191
|
previous = @state_mutex.synchronize { @renewal_thread if @renewal_generation != @generation }
|
|
192
|
-
Legion::Crypt::
|
|
192
|
+
Legion::Crypt::Vault::Operation.with_timeout { previous.join } if previous&.alive?
|
|
193
193
|
@state_mutex.synchronize do
|
|
194
194
|
return if @renewal_thread&.alive? || @stopped
|
|
195
195
|
return if Legion::Crypt.respond_to?(:stopping) && Legion::Crypt.stopping
|
|
@@ -218,7 +218,7 @@ module Legion
|
|
|
218
218
|
end
|
|
219
219
|
|
|
220
220
|
def shutdown(deadline: nil)
|
|
221
|
-
deadline ||= monotonic_now + Legion::Crypt::
|
|
221
|
+
deadline ||= monotonic_now + Legion::Crypt::Vault::Operation::SHUTDOWN_TIMEOUT
|
|
222
222
|
request_stop
|
|
223
223
|
@state_mutex.synchronize do
|
|
224
224
|
@active_leases.clear
|
|
@@ -376,7 +376,7 @@ module Legion
|
|
|
376
376
|
|
|
377
377
|
def with_lease_operation(name:, deadline: nil, **)
|
|
378
378
|
lock = @state_mutex.synchronize { @lease_locks[name] ||= Mutex.new }
|
|
379
|
-
Legion::Crypt::
|
|
379
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) do |operation_deadline|
|
|
380
380
|
lock.synchronize { yield operation_deadline }
|
|
381
381
|
end
|
|
382
382
|
end
|
|
@@ -510,7 +510,7 @@ module Legion
|
|
|
510
510
|
safe_to_retire = !handoff[:attempted_reconnect] || !lifecycle_current?(generation: generation, active: active)
|
|
511
511
|
if handoff[:attempted_reconnect] && restored
|
|
512
512
|
begin
|
|
513
|
-
safe_to_retire = Legion::Crypt::
|
|
513
|
+
safe_to_retire = Legion::Crypt::Vault::Operation.with_timeout(deadline: context[:deadline]) do
|
|
514
514
|
trigger_reconnect(name, generation: generation, active: active)
|
|
515
515
|
end
|
|
516
516
|
rescue StandardError => e
|
|
@@ -602,7 +602,7 @@ module Legion
|
|
|
602
602
|
end
|
|
603
603
|
return unless entry
|
|
604
604
|
|
|
605
|
-
Legion::Crypt::
|
|
605
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { entry[:client].sys.revoke(lease_id: entry[:lease_id]) }
|
|
606
606
|
@state_mutex.synchronize { @owned_leases.delete(key) if @owned_leases[key].equal?(entry) }
|
|
607
607
|
rescue StandardError => e
|
|
608
608
|
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.revoke_owned_lease', lease_name: entry&.dig(:name))
|
|
@@ -620,7 +620,7 @@ module Legion
|
|
|
620
620
|
thread = request_stop
|
|
621
621
|
return unless thread
|
|
622
622
|
|
|
623
|
-
Legion::Crypt::
|
|
623
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { thread.join }
|
|
624
624
|
@state_mutex.synchronize { @renewal_thread = nil if @renewal_thread.equal?(thread) }
|
|
625
625
|
rescue StandardError => e
|
|
626
626
|
handle_exception(e, level: :warn, operation: 'crypt.lease_manager.stop_renewal_thread')
|
data/lib/legion/crypt/mtls.rb
CHANGED
|
@@ -1,47 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/settings'
|
|
4
5
|
require 'socket'
|
|
5
6
|
|
|
6
7
|
module Legion
|
|
7
8
|
module Crypt
|
|
8
9
|
module Mtls
|
|
9
|
-
DEFAULT_PKI_PATH = 'pki/issue/legion-internal'
|
|
10
|
-
DEFAULT_TTL = '24h'
|
|
11
10
|
extend Legion::Logging::Helper
|
|
12
11
|
|
|
13
12
|
class << self
|
|
14
13
|
def enabled?
|
|
15
|
-
|
|
16
|
-
return false if security.nil?
|
|
17
|
-
|
|
18
|
-
mtls = security[:mtls] || security['mtls']
|
|
19
|
-
return false if mtls.nil?
|
|
20
|
-
|
|
21
|
-
mtls[:enabled] || mtls['enabled'] || false
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def pki_path
|
|
25
|
-
security = safe_security_settings
|
|
26
|
-
return DEFAULT_PKI_PATH if security.nil?
|
|
27
|
-
|
|
28
|
-
mtls = security[:mtls] || security['mtls'] || {}
|
|
29
|
-
mtls[:vault_pki_path] || mtls['vault_pki_path'] || DEFAULT_PKI_PATH
|
|
14
|
+
Legion::Settings[:crypt][:mtls][:enabled] == true
|
|
30
15
|
end
|
|
31
16
|
|
|
32
|
-
def issue_cert(common_name:, ttl: nil)
|
|
33
|
-
|
|
17
|
+
def issue_cert(common_name:, alt_names: '', ttl: nil, **)
|
|
18
|
+
pki_path = Legion::Settings[:crypt][:mtls][:vault_pki_path]
|
|
19
|
+
resolved_ttl = ttl.nil? ? Legion::Settings[:crypt][:mtls][:cert_ttl] : ttl
|
|
34
20
|
log.info "[mTLS] certificate issue requested common_name=#{common_name} ttl=#{resolved_ttl}"
|
|
35
21
|
|
|
36
22
|
response = Legion::Crypt.vault_client.logical.write(
|
|
37
23
|
path: pki_path,
|
|
38
|
-
data: { common_name: common_name, ttl: resolved_ttl, ip_sans: local_ip, alt_names:
|
|
24
|
+
data: { common_name: common_name, ttl: resolved_ttl, ip_sans: local_ip, alt_names: alt_names }
|
|
39
25
|
)
|
|
40
|
-
|
|
41
26
|
raise "Vault PKI returned nil for #{pki_path} (common_name=#{common_name})" if response.nil?
|
|
42
27
|
|
|
43
28
|
data = response.data
|
|
44
|
-
|
|
45
29
|
{
|
|
46
30
|
cert: data[:certificate],
|
|
47
31
|
key: data[:private_key],
|
|
@@ -54,34 +38,12 @@ module Legion
|
|
|
54
38
|
raise
|
|
55
39
|
end
|
|
56
40
|
|
|
57
|
-
def local_ip
|
|
41
|
+
def local_ip(**)
|
|
58
42
|
Socket.ip_address_list.find { |a| a.ipv4? && !a.ipv4_loopback? }&.ip_address || '127.0.0.1'
|
|
59
|
-
rescue
|
|
43
|
+
rescue SocketError, SystemCallError, IOError => e
|
|
60
44
|
handle_exception(e, level: :warn, operation: 'crypt.mtls.local_ip')
|
|
61
45
|
'127.0.0.1'
|
|
62
46
|
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def safe_security_settings
|
|
67
|
-
return nil unless defined?(Legion::Settings)
|
|
68
|
-
|
|
69
|
-
Legion::Settings[:security]
|
|
70
|
-
rescue StandardError => e
|
|
71
|
-
handle_exception(e, level: :debug, operation: 'crypt.mtls.safe_security_settings')
|
|
72
|
-
nil
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def cert_ttl_setting
|
|
76
|
-
security = safe_security_settings
|
|
77
|
-
return nil if security.nil?
|
|
78
|
-
|
|
79
|
-
mtls = security[:mtls] || security['mtls'] || {}
|
|
80
|
-
mtls[:cert_ttl] || mtls['cert_ttl']
|
|
81
|
-
rescue StandardError => e
|
|
82
|
-
handle_exception(e, level: :debug, operation: 'crypt.mtls.cert_ttl_setting')
|
|
83
|
-
nil
|
|
84
|
-
end
|
|
85
47
|
end
|
|
86
48
|
end
|
|
87
49
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'openssl'
|
|
4
4
|
require 'legion/logging/helper'
|
|
5
|
+
require 'legion/crypt/settings'
|
|
5
6
|
|
|
6
7
|
module Legion
|
|
7
8
|
module Crypt
|
|
@@ -10,12 +11,7 @@ module Legion
|
|
|
10
11
|
|
|
11
12
|
class << self
|
|
12
13
|
def derive_key(master_key:, tenant_id:, context: nil)
|
|
13
|
-
context
|
|
14
|
-
Legion::Settings[:crypt][:partition_keys][:derivation_context]
|
|
15
|
-
rescue StandardError => e
|
|
16
|
-
handle_exception(e, level: :debug, operation: 'crypt.partition_keys.derivation_context', tenant_id: tenant_id)
|
|
17
|
-
nil
|
|
18
|
-
end || 'legion-partition'
|
|
14
|
+
context = Legion::Settings[:crypt][:partition_keys][:derivation_context] if context.nil?
|
|
19
15
|
log.debug "PartitionKeys deriving key for tenant #{tenant_id}"
|
|
20
16
|
salt = OpenSSL::Digest::SHA256.digest(tenant_id.to_s)
|
|
21
17
|
OpenSSL::KDF.hkdf(master_key, salt: salt, info: context, length: 32, hash: 'SHA256')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/settings'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Crypt
|
|
@@ -17,6 +18,15 @@ module Legion
|
|
|
17
18
|
}
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
def self.mtls
|
|
22
|
+
{
|
|
23
|
+
enabled: false,
|
|
24
|
+
vault_pki_path: 'pki/issue/legion-internal',
|
|
25
|
+
cert_ttl: '24h',
|
|
26
|
+
renewal_window: 0.5
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
def self.spiffe
|
|
21
31
|
{
|
|
22
32
|
enabled: false,
|
|
@@ -30,14 +40,19 @@ module Legion
|
|
|
30
40
|
|
|
31
41
|
def self.default
|
|
32
42
|
{
|
|
33
|
-
vault:
|
|
34
|
-
jwt:
|
|
35
|
-
tls:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
vault: vault,
|
|
44
|
+
jwt: jwt,
|
|
45
|
+
tls: tls,
|
|
46
|
+
mtls: mtls,
|
|
47
|
+
cs_encrypt_ready: false,
|
|
48
|
+
dynamic_keys: true,
|
|
49
|
+
cluster_secret: nil,
|
|
50
|
+
cluster_length: 32,
|
|
51
|
+
cluster_secret_timeout: 5,
|
|
52
|
+
save_private_key: true,
|
|
53
|
+
read_private_key: true,
|
|
54
|
+
ed25519: { vault_key_prefix: 'keys' },
|
|
55
|
+
partition_keys: { derivation_context: 'legion-partition', vault_tenant_prefix: 'secret/data/legion/tenants' }
|
|
41
56
|
}
|
|
42
57
|
end
|
|
43
58
|
|
|
@@ -87,16 +102,4 @@ module Legion
|
|
|
87
102
|
end
|
|
88
103
|
end
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
if Legion.const_defined?('Settings')
|
|
92
|
-
Legion::Settings.merge_settings('crypt', Legion::Crypt::Settings.default)
|
|
93
|
-
Legion::Crypt::Settings.log.info('Legion::Crypt settings defaults merged')
|
|
94
|
-
end
|
|
95
|
-
rescue StandardError => e
|
|
96
|
-
if Legion::Crypt::Settings.respond_to?(:handle_exception)
|
|
97
|
-
Legion::Crypt::Settings.handle_exception(e, level: :fatal, operation: 'crypt.settings.merge_defaults')
|
|
98
|
-
else
|
|
99
|
-
puts e.message
|
|
100
|
-
puts e.backtrace
|
|
101
|
-
end
|
|
102
|
-
end
|
|
105
|
+
Legion::Settings.register_library(:crypt, Legion::Crypt::Settings.default)
|
|
@@ -130,16 +130,13 @@ module Legion
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def renewal_window
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
window = Legion::Settings.dig(:security, :spiffe, :renewal_window)
|
|
134
|
+
window = SVID_RENEWAL_RATIO if window.nil?
|
|
135
|
+
unless window.is_a?(Numeric) && window.finite? && window >= 0 && window < 1
|
|
136
|
+
raise ArgumentError, 'SVID renewal window must be finite and between zero and one (exclusive)'
|
|
137
|
+
end
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
spiffe[:renewal_window] || spiffe['renewal_window'] || SVID_RENEWAL_RATIO
|
|
140
|
-
rescue StandardError => e
|
|
141
|
-
handle_exception(e, level: :debug, operation: 'crypt.spiffe.svid_rotation.renewal_window')
|
|
142
|
-
SVID_RENEWAL_RATIO
|
|
139
|
+
window
|
|
143
140
|
end
|
|
144
141
|
end
|
|
145
142
|
end
|
data/lib/legion/crypt/spiffe.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'legion/logging/helper'
|
|
4
3
|
require 'uri'
|
|
5
4
|
require 'openssl'
|
|
5
|
+
require 'legion/logging/helper'
|
|
6
|
+
require 'legion/crypt/settings'
|
|
7
|
+
require 'legion/crypt/spiffe/workload_api_client'
|
|
8
|
+
require 'legion/crypt/spiffe/svid_rotation'
|
|
9
|
+
require 'legion/crypt/spiffe/identity_helpers'
|
|
6
10
|
|
|
7
11
|
module Legion
|
|
8
12
|
module Crypt
|
|
@@ -10,7 +14,7 @@ module Legion
|
|
|
10
14
|
SPIFFE_SCHEME = 'spiffe'
|
|
11
15
|
DEFAULT_SOCKET_PATH = '/tmp/spire-agent/public/api.sock'
|
|
12
16
|
DEFAULT_TRUST_DOMAIN = 'legion.internal'
|
|
13
|
-
SVID_RENEWAL_RATIO
|
|
17
|
+
SVID_RENEWAL_RATIO = 0.5
|
|
14
18
|
|
|
15
19
|
class Error < StandardError; end
|
|
16
20
|
class InvalidSpiffeIdError < Error; end
|
|
@@ -80,50 +84,30 @@ module Legion
|
|
|
80
84
|
parse_id(spiffe_id_string)
|
|
81
85
|
true
|
|
82
86
|
rescue InvalidSpiffeIdError => e
|
|
83
|
-
handle_exception(e, level: :
|
|
87
|
+
handle_exception(e, level: :warn, operation: 'crypt.spiffe.valid_id', spiffe_id: spiffe_id_string)
|
|
84
88
|
false
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def enabled?
|
|
88
|
-
security
|
|
89
|
-
return false if security.nil?
|
|
90
|
-
|
|
91
|
-
spiffe = security[:spiffe] || security['spiffe']
|
|
92
|
-
return false if spiffe.nil?
|
|
93
|
-
|
|
94
|
-
spiffe[:enabled] || spiffe['enabled'] || false
|
|
92
|
+
Legion::Settings.dig(:security, :spiffe, :enabled) == true
|
|
95
93
|
end
|
|
96
94
|
|
|
97
95
|
def socket_path
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
spiffe = security[:spiffe] || security['spiffe'] || {}
|
|
102
|
-
spiffe[:socket_path] || spiffe['socket_path'] || DEFAULT_SOCKET_PATH
|
|
96
|
+
path = Legion::Settings.dig(:security, :spiffe, :socket_path)
|
|
97
|
+
path.nil? ? DEFAULT_SOCKET_PATH : path
|
|
103
98
|
end
|
|
104
99
|
|
|
105
100
|
def trust_domain
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
spiffe = security[:spiffe] || security['spiffe'] || {}
|
|
110
|
-
spiffe[:trust_domain] || spiffe['trust_domain'] || DEFAULT_TRUST_DOMAIN
|
|
101
|
+
domain = Legion::Settings.dig(:security, :spiffe, :trust_domain)
|
|
102
|
+
domain.nil? ? DEFAULT_TRUST_DOMAIN : domain
|
|
111
103
|
end
|
|
112
104
|
|
|
113
105
|
def workload_id
|
|
114
|
-
security
|
|
115
|
-
return nil if security.nil?
|
|
116
|
-
|
|
117
|
-
spiffe = security[:spiffe] || security['spiffe'] || {}
|
|
118
|
-
spiffe[:workload_id] || spiffe['workload_id']
|
|
106
|
+
Legion::Settings.dig(:security, :spiffe, :workload_id)
|
|
119
107
|
end
|
|
120
108
|
|
|
121
109
|
def allow_x509_fallback?
|
|
122
|
-
security
|
|
123
|
-
return false if security.nil?
|
|
124
|
-
|
|
125
|
-
spiffe = security[:spiffe] || security['spiffe'] || {}
|
|
126
|
-
spiffe[:allow_x509_fallback] || spiffe['allow_x509_fallback'] || false
|
|
110
|
+
Legion::Settings.dig(:security, :spiffe, :allow_x509_fallback) == true
|
|
127
111
|
end
|
|
128
112
|
|
|
129
113
|
private
|
|
@@ -138,15 +122,6 @@ module Legion
|
|
|
138
122
|
|
|
139
123
|
raise InvalidSpiffeIdError, "SPIFFE ID must not contain userinfo, port, query, or fragment in '#{raw}'"
|
|
140
124
|
end
|
|
141
|
-
|
|
142
|
-
def safe_security_settings
|
|
143
|
-
return nil unless defined?(Legion::Settings)
|
|
144
|
-
|
|
145
|
-
Legion::Settings[:security]
|
|
146
|
-
rescue StandardError => e
|
|
147
|
-
handle_exception(e, level: :debug, operation: 'crypt.spiffe.safe_security_settings')
|
|
148
|
-
nil
|
|
149
|
-
end
|
|
150
125
|
end
|
|
151
126
|
end
|
|
152
127
|
end
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
4
|
require 'legion/crypt/kerberos_auth'
|
|
5
|
-
require 'legion/crypt/
|
|
6
|
-
require 'legion/crypt/
|
|
5
|
+
require 'legion/crypt/vault/operation'
|
|
6
|
+
require 'legion/crypt/vault/client'
|
|
7
7
|
|
|
8
8
|
module Legion
|
|
9
9
|
module Crypt
|
|
@@ -57,7 +57,7 @@ module Legion
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def stop(deadline: nil)
|
|
60
|
-
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) +
|
|
60
|
+
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::Vault::Operation::SHUTDOWN_TIMEOUT
|
|
61
61
|
@state_mutex.synchronize do
|
|
62
62
|
return if @stopping
|
|
63
63
|
|
|
@@ -86,7 +86,7 @@ module Legion
|
|
|
86
86
|
deadline = @state_mutex.synchronize do
|
|
87
87
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + (@expires_at - Time.now) if @expires_at
|
|
88
88
|
end
|
|
89
|
-
|
|
89
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) do
|
|
90
90
|
result = @vault_client.auth_token.renew_self
|
|
91
91
|
@state_mutex.synchronize do
|
|
92
92
|
return false unless active_generation?(generation: generation)
|
|
@@ -133,7 +133,7 @@ module Legion
|
|
|
133
133
|
true
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
|
-
|
|
136
|
+
Legion::Crypt::Vault::Operation.with_timeout do
|
|
137
137
|
result = Legion::Crypt::KerberosAuth.login(
|
|
138
138
|
vault_client: @vault_client,
|
|
139
139
|
service_principal: krb_config[:service_principal],
|
|
@@ -307,7 +307,7 @@ module Legion
|
|
|
307
307
|
end
|
|
308
308
|
return unless owned_token
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { @vault_client.auth_token.revoke_self }
|
|
311
311
|
log.info("TokenRenewer[#{@cluster_name}]: Vault token revoked")
|
|
312
312
|
rescue StandardError => e
|
|
313
313
|
handle_exception(e, level: :warn, operation: 'crypt.token_renewer.revoke_token', cluster_name: @cluster_name)
|