legion-crypt 1.4.10 → 1.4.11
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/.gitignore +3 -0
- data/CHANGELOG.md +6 -0
- data/lib/legion/crypt/cert_rotation.rb +143 -0
- data/lib/legion/crypt/mtls.rb +78 -0
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9955ad145758d89e705e6fdc0d7f90824710b98c2752979a425d6dbdfb65b693
|
|
4
|
+
data.tar.gz: c38bfc3054ec7facfb1b0c84c19983314e7c1af4cbd7170a74b2400b4a641fb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68364ee4acade59550e9e6f3054315f013948c7bc194b4693cc381eef7ee27edc44840ebf55ce27fbe271986f004581739a8cbaf436ffb1d5c8e7fbb91cc9948
|
|
7
|
+
data.tar.gz: bf403bc4b5ad7c28404b91968d08b4984e8e2fb0177388674eb49f6ef83261abcd6c2568ec58f6c02b250686a155a8924aa28a77f837b3eb1360093986ba35ad
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Legion::Crypt
|
|
2
2
|
|
|
3
|
+
## [1.4.11] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Crypt::Mtls` module: Vault PKI cert issuance with `.issue_cert`, `.enabled?`, `.pki_path`, `.local_ip`; feature-flagged via `security.mtls.enabled`
|
|
7
|
+
- `Legion::Crypt::CertRotation` class: background cert rotation at 50% TTL boundary with `#start`, `#stop`, `#rotate!`, `#needs_renewal?`; emits `cert.rotated` event via `Legion::Events`
|
|
8
|
+
|
|
3
9
|
## [1.4.10] - 2026-03-24
|
|
4
10
|
|
|
5
11
|
### Added
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Crypt
|
|
5
|
+
class CertRotation
|
|
6
|
+
DEFAULT_CHECK_INTERVAL = 43_200 # 12 hours
|
|
7
|
+
|
|
8
|
+
attr_reader :check_interval, :current_cert, :issued_at
|
|
9
|
+
|
|
10
|
+
def initialize(check_interval: DEFAULT_CHECK_INTERVAL)
|
|
11
|
+
@check_interval = check_interval
|
|
12
|
+
@current_cert = nil
|
|
13
|
+
@issued_at = nil
|
|
14
|
+
@running = false
|
|
15
|
+
@thread = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def start
|
|
19
|
+
return unless Legion::Crypt::Mtls.enabled?
|
|
20
|
+
return if running?
|
|
21
|
+
|
|
22
|
+
@running = true
|
|
23
|
+
@thread = Thread.new { rotation_loop }
|
|
24
|
+
log_info('[mTLS] CertRotation started')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stop
|
|
28
|
+
@running = false
|
|
29
|
+
if @thread&.alive?
|
|
30
|
+
@thread.kill
|
|
31
|
+
@thread.join(2)
|
|
32
|
+
end
|
|
33
|
+
@thread = nil
|
|
34
|
+
log_debug('[mTLS] CertRotation stopped')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def running?
|
|
38
|
+
(@running && @thread&.alive?) || false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rotate!
|
|
42
|
+
node_name = node_common_name
|
|
43
|
+
new_cert = Legion::Crypt::Mtls.issue_cert(common_name: node_name)
|
|
44
|
+
@current_cert = new_cert
|
|
45
|
+
@issued_at = Time.now
|
|
46
|
+
log_info("[mTLS] Certificate rotated: serial=#{new_cert[:serial]} expiry=#{new_cert[:expiry]}")
|
|
47
|
+
emit_rotated_event(new_cert)
|
|
48
|
+
new_cert
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def needs_renewal?
|
|
52
|
+
return false if @current_cert.nil? || @issued_at.nil?
|
|
53
|
+
|
|
54
|
+
expiry = @current_cert[:expiry]
|
|
55
|
+
total = expiry - @issued_at
|
|
56
|
+
return true if total <= 0
|
|
57
|
+
|
|
58
|
+
remaining = expiry - Time.now
|
|
59
|
+
fraction = remaining / total
|
|
60
|
+
fraction < renewal_window
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def rotation_loop
|
|
66
|
+
rotate!
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
log_warn("[mTLS] Initial rotation failed: #{e.message}")
|
|
69
|
+
ensure
|
|
70
|
+
loop_check
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def loop_check
|
|
74
|
+
while @running
|
|
75
|
+
sleep(@check_interval)
|
|
76
|
+
next unless @running && needs_renewal?
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
rotate!
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
log_warn("[mTLS] Rotation check failed: #{e.message}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
log_warn("[mTLS] CertRotation loop error: #{e.message}")
|
|
86
|
+
retry if @running
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def renewal_window
|
|
90
|
+
return 0.5 unless defined?(Legion::Settings)
|
|
91
|
+
|
|
92
|
+
security = Legion::Settings[:security]
|
|
93
|
+
return 0.5 if security.nil?
|
|
94
|
+
|
|
95
|
+
mtls = security[:mtls] || security['mtls'] || {}
|
|
96
|
+
mtls[:renewal_window] || mtls['renewal_window'] || 0.5
|
|
97
|
+
rescue StandardError
|
|
98
|
+
0.5
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def node_common_name
|
|
102
|
+
return 'legion.internal' unless defined?(Legion::Settings)
|
|
103
|
+
|
|
104
|
+
name = Legion::Settings[:client]&.dig(:name) || Legion::Settings[:client]&.dig('name')
|
|
105
|
+
name || 'legion.internal'
|
|
106
|
+
rescue StandardError
|
|
107
|
+
'legion.internal'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def emit_rotated_event(cert)
|
|
111
|
+
return unless defined?(Legion::Events)
|
|
112
|
+
|
|
113
|
+
Legion::Events.emit('cert.rotated', serial: cert[:serial], expiry: cert[:expiry])
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
log_debug("[mTLS] Event emit failed: #{e.message}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def log_info(msg)
|
|
119
|
+
if defined?(Legion::Logging)
|
|
120
|
+
Legion::Logging.info(msg)
|
|
121
|
+
else
|
|
122
|
+
$stdout.puts(msg)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def log_debug(msg)
|
|
127
|
+
if defined?(Legion::Logging)
|
|
128
|
+
Legion::Logging.debug(msg)
|
|
129
|
+
else
|
|
130
|
+
$stdout.puts("[DEBUG] #{msg}")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def log_warn(msg)
|
|
135
|
+
if defined?(Legion::Logging)
|
|
136
|
+
Legion::Logging.warn(msg)
|
|
137
|
+
else
|
|
138
|
+
warn("[WARN] #{msg}")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Crypt
|
|
7
|
+
module Mtls
|
|
8
|
+
DEFAULT_PKI_PATH = 'pki/issue/legion-internal'
|
|
9
|
+
DEFAULT_TTL = '24h'
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def enabled?
|
|
13
|
+
security = safe_security_settings
|
|
14
|
+
return false if security.nil?
|
|
15
|
+
|
|
16
|
+
mtls = security[:mtls] || security['mtls']
|
|
17
|
+
return false if mtls.nil?
|
|
18
|
+
|
|
19
|
+
mtls[:enabled] || mtls['enabled'] || false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def pki_path
|
|
23
|
+
security = safe_security_settings
|
|
24
|
+
return DEFAULT_PKI_PATH if security.nil?
|
|
25
|
+
|
|
26
|
+
mtls = security[:mtls] || security['mtls'] || {}
|
|
27
|
+
mtls[:vault_pki_path] || mtls['vault_pki_path'] || DEFAULT_PKI_PATH
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def issue_cert(common_name:, ttl: nil)
|
|
31
|
+
resolved_ttl = ttl || cert_ttl_setting || DEFAULT_TTL
|
|
32
|
+
|
|
33
|
+
response = ::Vault.logical.write(
|
|
34
|
+
pki_path,
|
|
35
|
+
common_name: common_name,
|
|
36
|
+
ttl: resolved_ttl,
|
|
37
|
+
ip_sans: local_ip,
|
|
38
|
+
alt_names: ''
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
raise "Vault PKI returned nil for #{pki_path} (common_name=#{common_name})" if response.nil?
|
|
42
|
+
|
|
43
|
+
data = response.data
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
cert: data[:certificate],
|
|
47
|
+
key: data[:private_key],
|
|
48
|
+
ca_chain: Array(data[:ca_chain]),
|
|
49
|
+
serial: data[:serial_number],
|
|
50
|
+
expiry: Time.at(data[:expiration].to_i)
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def local_ip
|
|
55
|
+
Socket.ip_address_list.find { |a| a.ipv4? && !a.ipv4_loopback? }&.ip_address || '127.0.0.1'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def safe_security_settings
|
|
61
|
+
return nil unless defined?(Legion::Settings)
|
|
62
|
+
|
|
63
|
+
Legion::Settings[:security]
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cert_ttl_setting
|
|
69
|
+
security = safe_security_settings
|
|
70
|
+
return nil if security.nil?
|
|
71
|
+
|
|
72
|
+
mtls = security[:mtls] || security['mtls'] || {}
|
|
73
|
+
mtls[:cert_ttl] || mtls['cert_ttl']
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/legion/crypt/version.rb
CHANGED
data/lib/legion/crypt.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-crypt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -73,6 +73,7 @@ files:
|
|
|
73
73
|
- legion-crypt.gemspec
|
|
74
74
|
- lib/legion/crypt.rb
|
|
75
75
|
- lib/legion/crypt/attestation.rb
|
|
76
|
+
- lib/legion/crypt/cert_rotation.rb
|
|
76
77
|
- lib/legion/crypt/cipher.rb
|
|
77
78
|
- lib/legion/crypt/cluster_secret.rb
|
|
78
79
|
- lib/legion/crypt/ed25519.rb
|
|
@@ -83,6 +84,7 @@ files:
|
|
|
83
84
|
- lib/legion/crypt/ldap_auth.rb
|
|
84
85
|
- lib/legion/crypt/lease_manager.rb
|
|
85
86
|
- lib/legion/crypt/mock_vault.rb
|
|
87
|
+
- lib/legion/crypt/mtls.rb
|
|
86
88
|
- lib/legion/crypt/partition_keys.rb
|
|
87
89
|
- lib/legion/crypt/settings.rb
|
|
88
90
|
- lib/legion/crypt/tls.rb
|