legion-crypt 1.4.15 → 1.4.18

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: 5f43eee9680197c62f53f2a7ed8c77eee725f7f01744c32e5ad0115fdfb2ad21
4
- data.tar.gz: 3ba3cd7da0684d8a9ec68d23797d487240035dc2f5e07fdba7ab0cfb1727dafe
3
+ metadata.gz: 9397c4eab60b75a13e4f1e9fd842ce6b4908d94d891e9ea29a893fb33e71d5ef
4
+ data.tar.gz: 852ec5569b543089412ce28e96b32f6424e7da1f169e836d3e596dd51719c74d
5
5
  SHA512:
6
- metadata.gz: 79fe4cd8653f9a09c3c2f6acc7da403e00d8a8e05b6fb6f488fb25a3c8fadfcbee00cc1ffa7e31319840025a5d622d2b1b6a32210ff0cd9141a04aef26c27e59
7
- data.tar.gz: b691e62e093b3504d7e7da0501cda48aca9ae705429ed1d5caf97dd3ab7a9b7f87826a8d0cf4dabbff01e1f755d7cee555d74dee85754f7206feb5cadbfdd8f8
6
+ metadata.gz: 59170bcaead98cda37e2be65b1e9b294ab4eff65fde9251ebb2de24390224e8ed6bf3d62a523a11cd9ac1f2ead894dbffeee79815194c583e47a565b9ac7ff1c
7
+ data.tar.gz: 7f65e04abcb52c320f0d6e133569604d8eba37dcd4d63bcf046b0ba8fb70c631ebf4723bce2cd8ce728bf46a7b4992af389c3cb0da25bfd9c7c1176a38e56866
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.4.18] - 2026-03-26
4
+
5
+ ### Fixed
6
+ - `KerberosAuth.login`: clear `@kerberos_principal` at the start of each login attempt so a failed re-auth does not leave a stale principal from a previous successful login
7
+
8
+ ### Added
9
+ - `crypt_spec.rb`: delegation spec for `Legion::Crypt.kerberos_principal`
10
+ - `kerberos_auth_spec.rb`: spec verifying stale principal is cleared before a failing login attempt
11
+
12
+ ## [1.4.17] - 2026-03-26
13
+
14
+ ### Added
15
+ - Store Kerberos principal after successful SPNEGO authentication (`KerberosAuth.kerberos_principal`)
16
+ - Expose `Legion::Crypt.kerberos_principal` delegation
17
+
18
+ ## [1.4.16] - 2026-03-26
19
+
20
+ ### Changed
21
+ - `KerberosAuth#exchange_token`: removed namespace clear/restore logic — Kerberos auth is now mounted inside the target namespace, client namespace is preserved so the issued token is scoped correctly
22
+ - `VaultCluster#connect_kerberos_cluster`: set token on the cached vault_client after Kerberos auth (`vault_client(name).token = result[:token]`) so the memoized client is immediately usable
23
+ - `VaultCluster#build_vault_client`: fall back to `Settings[:crypt][:vault][:vault_namespace]` when `config[:namespace]` is absent, guarded with `defined?(Legion::Settings)`
24
+ - `TokenRenewer#stop`: revoke the Vault token on shutdown (only for Kerberos auth_method; token-based clusters are not revoked)
25
+ - `LeaseManager#start`: accepts optional `vault_client:` keyword argument; stores and routes `logical.read` through it when provided
26
+ - `LeaseManager#shutdown`: routes `sys.revoke` through the cluster vault_client when one was supplied
27
+ - `LeaseManager#renew_lease`: routes `sys.renew` through the cluster vault_client when one was supplied
28
+ - `Crypt#start_lease_manager`: triggers when `connected_clusters.any?` in addition to the single-cluster `vault.connected` flag; passes the default cluster client to the lease manager
29
+
30
+ ### Added
31
+ - `vault_namespace: 'legionio'` default in `Settings.vault` — used as namespace fallback for cluster clients when `config[:namespace]` is not set
32
+ - `TokenRenewer#revoke_token` private method: self-revokes the token via `auth_token.revoke_self`, guarded to Kerberos auth_method only
33
+
34
+ ### Fixed
35
+ - `TokenRenewer#stop`: skip token revocation when renewal thread is still alive after join timeout to prevent racy revocation against a running thread; log warning instead
36
+ - `Crypt#start_lease_manager`: use `vault_settings[:default]` (matching `VaultCluster#default_cluster_name`) instead of the nonexistent `:default_cluster` key so configured default cluster is honored
37
+ - `LeaseManager#start`: always assign `@vault_client` before early return so subsequent `shutdown`/`reset!` calls do not use a stale cluster client; clear `@vault_client` in both `shutdown` and `reset!`
38
+
3
39
  ## [1.4.15] - 2026-03-26
4
40
 
5
41
  ### Fixed
data/CLAUDE.md CHANGED
@@ -57,6 +57,12 @@ Legion::Crypt (singleton module)
57
57
  ├── Erasure # Cryptographic erasure via Vault master key deletion
58
58
  ├── Attestation # Signed identity claims with Ed25519, freshness checking
59
59
  ├── LeaseManager # Dynamic Vault lease lifecycle: fetch, cache, renew, rotate, push-back
60
+ ├── KerberosAuth # GSSAPI SPNEGO token acquisition; `disable_gssapi_finalizers` prevents GC segfault on macOS
61
+ ├── VaultKerberosAuth # Vault Kerberos auth: SPNEGO as `Authorization` header, namespace clear/restore, TokenRenewer wiring
62
+ ├── LdapAuth # Vault LDAP auth backend
63
+ ├── Tls # TLS settings (cert/key/CA/verify_peer/Vault PKI)
64
+ ├── Mtls # mTLS cert issuance (Vault PKI) + CertRotation background thread (50% TTL renewal)
65
+ ├── TokenRenewer # Background renewal thread: 75% TTL renew, Kerberos re-auth on failure, exponential backoff
60
66
  ├── MockVault # In-memory Vault mock for local development mode
61
67
  ├── Settings # Default crypt config
62
68
  └── Version
@@ -120,6 +126,12 @@ Dev dependencies: `legion-logging`, `legion-settings`
120
126
  | `lib/legion/crypt/partition_keys.rb` | HKDF per-tenant key derivation with AES-256-GCM |
121
127
  | `lib/legion/crypt/erasure.rb` | Cryptographic erasure via Vault master key deletion |
122
128
  | `lib/legion/crypt/attestation.rb` | Signed identity claims with Ed25519 signatures |
129
+ | `lib/legion/crypt/kerberos_auth.rb` | GSSAPI/Kerberos token acquisition; `obtain_spnego_token`, `disable_gssapi_finalizers` (prevents GC segfault on macOS) |
130
+ | `lib/legion/crypt/vault_kerberos_auth.rb` | Vault Kerberos auth backend: sends SPNEGO token as `Authorization` HTTP header, clears/restores namespace, wires `TokenRenewer` |
131
+ | `lib/legion/crypt/ldap_auth.rb` | Vault LDAP auth backend integration |
132
+ | `lib/legion/crypt/tls.rb` | TLS settings module (cert, key, CA paths, verify_peer, Vault PKI flag) |
133
+ | `lib/legion/crypt/mtls.rb` | mTLS certificate issuance from Vault PKI; `CertRotation` background renewal thread (50% TTL) |
134
+ | `lib/legion/crypt/token_renewer.rb` | Plain Thread renewer: renews at 75% TTL, re-auths via Kerberos on failure, exponential backoff |
123
135
  | `lib/legion/crypt/version.rb` | VERSION constant |
124
136
 
125
137
  ## Role in LegionIO
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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
4
 
5
- **Version**: 1.4.7
5
+ **Version**: 1.4.15
6
6
 
7
7
  ## Installation
8
8
 
@@ -190,12 +190,41 @@ client = Legion::Crypt.vault_client(:primary)
190
190
 
191
191
  When `clusters` is empty, the legacy single-cluster path is used (backward compatible).
192
192
 
193
+ ## Kerberos Authentication
194
+
195
+ When `crypt.vault.auth_method` is set to `kerberos`, `Crypt.start` performs Kerberos auto-auth to Vault using `KerberosAuth`:
196
+
197
+ ```ruby
198
+ # Settings
199
+ {
200
+ "crypt": {
201
+ "vault": {
202
+ "auth_method": "kerberos",
203
+ "kerberos": {
204
+ "service_principal": "HTTP/vault.example.com@REALM",
205
+ "auth_path": "auth/kerberos/login"
206
+ }
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ The SPNEGO token is sent as an HTTP `Authorization` header (not JSON body). The Vault namespace is cleared before auth (Kerberos mount is at root) and restored after. Requires Homebrew MIT Kerberos (`brew install krb5`) on macOS — the system Heimdal library is not compatible.
213
+
214
+ `TokenRenewer` keeps the Vault token alive: renews at 75% TTL, re-auths via Kerberos if renewal fails, uses exponential backoff.
215
+
216
+ ## mTLS
217
+
218
+ `Crypt::Mtls` issues mTLS certificates from Vault PKI. `Crypt::CertRotation` runs a background thread renewing certs at 50% TTL. `Transport::Connection::Vault` applies tempfile-based Bunny mTLS. Feature-flagged via `security.mtls.enabled: false`.
219
+
193
220
  ## Requirements
194
221
 
195
222
  - Ruby >= 3.4
223
+ - `ed25519` (~> 1.3)
196
224
  - `jwt` gem (>= 2.7)
197
225
  - `vault` gem (>= 0.17, optional)
198
226
  - HashiCorp Vault (optional, for secrets management)
227
+ - `gssapi` gem (optional, required for Kerberos auth)
199
228
 
200
229
  ## License
201
230
 
@@ -8,11 +8,20 @@ module Legion
8
8
 
9
9
  DEFAULT_AUTH_PATH = 'auth/kerberos/login'
10
10
 
11
+ @kerberos_principal = nil
12
+
13
+ class << self
14
+ attr_reader :kerberos_principal
15
+ end
16
+
11
17
  def self.login(vault_client:, service_principal:, auth_path: DEFAULT_AUTH_PATH)
12
18
  raise GemMissingError, 'lex-kerberos gem is required for Kerberos auth' unless spnego_available?
13
19
 
20
+ @kerberos_principal = nil
14
21
  token = obtain_token(service_principal)
15
- exchange_token(vault_client, token, auth_path)
22
+ result = exchange_token(vault_client, token, auth_path)
23
+ @kerberos_principal = result[:metadata]&.dig('username') || result[:metadata]&.dig(:username)
24
+ result
16
25
  end
17
26
 
18
27
  def self.spnego_available?
@@ -29,6 +38,7 @@ module Legion
29
38
 
30
39
  def self.reset!
31
40
  @spnego_available = nil
41
+ @kerberos_principal = nil
32
42
  end
33
43
 
34
44
  class << self
@@ -43,11 +53,8 @@ module Legion
43
53
  end
44
54
 
45
55
  def exchange_token(vault_client, spnego_token, auth_path)
46
- # Kerberos auth is mounted at the root namespace. Temporarily
47
- # clear the client namespace so the request reaches the correct
48
- # mount path, then restore it for subsequent operations.
49
- saved_ns = vault_client.namespace
50
- vault_client.namespace = nil
56
+ # Kerberos auth is mounted inside the target namespace. Keep the
57
+ # client namespace so the token is scoped to it.
51
58
 
52
59
  # The Vault Kerberos plugin reads the SPNEGO token from the HTTP
53
60
  # Authorization header, not the JSON body.
@@ -59,8 +66,6 @@ module Legion
59
66
  response = ::Vault::Secret.decode(json)
60
67
  raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
61
68
 
62
- vault_client.namespace = saved_ns
63
-
64
69
  auth = response.auth
65
70
  {
66
71
  token: auth.client_token,
@@ -70,7 +75,6 @@ module Legion
70
75
  metadata: auth.metadata
71
76
  }
72
77
  rescue ::Vault::HTTPClientError => e
73
- vault_client.namespace = saved_ns if saved_ns
74
78
  raise AuthError, "Vault Kerberos auth failed: #{e.message}"
75
79
  end
76
80
  end
@@ -17,7 +17,8 @@ module Legion
17
17
  @renewal_thread = nil
18
18
  end
19
19
 
20
- def start(definitions)
20
+ def start(definitions, vault_client: nil)
21
+ @vault_client = vault_client
21
22
  return if definitions.nil? || definitions.empty?
22
23
 
23
24
  definitions.each do |name, opts|
@@ -25,7 +26,7 @@ module Legion
25
26
  next unless path
26
27
 
27
28
  begin
28
- response = ::Vault.logical.read(path)
29
+ response = logical.read(path)
29
30
  next unless response
30
31
 
31
32
  @lease_cache[name] = response.data || {}
@@ -95,7 +96,7 @@ module Legion
95
96
  next if lease_id.nil? || lease_id.empty?
96
97
 
97
98
  begin
98
- ::Vault.sys.revoke(lease_id)
99
+ sys.revoke(lease_id)
99
100
  log_debug("LeaseManager: revoked lease '#{name}' (#{lease_id})")
100
101
  rescue StandardError => e
101
102
  log_warn("LeaseManager: failed to revoke lease '#{name}' (#{lease_id}): #{e.message}")
@@ -105,6 +106,7 @@ module Legion
105
106
  @lease_cache.clear
106
107
  @active_leases.clear
107
108
  @refs.clear
109
+ @vault_client = nil
108
110
  end
109
111
 
110
112
  def reset!
@@ -112,10 +114,19 @@ module Legion
112
114
  @lease_cache.clear
113
115
  @active_leases.clear
114
116
  @refs.clear
117
+ @vault_client = nil
115
118
  end
116
119
 
117
120
  private
118
121
 
122
+ def logical
123
+ @vault_client ? @vault_client.logical : ::Vault.logical
124
+ end
125
+
126
+ def sys
127
+ @vault_client ? @vault_client.sys : ::Vault.sys
128
+ end
129
+
119
130
  def stop_renewal_thread
120
131
  @running = false
121
132
  if @renewal_thread&.alive?
@@ -145,7 +156,7 @@ module Legion
145
156
  end
146
157
 
147
158
  def renew_lease(name, lease)
148
- response = ::Vault.sys.renew(lease[:lease_id])
159
+ response = sys.renew(lease[:lease_id])
149
160
  lease[:expires_at] = Time.now + (response.lease_duration || 0)
150
161
 
151
162
  if response.data && response.data != @lease_cache[name]
@@ -52,6 +52,7 @@ module Legion
52
52
  kv_path: ENV['LEGION_VAULT_KV_PATH'] || 'legion',
53
53
  leases: {},
54
54
  default: nil,
55
+ vault_namespace: 'legionio',
55
56
  kerberos: {
56
57
  service_principal: nil,
57
58
  auth_path: 'auth/kerberos/login'
@@ -34,9 +34,7 @@ module Legion
34
34
  rescue ThreadError
35
35
  nil
36
36
  ensure
37
- @thread&.join(5)
38
- @thread = nil
39
- log_debug('token renewal thread stopped')
37
+ stop_thread_and_revoke
40
38
  end
41
39
 
42
40
  def running?
@@ -127,6 +125,31 @@ module Legion
127
125
  end
128
126
  end
129
127
 
128
+ def stop_thread_and_revoke
129
+ return unless @thread
130
+
131
+ @thread.join(5)
132
+ thread_still_running = @thread.alive?
133
+ @thread = nil
134
+
135
+ if thread_still_running
136
+ log_warn('token renewal thread did not stop within timeout; skipping token revocation')
137
+ else
138
+ revoke_token
139
+ log_debug('token renewal thread stopped')
140
+ end
141
+ end
142
+
143
+ def revoke_token
144
+ return unless @vault_client&.token
145
+ return unless @config[:auth_method]&.to_s == 'kerberos'
146
+
147
+ @vault_client.auth_token.revoke_self
148
+ log_info('Vault token revoked')
149
+ rescue StandardError => e
150
+ log_warn("Vault token revoke failed: #{e.message}")
151
+ end
152
+
130
153
  def log_debug(message)
131
154
  Legion::Logging.debug("TokenRenewer[#{@cluster_name}]: #{message}") if defined?(Legion::Logging)
132
155
  end
@@ -78,7 +78,14 @@ module Legion
78
78
  address: "#{config[:protocol]}://#{config[:address]}:#{config[:port]}",
79
79
  token: config[:token]
80
80
  )
81
- client.namespace = config[:namespace] if config[:namespace]
81
+ namespace =
82
+ if config.key?(:namespace)
83
+ config[:namespace]
84
+ elsif defined?(Legion::Settings)
85
+ crypt_settings = Legion::Settings[:crypt]
86
+ crypt_settings.respond_to?(:dig) ? crypt_settings.dig(:vault, :vault_namespace) : nil
87
+ end
88
+ client.namespace = namespace if namespace
82
89
  client
83
90
  end
84
91
 
@@ -111,6 +118,7 @@ module Legion
111
118
  config[:lease_duration] = result[:lease_duration]
112
119
  config[:renewable] = result[:renewable]
113
120
  config[:connected] = true
121
+ vault_client(name).token = result[:token]
114
122
  log_cluster_connected(name, config)
115
123
  true
116
124
  rescue Legion::Crypt::KerberosAuth::GemMissingError => e
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.15'
5
+ VERSION = '1.4.18'
6
6
  end
7
7
  end
data/lib/legion/crypt.rb CHANGED
@@ -34,6 +34,10 @@ module Legion
34
34
  Legion::Settings[:crypt][:vault]
35
35
  end
36
36
 
37
+ def kerberos_principal
38
+ KerberosAuth.kerberos_principal
39
+ end
40
+
37
41
  def start
38
42
  Legion::Logging.debug 'Legion::Crypt is running start'
39
43
  ::File.write('./legionio.key', private_key) if settings[:save_private_key]
@@ -99,10 +103,25 @@ module Legion
99
103
  def start_lease_manager
100
104
  leases = settings.dig(:vault, :leases) || {}
101
105
  return if leases.empty?
102
- return unless settings.dig(:vault, :connected)
103
-
106
+ return unless settings.dig(:vault, :connected) || connected_clusters.any?
107
+
108
+ client = nil
109
+
110
+ if settings.dig(:vault, :connected)
111
+ client = vault_client
112
+ elsif connected_clusters.any?
113
+ default_cluster = vault_settings[:default]
114
+ selected_cluster =
115
+ if default_cluster && connected_clusters.include?(default_cluster.to_sym)
116
+ default_cluster.to_sym
117
+ else
118
+ connected_clusters.keys.first
119
+ end
120
+
121
+ client = selected_cluster ? vault_client(selected_cluster) : nil
122
+ end
104
123
  lease_manager = Legion::Crypt::LeaseManager.instance
105
- lease_manager.start(leases)
124
+ lease_manager.start(leases, vault_client: client)
106
125
  lease_manager.start_renewal_thread
107
126
  Legion::Logging.info "LeaseManager: #{leases.size} lease(s) initialized"
108
127
  rescue StandardError => e
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.15
4
+ version: 1.4.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity