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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14e4a34517eb19612bc6e2f1f07093f3e5d2affef6e04f48b5c8a1fb1bd06a74
4
- data.tar.gz: 793e8657ba9d56a34a96362fb0777c7f22e1c63f8953ab00b562fa3e57e9e5a3
3
+ metadata.gz: 9955ad145758d89e705e6fdc0d7f90824710b98c2752979a425d6dbdfb65b693
4
+ data.tar.gz: c38bfc3054ec7facfb1b0c84c19983314e7c1af4cbd7170a74b2400b4a641fb8
5
5
  SHA512:
6
- metadata.gz: e56636a54d3a9e6c615184361ca3242d7c17919c0235878cf43e8283858b2e261dc82ff02fa4777d57b2b619cd16f3c254e7aa5e453cf56cdcfca6156166c98d
7
- data.tar.gz: e5f5b735f176da76b49438d1bd5b0a40a42b1087c288d5487edbcc3b0601466ca9f4d16fba257bda8cd2f84782102ab675d117c976567efab4dcaa493706343e
6
+ metadata.gz: 68364ee4acade59550e9e6f3054315f013948c7bc194b4693cc381eef7ee27edc44840ebf55ce27fbe271986f004581739a8cbaf436ffb1d5c8e7fbb91cc9948
7
+ data.tar.gz: bf403bc4b5ad7c28404b91968d08b4984e8e2fb0177388674eb49f6ef83261abcd6c2568ec58f6c02b250686a155a8924aa28a77f837b3eb1360093986ba35ad
data/.gitignore CHANGED
@@ -13,3 +13,6 @@
13
13
  # rspec failure tracking
14
14
  .rspec_status
15
15
  legionio.key
16
+
17
+ # git worktrees
18
+ .worktrees/
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.10'
5
+ VERSION = '1.4.11'
6
6
  end
7
7
  end
data/lib/legion/crypt.rb CHANGED
@@ -11,6 +11,8 @@ require 'legion/crypt/lease_manager'
11
11
  require 'legion/crypt/vault_cluster'
12
12
  require 'legion/crypt/ldap_auth'
13
13
  require 'legion/crypt/helper'
14
+ require 'legion/crypt/mtls'
15
+ require 'legion/crypt/cert_rotation'
14
16
 
15
17
  module Legion
16
18
  module Crypt
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.10
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