legion-crypt 1.4.15 → 1.4.16

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: 6ff70cf0304e424576841101c5c3399bded877b2995200049a76d7741f008bdf
4
+ data.tar.gz: 7440aa2dfb8246fac7cb115bbed096a9ee7dd21b7538efdea69388a3293df313
5
5
  SHA512:
6
- metadata.gz: 79fe4cd8653f9a09c3c2f6acc7da403e00d8a8e05b6fb6f488fb25a3c8fadfcbee00cc1ffa7e31319840025a5d622d2b1b6a32210ff0cd9141a04aef26c27e59
7
- data.tar.gz: b691e62e093b3504d7e7da0501cda48aca9ae705429ed1d5caf97dd3ab7a9b7f87826a8d0cf4dabbff01e1f755d7cee555d74dee85754f7206feb5cadbfdd8f8
6
+ metadata.gz: 10d2966123e6e1764e039001f8543f56abd5b2239293947ade3d0db69dcbe80e5117e8ef948b3bbbbdd42d42ce31bdf539c2f4d3a2a0007afcdcb7308286f6fd
7
+ data.tar.gz: c226b7ac31c8a8dc310224c4d7fb8501da35ac4489cfa144d469ca4d1a96a4fa9337e23d43b5725c893c95a47e52f8503378cb2d9f743cf3b45b963d7b8dfe28
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.4.16] - 2026-03-26
4
+
5
+ ### Changed
6
+ - `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
7
+ - `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
8
+ - `VaultCluster#build_vault_client`: fall back to `Settings[:crypt][:vault][:vault_namespace]` when `config[:namespace]` is absent, guarded with `defined?(Legion::Settings)`
9
+ - `TokenRenewer#stop`: revoke the Vault token on shutdown (only for Kerberos auth_method; token-based clusters are not revoked)
10
+ - `LeaseManager#start`: accepts optional `vault_client:` keyword argument; stores and routes `logical.read` through it when provided
11
+ - `LeaseManager#shutdown`: routes `sys.revoke` through the cluster vault_client when one was supplied
12
+ - `LeaseManager#renew_lease`: routes `sys.renew` through the cluster vault_client when one was supplied
13
+ - `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
14
+
15
+ ### Added
16
+ - `vault_namespace: 'legionio'` default in `Settings.vault` — used as namespace fallback for cluster clients when `config[:namespace]` is not set
17
+ - `TokenRenewer#revoke_token` private method: self-revokes the token via `auth_token.revoke_self`, guarded to Kerberos auth_method only
18
+
19
+ ### Fixed
20
+ - `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
21
+ - `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
22
+ - `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!`
23
+
3
24
  ## [1.4.15] - 2026-03-26
4
25
 
5
26
  ### 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
 
@@ -43,11 +43,8 @@ module Legion
43
43
  end
44
44
 
45
45
  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
46
+ # Kerberos auth is mounted inside the target namespace. Keep the
47
+ # client namespace so the token is scoped to it.
51
48
 
52
49
  # The Vault Kerberos plugin reads the SPNEGO token from the HTTP
53
50
  # Authorization header, not the JSON body.
@@ -59,8 +56,6 @@ module Legion
59
56
  response = ::Vault::Secret.decode(json)
60
57
  raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
61
58
 
62
- vault_client.namespace = saved_ns
63
-
64
59
  auth = response.auth
65
60
  {
66
61
  token: auth.client_token,
@@ -70,7 +65,6 @@ module Legion
70
65
  metadata: auth.metadata
71
66
  }
72
67
  rescue ::Vault::HTTPClientError => e
73
- vault_client.namespace = saved_ns if saved_ns
74
68
  raise AuthError, "Vault Kerberos auth failed: #{e.message}"
75
69
  end
76
70
  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.16'
6
6
  end
7
7
  end
data/lib/legion/crypt.rb CHANGED
@@ -99,10 +99,25 @@ module Legion
99
99
  def start_lease_manager
100
100
  leases = settings.dig(:vault, :leases) || {}
101
101
  return if leases.empty?
102
- return unless settings.dig(:vault, :connected)
103
-
102
+ return unless settings.dig(:vault, :connected) || connected_clusters.any?
103
+
104
+ client = nil
105
+
106
+ if settings.dig(:vault, :connected)
107
+ client = vault_client
108
+ elsif connected_clusters.any?
109
+ default_cluster = vault_settings[:default]
110
+ selected_cluster =
111
+ if default_cluster && connected_clusters.include?(default_cluster.to_sym)
112
+ default_cluster.to_sym
113
+ else
114
+ connected_clusters.keys.first
115
+ end
116
+
117
+ client = selected_cluster ? vault_client(selected_cluster) : nil
118
+ end
104
119
  lease_manager = Legion::Crypt::LeaseManager.instance
105
- lease_manager.start(leases)
120
+ lease_manager.start(leases, vault_client: client)
106
121
  lease_manager.start_renewal_thread
107
122
  Legion::Logging.info "LeaseManager: #{leases.size} lease(s) initialized"
108
123
  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.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity