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 +4 -4
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +12 -0
- data/README.md +30 -1
- data/lib/legion/crypt/kerberos_auth.rb +2 -8
- data/lib/legion/crypt/lease_manager.rb +15 -4
- data/lib/legion/crypt/settings.rb +1 -0
- data/lib/legion/crypt/token_renewer.rb +26 -3
- data/lib/legion/crypt/vault_cluster.rb +9 -1
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +18 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ff70cf0304e424576841101c5c3399bded877b2995200049a76d7741f008bdf
|
|
4
|
+
data.tar.gz: 7440aa2dfb8246fac7cb115bbed096a9ee7dd21b7538efdea69388a3293df313
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
47
|
-
#
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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]
|
|
@@ -34,9 +34,7 @@ module Legion
|
|
|
34
34
|
rescue ThreadError
|
|
35
35
|
nil
|
|
36
36
|
ensure
|
|
37
|
-
|
|
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
|
-
|
|
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
|
data/lib/legion/crypt/version.rb
CHANGED
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
|