legion-crypt 1.4.3 → 1.4.5

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: 755c4278bc3e6f3ba845f5d46ba61b753a19b9a4fc0212f9da9827e6fefe3ac2
4
- data.tar.gz: '04585ab20ddb62568949ca90c33405b61bb477cd2750503944acadcf665925b9'
3
+ metadata.gz: 2e448a5dd1e2c70e43cff0b6512e1b2af1d41f9a2af7b9719495ba5b10c90ec7
4
+ data.tar.gz: 6c270665c5a185baa844e5b7e80af9f2ac8922a520748e387a9d10363294061e
5
5
  SHA512:
6
- metadata.gz: 53c5f43b6ab1ca2f0dad978a3cee6b1d3314b8aa57ffae077641d9882756f3a331b21258f3b45a9789639095facdff27baec911e6cbd066c7d2c2fc1058237bd
7
- data.tar.gz: f3b542c8ca98c768ddfae7763da33ddc3638fd46dbb174abf037cd0baca348fa6127c4ad1d3efa46c86a943b2cd3d2c6f2df5d8d179e0a64a5b94d5b3ec4f35c
6
+ metadata.gz: bc43cab1939dee1cd1d6c28a07a2766953af52fc30230375d47e23e289f53beaf3a2be47d98c549bd9098c545d6dd540faa8a87143bb678429265a96b55d96cf
7
+ data.tar.gz: 1813d049d59f5bb81baeef8f10fb2e9f68d7540c8fb4858a75f2f40d881eb7b101a42ce4f89161a38ef14122da0e53e1cf0d2009716d7411ba16c0a77902504c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.4.5] - 2026-03-20
4
+
5
+ ### Changed
6
+ - Refactored `Legion::Crypt::TLS` to standard `resolve` pattern: pure config normalizer with port auto-detect, vault URI resolution, legacy key migration, and three verification levels (none/peer/mutual)
7
+ - Removed consumer-specific `bunny_options` and `sequel_options` methods (moved to consuming gems)
8
+
9
+ ### Added
10
+ - `TLS.resolve(tls_config, port:)` — standard TLS config resolver
11
+ - `TLS.migrate_legacy(config)` — backwards-compat mapping for transport's old TLS keys
12
+ - `TLS::TLS_PORTS` — known TLS port auto-detection map (5671, 6380, 11207)
13
+ - Default `tls:` settings block in `Legion::Crypt::Settings`
14
+
15
+ ## [1.4.4] - 2026-03-18
16
+
17
+ ### Added
18
+ - Multi-cluster Vault support: named clusters with `default` pointer in `crypt.vault.clusters`
19
+ - `VaultCluster` module: per-cluster `::Vault::Client` management, `connect_all_clusters`
20
+ - `LdapAuth` module: LDAP authentication via Vault HTTP API (`auth/ldap/login/:username`)
21
+ - `ldap_login_all` authenticates to all LDAP-configured clusters with single credentials
22
+ - `VaultRenewer` now renews tokens for all connected clusters
23
+ - Backward compatible: single-cluster config (`crypt.vault.address`) still works unchanged
24
+
3
25
  ## [1.4.3] - 2026-03-17
4
26
 
5
27
  ### Added
data/CLAUDE.md CHANGED
@@ -8,6 +8,7 @@
8
8
  Handles encryption, decryption, secrets management, JWT token management, and HashiCorp Vault connectivity for the LegionIO framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, and Vault token lifecycle management.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-crypt
11
+ **Version**: 1.4.4
11
12
  **License**: Apache-2.0
12
13
 
13
14
  ## Architecture
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # legion-crypt
2
2
 
3
- Encryption, secrets management, JWT token management, and HashiCorp Vault integration for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, and Vault token lifecycle management.
3
+ Encryption, secrets management, JWT token management, and HashiCorp Vault integration for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, Vault token lifecycle management, and multi-cluster Vault connectivity.
4
+
5
+ **Version**: 1.4.4
4
6
 
5
7
  ## Installation
6
8
 
@@ -146,6 +148,48 @@ Both `username` and `password` come from a single Vault read — one lease, one
146
148
 
147
149
  Lease names are stable across environments. The actual Vault paths are deployment-specific config.
148
150
 
151
+ ## Multi-Cluster Vault
152
+
153
+ `VaultCluster` supports connecting to multiple Vault clusters simultaneously. Each cluster has its own `::Vault::Client` instance.
154
+
155
+ ```json
156
+ {
157
+ "crypt": {
158
+ "vault": {
159
+ "default": "primary",
160
+ "clusters": {
161
+ "primary": {
162
+ "protocol": "https",
163
+ "address": "vault.example.com",
164
+ "port": 8200,
165
+ "namespace": "my-namespace",
166
+ "auth_method": "ldap"
167
+ },
168
+ "secondary": {
169
+ "protocol": "https",
170
+ "address": "vault2.example.com",
171
+ "port": 8200,
172
+ "auth_method": "ldap"
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ ```ruby
181
+ # Authenticate to all LDAP-configured clusters at once
182
+ Legion::Crypt.ldap_login_all(username: 'user', password: 'pass')
183
+
184
+ # Read from specific cluster
185
+ Legion::Crypt.read('secret/data/mykey', cluster: :secondary)
186
+
187
+ # Get a Vault client for a specific cluster
188
+ client = Legion::Crypt.vault_client(:primary)
189
+ ```
190
+
191
+ When `clusters` is empty, the legacy single-cluster path is used (backward compatible).
192
+
149
193
  ## Requirements
150
194
 
151
195
  - Ruby >= 3.4
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Crypt
5
+ module LdapAuth
6
+ def ldap_login(cluster_name:, username:, password:)
7
+ cluster_name = cluster_name.to_sym
8
+ client = vault_client(cluster_name)
9
+ secret = client.logical.write("auth/ldap/login/#{username}", password: password)
10
+ auth = secret.auth
11
+ token = auth.client_token
12
+
13
+ clusters[cluster_name][:token] = token
14
+ clusters[cluster_name][:connected] = true
15
+
16
+ { token: token, lease_duration: auth.lease_duration,
17
+ renewable: auth.renewable, policies: auth.policies }
18
+ end
19
+
20
+ def ldap_login_all(username:, password:)
21
+ results = {}
22
+ clusters.each do |name, config|
23
+ next unless config[:auth_method] == 'ldap'
24
+
25
+ results[name] = ldap_login(cluster_name: name, username: username, password: password)
26
+ rescue StandardError => e
27
+ results[name] = { error: e.message }
28
+ end
29
+ results
30
+ end
31
+ end
32
+ end
33
+ end
@@ -3,10 +3,21 @@
3
3
  module Legion
4
4
  module Crypt
5
5
  module Settings
6
+ def self.tls
7
+ {
8
+ enabled: false,
9
+ verify: 'peer',
10
+ ca: nil,
11
+ cert: nil,
12
+ key: nil
13
+ }
14
+ end
15
+
6
16
  def self.default
7
17
  {
8
18
  vault: vault,
9
19
  jwt: jwt,
20
+ tls: tls,
10
21
  cs_encrypt_ready: false,
11
22
  dynamic_keys: true,
12
23
  cluster_secret: nil,
@@ -39,7 +50,9 @@ module Legion
39
50
  push_cluster_secret: true,
40
51
  read_cluster_secret: true,
41
52
  kv_path: ENV['LEGION_VAULT_KV_PATH'] || 'legion',
42
- leases: {}
53
+ leases: {},
54
+ default: nil,
55
+ clusters: {}
43
56
  }
44
57
  end
45
58
  end
@@ -1,78 +1,92 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'openssl'
4
-
5
3
  module Legion
6
4
  module Crypt
7
5
  module TLS
8
- DEFAULT_CERT_DIR = '/etc/legion/tls'
6
+ TLS_PORTS = {
7
+ 5671 => 'amqp',
8
+ 6380 => 'redis',
9
+ 11_207 => 'memcached'
10
+ }.freeze
9
11
 
10
12
  class << self
11
- def enabled?
12
- settings_dig(:enabled) == true
13
- end
13
+ def resolve(tls_config, port: nil)
14
+ config = symbolize_keys(migrate_legacy(tls_config || {}))
14
15
 
15
- def ssl_context(role: :client) # rubocop:disable Lint/UnusedMethodArgument
16
- ctx = OpenSSL::SSL::SSLContext.new
17
- ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
18
- ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
16
+ enabled = config[:enabled]
17
+ auto_detected = false
19
18
 
20
- ctx.cert = OpenSSL::X509::Certificate.new(File.read(cert_path)) if cert_path && File.exist?(cert_path)
21
- ctx.key = OpenSSL::PKey.read(File.read(key_path)) if key_path && File.exist?(key_path)
22
- ctx.ca_file = ca_path if ca_path && File.exist?(ca_path)
19
+ if enabled.nil? && port && TLS_PORTS.key?(port.to_i)
20
+ enabled = true
21
+ auto_detected = true
22
+ log_warn("TLS auto-enabled for port #{port}")
23
+ end
23
24
 
24
- ctx
25
- end
25
+ enabled = false if enabled.nil?
26
26
 
27
- def bunny_options
28
- return {} unless enabled?
27
+ verify = normalize_verify(config[:verify])
28
+ ca = resolve_uri(config[:ca])
29
+ cert = resolve_uri(config[:cert])
30
+ key = resolve_uri(config[:key])
31
+
32
+ if verify == :mutual && (cert.nil? || key.nil?)
33
+ log_warn('TLS mutual requested but cert or key missing, downgrading to peer')
34
+ verify = :peer
35
+ end
29
36
 
30
37
  {
31
- tls: true,
32
- tls_cert: cert_path,
33
- tls_key: key_path,
34
- tls_ca_certificates: [ca_path].compact,
35
- verify_peer: true
38
+ enabled: enabled,
39
+ verify: verify,
40
+ ca: ca,
41
+ cert: cert,
42
+ key: key,
43
+ auto_detected: auto_detected
36
44
  }
37
45
  end
38
46
 
39
- def sequel_options
40
- return {} unless enabled?
47
+ def migrate_legacy(config)
48
+ config = symbolize_keys(config)
49
+ return config unless config.key?(:use_tls) && !config.key?(:enabled)
41
50
 
42
51
  {
43
- sslmode: 'verify-full',
44
- sslcert: cert_path,
45
- sslkey: key_path,
46
- sslrootcert: ca_path
52
+ enabled: config[:use_tls],
53
+ verify: config[:verify_peer] ? 'peer' : 'none',
54
+ ca: config[:ca_certs],
55
+ cert: config[:tls_cert],
56
+ key: config[:tls_key]
47
57
  }
48
58
  end
49
59
 
50
- def cert_path
51
- settings_dig(:cert_path) || File.join(DEFAULT_CERT_DIR, 'legion.crt')
52
- end
60
+ private
53
61
 
54
- def key_path
55
- settings_dig(:key_path) || File.join(DEFAULT_CERT_DIR, 'legion.key')
62
+ def symbolize_keys(hash)
63
+ hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
56
64
  end
57
65
 
58
- def ca_path
59
- settings_dig(:ca_path) || File.join(DEFAULT_CERT_DIR, 'ca-bundle.crt')
66
+ def normalize_verify(value)
67
+ case value.to_s
68
+ when 'none' then :none
69
+ when 'mutual' then :mutual
70
+ else :peer
71
+ end
60
72
  end
61
73
 
62
- private
63
-
64
- def settings_dig(*keys)
65
- return nil unless defined?(Legion::Settings)
74
+ def resolve_uri(value)
75
+ return nil if value.nil?
66
76
 
67
- result = Legion::Settings[:crypt]
68
- [:tls, *keys].each do |key|
69
- return nil unless result.is_a?(Hash)
77
+ if defined?(Legion::Settings::Resolver)
78
+ Legion::Settings::Resolver.resolve_value(value)
79
+ else
80
+ value
81
+ end
82
+ end
70
83
 
71
- result = result[key]
84
+ def log_warn(msg)
85
+ if defined?(Legion::Logging)
86
+ Legion::Logging.warn(msg)
87
+ else
88
+ warn msg
72
89
  end
73
- result
74
- rescue StandardError
75
- nil
76
90
  end
77
91
  end
78
92
  end
@@ -83,8 +83,21 @@ module Legion
83
83
  end
84
84
 
85
85
  def renew_sessions(**_opts)
86
- @sessions.each do |session|
87
- renew_session(session: session)
86
+ if respond_to?(:connected_clusters) && connected_clusters.any?
87
+ renew_cluster_tokens
88
+ else
89
+ @sessions.each do |session|
90
+ renew_session(session: session)
91
+ end
92
+ end
93
+ end
94
+
95
+ def renew_cluster_tokens
96
+ connected_clusters.each_key do |name|
97
+ client = vault_client(name)
98
+ client.auth_token.renew_self
99
+ rescue StandardError => e
100
+ log_vault_error(name, e)
88
101
  end
89
102
  end
90
103
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vault'
4
+
5
+ module Legion
6
+ module Crypt
7
+ module VaultCluster
8
+ def vault_client(name = nil)
9
+ name = resolve_cluster_name(name)
10
+ @vault_clients ||= {}
11
+ @vault_clients[name] ||= build_vault_client(clusters[name])
12
+ end
13
+
14
+ def cluster(name = nil)
15
+ name = resolve_cluster_name(name)
16
+ clusters[name]
17
+ end
18
+
19
+ def default_cluster_name
20
+ name = vault_settings[:default]
21
+ name ? name.to_sym : clusters.keys.first
22
+ end
23
+
24
+ def clusters
25
+ vault_settings[:clusters] || {}
26
+ end
27
+
28
+ def connected_clusters
29
+ clusters.select { |_, config| config[:token] && config[:connected] }
30
+ end
31
+
32
+ def connect_all_clusters
33
+ results = {}
34
+ clusters.each do |name, config|
35
+ next unless config[:token]
36
+
37
+ client = vault_client(name)
38
+ config[:connected] = client.sys.health_status.initialized?
39
+ results[name] = config[:connected]
40
+ rescue StandardError => e
41
+ config[:connected] = false
42
+ results[name] = false
43
+ log_vault_error(name, e)
44
+ end
45
+ results
46
+ end
47
+
48
+ private
49
+
50
+ def resolve_cluster_name(name)
51
+ return name.to_sym if name
52
+
53
+ default_cluster_name
54
+ end
55
+
56
+ def build_vault_client(config)
57
+ return nil unless config.is_a?(Hash)
58
+
59
+ client = ::Vault::Client.new(
60
+ address: "#{config[:protocol]}://#{config[:address]}:#{config[:port]}",
61
+ token: config[:token]
62
+ )
63
+ client.namespace = config[:namespace] if config[:namespace]
64
+ client
65
+ end
66
+
67
+ def log_vault_error(name, error)
68
+ if defined?(Legion::Logging)
69
+ Legion::Logging.error("Vault cluster #{name}: #{error.message}")
70
+ else
71
+ warn("Vault cluster #{name}: #{error.message}")
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Crypt
5
+ module VaultKerberosAuth
6
+ DEFAULT_AUTH_PATH = 'auth/kerberos/login'
7
+
8
+ class AuthError < StandardError; end
9
+
10
+ def self.login(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
11
+ raise AuthError, 'Vault is not connected' unless vault_connected?
12
+
13
+ response = ::Vault.logical.write(auth_path, authorization: "Negotiate #{spnego_token}")
14
+ raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
15
+
16
+ {
17
+ token: response.auth.client_token,
18
+ lease_duration: response.auth.lease_duration,
19
+ renewable: response.auth.renewable,
20
+ policies: response.auth.policies,
21
+ metadata: response.auth.metadata
22
+ }
23
+ rescue ::Vault::HTTPClientError => e
24
+ raise AuthError, "Vault Kerberos auth failed: #{e.message}"
25
+ end
26
+
27
+ def self.login!(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
28
+ result = login(spnego_token: spnego_token, auth_path: auth_path)
29
+ ::Vault.token = result[:token]
30
+ result
31
+ end
32
+
33
+ def self.vault_connected?
34
+ defined?(::Vault) && defined?(Legion::Settings) &&
35
+ Legion::Settings[:crypt][:vault][:connected] == true
36
+ rescue StandardError
37
+ false
38
+ end
39
+
40
+ private_class_method :vault_connected?
41
+ end
42
+ end
43
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.3'
5
+ VERSION = '1.4.5'
6
6
  end
7
7
  end
data/lib/legion/crypt.rb CHANGED
@@ -8,9 +8,14 @@ require 'legion/crypt/cipher'
8
8
  require 'legion/crypt/jwt'
9
9
  require 'legion/crypt/vault_jwt_auth'
10
10
  require 'legion/crypt/lease_manager'
11
+ require 'legion/crypt/vault_cluster'
12
+ require 'legion/crypt/ldap_auth'
11
13
 
12
14
  module Legion
13
15
  module Crypt
16
+ extend Legion::Crypt::VaultCluster
17
+ extend Legion::Crypt::LdapAuth
18
+
14
19
  class << self
15
20
  attr_reader :sessions
16
21
 
@@ -21,11 +26,19 @@ module Legion
21
26
  include Legion::Crypt::Vault
22
27
  end
23
28
 
29
+ def vault_settings
30
+ Legion::Settings[:crypt][:vault]
31
+ end
32
+
24
33
  def start
25
34
  Legion::Logging.debug 'Legion::Crypt is running start'
26
35
  ::File.write('./legionio.key', private_key) if settings[:save_private_key]
27
36
 
28
- connect_vault unless settings[:vault][:token].nil?
37
+ if vault_settings[:clusters]&.any?
38
+ connect_all_clusters
39
+ else
40
+ connect_vault unless settings[:vault][:token].nil?
41
+ end
29
42
  start_lease_manager
30
43
  end
31
44
 
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.3
4
+ version: 1.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -78,13 +78,16 @@ files:
78
78
  - lib/legion/crypt/erasure.rb
79
79
  - lib/legion/crypt/jwks_client.rb
80
80
  - lib/legion/crypt/jwt.rb
81
+ - lib/legion/crypt/ldap_auth.rb
81
82
  - lib/legion/crypt/lease_manager.rb
82
83
  - lib/legion/crypt/mock_vault.rb
83
84
  - lib/legion/crypt/partition_keys.rb
84
85
  - lib/legion/crypt/settings.rb
85
86
  - lib/legion/crypt/tls.rb
86
87
  - lib/legion/crypt/vault.rb
88
+ - lib/legion/crypt/vault_cluster.rb
87
89
  - lib/legion/crypt/vault_jwt_auth.rb
90
+ - lib/legion/crypt/vault_kerberos_auth.rb
88
91
  - lib/legion/crypt/vault_renewer.rb
89
92
  - lib/legion/crypt/version.rb
90
93
  - sonar-project.properties