legion-crypt 1.5.38 → 1.5.42

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -3
  3. data/.rspec +2 -0
  4. data/AGENTS.md +2 -0
  5. data/CHANGELOG.md +28 -0
  6. data/CLAUDE.md +64 -192
  7. data/CODEOWNERS +4 -4
  8. data/Gemfile +4 -1
  9. data/README.md +60 -16
  10. data/legion-crypt.gemspec +1 -0
  11. data/lib/legion/crypt/cert_rotation.rb +7 -21
  12. data/lib/legion/crypt/cluster_secret.rb +3 -14
  13. data/lib/legion/crypt/ed25519.rb +2 -10
  14. data/lib/legion/crypt/erasure.rb +3 -13
  15. data/lib/legion/crypt/jwks_client.rb +4 -4
  16. data/lib/legion/crypt/kerberos_auth.rb +2 -2
  17. data/lib/legion/crypt/ldap_auth.rb +2 -2
  18. data/lib/legion/crypt/lease_manager.rb +7 -7
  19. data/lib/legion/crypt/mtls.rb +8 -46
  20. data/lib/legion/crypt/partition_keys.rb +2 -6
  21. data/lib/legion/crypt/settings.rb +24 -21
  22. data/lib/legion/crypt/spiffe/svid_rotation.rb +6 -9
  23. data/lib/legion/crypt/spiffe.rb +14 -39
  24. data/lib/legion/crypt/token_renewer.rb +6 -6
  25. data/lib/legion/crypt/vault/client.rb +355 -0
  26. data/lib/legion/crypt/vault/cluster.rb +330 -0
  27. data/lib/legion/crypt/vault/entity.rb +80 -0
  28. data/lib/legion/crypt/vault/jwt_auth.rb +99 -0
  29. data/lib/legion/crypt/vault/kerberos_auth.rb +53 -0
  30. data/lib/legion/crypt/vault/operation.rb +29 -0
  31. data/lib/legion/crypt/vault.rb +6 -11
  32. data/lib/legion/crypt/version.rb +1 -1
  33. data/lib/legion/crypt.rb +69 -80
  34. metadata +23 -8
  35. data/lib/legion/crypt/vault_client.rb +0 -355
  36. data/lib/legion/crypt/vault_cluster.rb +0 -331
  37. data/lib/legion/crypt/vault_entity.rb +0 -78
  38. data/lib/legion/crypt/vault_jwt_auth.rb +0 -97
  39. data/lib/legion/crypt/vault_kerberos_auth.rb +0 -51
  40. data/lib/legion/crypt/vault_operation.rb +0 -27
  41. /data/lib/legion/crypt/{vault_renewer.rb → vault/renewer.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d7d1cbd214ebc5bc7f8d109b958e3d307e3a4faaa81fdb2c47b527670ab720f
4
- data.tar.gz: 4ea67b85552ddae267d4945be1586e7248a30b3dc38dfa2f280ae59311524614
3
+ metadata.gz: bfa9ecbf9d8e83066a57605aa0535a3b529a0a43cfaf3bca6efef509a9d2754a
4
+ data.tar.gz: 8e9107d222b6c35bc269c9d8d52f3c356a64e42b76b8108f9139286e42daa1c3
5
5
  SHA512:
6
- metadata.gz: 2af577b9a0c3c7e464e79f11bbd13e044d9e3e76520c2b4ee6ad27949135c0e873aa8aadfca33d67324061dd55d1f7f5f21ceb1715aa9a7d1d0f1fac77d74852
7
- data.tar.gz: 4226e44bc08ebdc1928ce91630125db8e9a3c674e5ce64cf47ed7375706876388f9aeb3e93816402e41b10c0961bb7f19bd57dc6f64e826289333b27f558c36e
6
+ metadata.gz: 2a94e249cae525b2298465a0f9b2db67c16c75d517cd9a011675a63c63d532d3d128e671762847e32fc71fa18b9466883e7cfac0b12056260a6b22395e4b15e6
7
+ data.tar.gz: 511104bdfec4843ece4157715128f3d38a450194a401086f0626a8670cb0c27a62ca764e892eb4e6fd7cf28ada63c2c19c83e56000daf6d800113b20356a8085
@@ -6,29 +6,56 @@ on:
6
6
  schedule:
7
7
  - cron: '0 9 * * 1'
8
8
 
9
+ permissions: {}
10
+
9
11
  jobs:
10
12
  ci:
11
- uses: LegionIO/.github/.github/workflows/ci.yml@main
13
+ permissions:
14
+ contents: read
15
+ uses: LegionIO/.github/.github/workflows/ci.yml@e0fc7364b73f446d4cb9bdc162a66ed34bf0daa7
16
+ with:
17
+ companion-gems: ${{ github.event_name == 'pull_request' && '{"legion-settings":{"repository":"LegionIO/legion-settings","ref":"8e73a9ef3c13958e3e1e7fc98532e41283159706"}}' || '{}' }}
12
18
 
13
19
  lint:
20
+ permissions:
21
+ checks: write
22
+ contents: read
23
+ pull-requests: read
14
24
  uses: LegionIO/.github/.github/workflows/lint-patterns.yml@main
15
25
 
16
26
  security:
17
- uses: LegionIO/.github/.github/workflows/security-scan.yml@main
27
+ permissions:
28
+ contents: read
29
+ security-events: write
30
+ uses: LegionIO/.github/.github/workflows/security-scan.yml@e0fc7364b73f446d4cb9bdc162a66ed34bf0daa7
31
+ with:
32
+ companion-gems: ${{ github.event_name == 'pull_request' && '{"legion-settings":{"repository":"LegionIO/legion-settings","ref":"8e73a9ef3c13958e3e1e7fc98532e41283159706"}}' || '{}' }}
18
33
 
19
34
  version-changelog:
35
+ permissions:
36
+ contents: read
37
+ pull-requests: read
20
38
  uses: LegionIO/.github/.github/workflows/version-changelog.yml@main
21
39
 
22
40
  dependency-review:
41
+ permissions:
42
+ contents: read
43
+ pull-requests: write
23
44
  uses: LegionIO/.github/.github/workflows/dependency-review.yml@main
24
45
 
25
46
  stale:
26
47
  if: github.event_name == 'schedule'
48
+ permissions:
49
+ issues: write
50
+ pull-requests: write
27
51
  uses: LegionIO/.github/.github/workflows/stale.yml@main
28
52
 
29
53
  release:
30
54
  needs: [ci, lint]
31
55
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
56
+ permissions:
57
+ contents: write
58
+ packages: write
32
59
  uses: LegionIO/.github/.github/workflows/release.yml@main
33
60
  secrets:
34
- rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
61
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require ./spec/spec_helper
2
+ --pattern spec/legion/**/*.rb
data/AGENTS.md CHANGED
@@ -19,6 +19,7 @@ bundle exec rubocop
19
19
  - `lib/legion/crypt/jwt.rb`
20
20
  - `lib/legion/crypt/jwks_client.rb`
21
21
  - `lib/legion/crypt/vault.rb`
22
+ - `lib/legion/crypt/vault/` — `Legion::Crypt::Vault::{Client, Cluster, Operation, Entity, JwtAuth, KerberosAuth, Renewer}`
22
23
  - `lib/legion/crypt/lease_manager.rb`
23
24
  - `lib/legion/crypt/token_renewer.rb`
24
25
  - `lib/legion/crypt/mtls.rb`
@@ -30,6 +31,7 @@ bundle exec rubocop
30
31
  - Keep Vault-dependent logic optional and safely guarded for environments without Vault.
31
32
  - Background renewal/rotation threads must stop cleanly on shutdown and handle failure with bounded retry.
32
33
  - Maintain compatibility for Kerberos, LDAP, and JWT Vault auth paths.
34
+ - Vault components are nested under `Legion::Crypt::Vault`; core `Legion::Crypt::KerberosAuth` handles SPNEGO acquisition and `Legion::Crypt::TokenRenewer` remains outside that namespace.
33
35
  - Cryptographic defaults and key lifecycle behavior are contract-sensitive; change only with test coverage.
34
36
 
35
37
  ## Known Risks
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.5.42] - 2026-11-08
4
+
5
+ ### Changed
6
+ - Move Vault client, cluster routing, operation deadlines, entity management, JWT/Kerberos authentication, and the legacy renewal actor into `lib/legion/crypt/vault/` with matching `Legion::Crypt::Vault::*` namespaces.
7
+ - Update library consumers, specs, and current documentation to the nested require paths and constants; remove the old flat paths without compatibility aliases. Core SPNEGO acquisition and `TokenRenewer` remain in their existing locations.
8
+ - Mirror implementation paths and literal filenames under `spec/`, consolidating root Crypt API examples without dropping coverage. Configure RSpec discovery for the mirrored layout and prioritize `lib/` on the load path to prevent spec files from shadowing production requires.
9
+
10
+ ## [1.5.41] - 2026-11-07
11
+
12
+ ### Fixed
13
+ - Propagate programming errors from Vault connectivity, bootstrap credential acquisition/revocation, lease and SPIFFE startup orchestration, and mTLS interface discovery instead of concealing them as disconnected or fallback states.
14
+ - Handle declared operational failures through structured warning-level exceptions without duplicate raw exception logs; report invalid SPIFFE IDs at warning level.
15
+
16
+ ### Changed
17
+ - Accept extra keyword context on touched startup, credential, and SVID APIs and fully qualify sibling Crypt constants. Preserve mode-specific credential selection and certificate SAN/TTL behavior; fixed role mappings remain frozen constants.
18
+
19
+ ## [1.5.40] - 2026-11-07
20
+
21
+ ### Fixed
22
+ - Use direct core settings reads for Vault connectivity, lease startup, JWT operations, and mTLS PKI paths; remove settings-only JWT/PKI accessors and preserve complete, isolated defaults in their specs.
23
+ - Exercise the real `legion-settings` dependency throughout specs, including resolver and loader rebuild behavior; isolate mutable settings between examples instead of mocking the dependency.
24
+
25
+ ## [1.5.39] - 2026-11-01
26
+
27
+ ### Fixed
28
+ - Read crypto runtime configuration from live `Legion::Settings` instead of fallback settings wrappers; register Crypt defaults at boot and require Settings >= 1.4.2 without changing lease-reference publication.
29
+ - Honor live JWT verification flags, key-derivation configuration, Vault operation budgets, and mTLS/SPIFFE policy; reject invalid renewal windows rather than silently substituting defaults. Vault environment tokens supply boot defaults only, never overwrite explicit runtime configuration.
30
+
3
31
  ## [1.5.38] - 2026-10-31
4
32
 
5
33
  ### Fixed
data/CLAUDE.md CHANGED
@@ -1,216 +1,88 @@
1
- # legion-crypt: Encryption and Vault Integration for LegionIO
1
+ # legion-crypt
2
2
 
3
- **Repository Level 3 Documentation**
4
- - **Parent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
5
-
6
- ## Purpose
7
-
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
-
10
- **GitHub**: https://github.com/LegionIO/legion-crypt
11
- **Version**: 1.5.7
12
- **License**: Apache-2.0
3
+ Core encryption, secrets management, JWT, and HashiCorp Vault integration for LegionIO. Follow [LegionIO coding standards](../docs/legionio-standards.md); this file adds Crypt-specific guidance.
13
4
 
14
5
  ## Architecture
15
6
 
16
7
  ```
17
- Legion::Crypt (singleton module)
18
- ├── .start # Initialize: generate keys, connect to Vault
19
- ├── .encrypt(string) # AES-256-CBC encryption
20
- ├── .decrypt(message) # AES-256-CBC decryption
21
- ├── .shutdown # Stop Vault renewer, close sessions
22
- │
23
- ├── Cipher # OpenSSL cipher operations (AES-256-CBC)
24
- │ ├── .encrypt # Encrypt with cluster secret
25
- │ ├── .decrypt # Decrypt with cluster secret
26
- │ ├── .private_key # RSA private key (generated or loaded)
27
- │ └── .public_key # RSA public key
28
- │
29
- ├── Vault # HashiCorp Vault integration
30
- │ ├── .connect_vault # Establish Vault session
31
- │ ├── .read(path) # Read secret from Vault
32
- │ ├── .write(path) # Write secret to Vault
33
- │ └── .renew_token # Token renewal
34
- │
35
- ├── JWT # JSON Web Token operations
36
- │ ├── .issue # Create signed JWT (HS256 or RS256)
37
- │ ├── .verify # Verify and decode JWT
38
- │ ├── .verify_with_jwks # Verify RS256 token via external JWKS endpoint (Entra ID, etc.)
39
- │ └── .decode # Decode without verification (inspection)
40
- │
41
- ├── JwksClient # External JWKS endpoint integration (thread-safe)
42
- │ ├── .fetch_keys # Fetch and parse JWKS from a URL
43
- │ ├── .find_key # Lookup key by kid (cache-first, re-fetch on miss)
44
- │ └── .clear_cache # Clear the key cache
45
- │
46
- ├── ClusterSecret # Cluster-wide shared secret management
47
- │ └── .cs # Generate/distribute cluster secret
48
- │
49
- ├── VaultJwtAuth # Vault JWT auth backend integration
50
- │ ├── .login # Authenticate to Vault using a JWT token, returns Vault token hash
51
- │ ├── .login! # Authenticate and set ::Vault.token for subsequent operations
52
- │ └── .worker_login # Issue a Legion JWT and authenticate to Vault in one step
53
- │
54
- ├── VaultRenewer # Background Vault token renewal thread
55
- ├── Ed25519 # Ed25519 key generation, signing, verification, Vault storage
56
- ├── PartitionKeys # HKDF per-tenant key derivation, AES-256-GCM encrypt/decrypt
57
- ├── Erasure # Cryptographic erasure via Vault master key deletion
58
- ├── Attestation # Signed identity claims with Ed25519, freshness checking
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
66
- ├── Spiffe # SPIFFE identity support: parse_id, valid_id?, X509Svid, JwtSvid structs; reads security.spiffe settings
67
- ├── Spiffe::IdentityHelpers # Mixin for SPIFFE identity operations
68
- ├── Spiffe::SvidRotation # Background SVID renewal at 50% TTL
69
- ├── Spiffe::WorkloadApiClient # gRPC workload API client for SPIRE agent (unix socket)
70
- ├── VaultEntity # Vault entity/alias lifecycle (ensure_entity, ensure_alias, find_by_name); all non-fatal
71
- ├── MockVault # In-memory Vault mock for local development mode
72
- ├── Settings # Default crypt config
73
- └── Version
8
+ Legion::Crypt
9
+ ├── Cipher # AES-256-GCM encryption; legacy CBC decryption; RSA key pair
10
+ ├── Vault # read/write/connect/session renewal through Vault::Client
11
+ │ ├── Client # core-owned HTTP client using Faraday and Legion::JSON
12
+ │ ├── Cluster # client selection, caching, authentication grant publication
13
+ │ ├── Operation # bounded operations and shared shutdown deadlines
14
+ │ ├── Entity # entity and alias lookup
15
+ │ ├── JwtAuth # .login / .login! / .worker_login
16
+ │ ├── KerberosAuth # SPNEGO as Authorization header; preserves namespace
17
+ │ └── Renewer # legacy Every actor for session renewal
18
+ ├── JWT # issue/verify/decode (HS256 cluster-secret or RS256 RSA)
19
+ ├── JwksClient # external JWKS endpoint, 1hr TTL cache, thread-safe
20
+ ├── ClusterSecret # shared AES key distributed across nodes
21
+ ├── LeaseManager # dynamic Vault lease lifecycle
22
+ ├── KerberosAuth # SPNEGO acquisition through the optional lex-kerberos helper
23
+ ├── LdapAuth # cluster-scoped LDAP login and token maintenance
24
+ ├── TokenRenewer # 75% TTL renewal; Kerberos-only re-auth; retry backoff
25
+ ├── Ed25519 # key gen, sign, verify, Vault storage
26
+ ├── PartitionKeys # HKDF per-tenant key derivation, AES-256-GCM
27
+ ├── TLS # TLS configuration and Vault-backed material resolution
28
+ ├── Mtls # Vault PKI certificate issuance using crypt.mtls settings
29
+ ├── CertRotation # background renewal using the configured renewal window
30
+ ├── Spiffe # SPIFFE ID parse/validate, X509Svid/JwtSvid, SPIRE workload API
31
+ ├── Attestation # signed identity claims with Ed25519
32
+ ├── Erasure # cryptographic erasure via Vault master key deletion
33
+ └── MockVault # in-memory mock for local dev
74
34
  ```
75
35
 
76
- ### Key Design Patterns
77
-
78
- - **Dynamic Keys**: By default, generates new RSA key pair per process start (no persistent keys)
79
- - **Cluster Secret**: Shared AES key distributed across Legion nodes for inter-node encrypted communication
80
- - **Vault Conditional**: Vault module is only included if the `vault` gem is available
81
- - **Token Lifecycle**: VaultRenewer runs background thread for automatic token renewal
82
- - **JWT Dual Algorithm**: HS256 (symmetric, cluster secret) for intra-cluster tokens; RS256 (asymmetric, RSA keypair) for tokens verifiable without sharing the signing key
83
- - **JWKS External Validation**: `JwksClient` fetches public keys from external identity provider JWKS endpoints (Entra ID, Bot Framework). Keys cached for 1 hour (CACHE_TTL=3600s), thread-safe via Mutex, automatic re-fetch on cache miss handles key rotation
84
-
85
- ## Default Settings
86
-
87
- ```json
88
- {
89
- "vault": { "..." : "see vault settings" },
90
- "jwt": {
91
- "enabled": true,
92
- "default_algorithm": "HS256",
93
- "default_ttl": 3600,
94
- "issuer": "legion",
95
- "verify_expiration": true,
96
- "verify_issuer": true
97
- },
98
- "cs_encrypt_ready": false,
99
- "dynamic_keys": true,
100
- "cluster_secret": null,
101
- "save_private_key": true,
102
- "read_private_key": true
103
- }
104
- ```
36
+ Vault components live under `lib/legion/crypt/vault/`: `client.rb`, `cluster.rb`, `operation.rb`, `entity.rb`, `jwt_auth.rb`, `kerberos_auth.rb`, and `renewer.rb`. Core `KerberosAuth` and `TokenRenewer` remain directly under `Legion::Crypt`.
105
37
 
106
- ## Dependencies
107
-
108
- | Gem | Purpose |
109
- |-----|---------|
110
- | `ed25519` (~> 1.3) | Ed25519 key operations (pure Ruby) |
111
- | `jwt` (>= 2.7) | JSON Web Token encoding/decoding |
112
- | `vault` (>= 0.17) | HashiCorp Vault Ruby client |
113
-
114
- Dev dependencies: `legion-logging`, `legion-settings`
115
-
116
- ## File Map
117
-
118
- | Path | Purpose |
119
- |------|---------|
120
- | `lib/legion/crypt.rb` | Module entry, start/shutdown lifecycle |
121
- | `lib/legion/crypt/cipher.rb` | AES-256-CBC encrypt/decrypt, RSA key generation |
122
- | `lib/legion/crypt/jwt.rb` | JWT issue/verify/decode/verify_with_jwks operations |
123
- | `lib/legion/crypt/jwks_client.rb` | JWKS endpoint fetch, parse, cache (thread-safe, 1hr TTL) |
124
- | `lib/legion/crypt/vault.rb` | Vault read/write/connect/renew operations |
125
- | `lib/legion/crypt/cluster_secret.rb` | Cluster-wide shared secret management |
126
- | `lib/legion/crypt/vault_jwt_auth.rb` | Vault JWT auth backend: `.login`, `.login!`, `.worker_login`; raises `AuthError` on failure |
127
- | `lib/legion/crypt/vault_renewer.rb` | Background Vault token renewal |
128
- | `lib/legion/crypt/lease_manager.rb` | Dynamic Vault lease lifecycle management |
129
- | `lib/legion/crypt/settings.rb` | Default configuration |
130
- | `lib/legion/crypt/ed25519.rb` | Ed25519 key generation, signing, verification, Vault storage |
131
- | `lib/legion/crypt/partition_keys.rb` | HKDF per-tenant key derivation with AES-256-GCM |
132
- | `lib/legion/crypt/erasure.rb` | Cryptographic erasure via Vault master key deletion |
133
- | `lib/legion/crypt/attestation.rb` | Signed identity claims with Ed25519 signatures |
134
- | `lib/legion/crypt/kerberos_auth.rb` | GSSAPI/Kerberos token acquisition; `obtain_spnego_token`, `disable_gssapi_finalizers` (prevents GC segfault on macOS) |
135
- | `lib/legion/crypt/vault_kerberos_auth.rb` | Vault Kerberos auth backend: sends SPNEGO token as `Authorization` HTTP header, clears/restores namespace, wires `TokenRenewer` |
136
- | `lib/legion/crypt/ldap_auth.rb` | Vault LDAP auth backend integration |
137
- | `lib/legion/crypt/tls.rb` | TLS settings module (cert, key, CA paths, verify_peer, Vault PKI flag) |
138
- | `lib/legion/crypt/mtls.rb` | mTLS certificate issuance from Vault PKI; `CertRotation` background renewal thread (50% TTL) |
139
- | `lib/legion/crypt/token_renewer.rb` | Plain Thread renewer: renews at 75% TTL, re-auths via Kerberos on failure, exponential backoff |
140
- | `lib/legion/crypt/spiffe.rb` | SPIFFE identity: parse/validate SPIFFE IDs, X509Svid/JwtSvid structs, settings helpers |
141
- | `lib/legion/crypt/spiffe/identity_helpers.rb` | Mixin for SPIFFE identity operations |
142
- | `lib/legion/crypt/spiffe/svid_rotation.rb` | Background SVID renewal thread (50% TTL) |
143
- | `lib/legion/crypt/spiffe/workload_api_client.rb` | gRPC workload API client for SPIRE agent |
144
- | `lib/legion/crypt/vault_entity.rb` | Vault entity/alias lifecycle: `ensure_entity`, `ensure_alias`, `find_by_name`; all operations non-fatal |
145
- | `lib/legion/crypt/version.rb` | VERSION constant |
146
-
147
- ## Role in LegionIO
148
-
149
- First service-level module initialized during `Legion::Service` startup (before transport). Provides:
150
- 1. Vault token for `legion-transport` to fetch RabbitMQ credentials
151
- 2. Message encryption for `legion-transport` (optional `transport.messages.encrypt`)
152
- 3. Cluster secret for inter-node encrypted communication
153
- 4. JWT tokens for node authentication and task authorization
154
- 5. External token verification for identity providers (Entra ID OIDC via JWKS)
155
-
156
- ### Vault JWT Auth Usage
38
+ ## Key Design Decisions
157
39
 
158
- ```ruby
159
- # Authenticate to Vault using a JWT (Vault must have JWT auth method enabled)
160
- result = Legion::Crypt::VaultJwtAuth.login(jwt: token, role: 'legion-worker')
161
- # => { token: '...', lease_duration: 3600, renewable: true, policies: [...], metadata: {} }
40
+ - RSA keys are memoized per process. `read_private_key` loads an existing `./legionio.key`; otherwise a key is generated. `save_private_key` persists it with owner-only permissions.
41
+ - HS256 (cluster secret) for intra-cluster tokens; RS256 (RSA keypair) for externally-verifiable tokens
42
+ - `JwksClient` caches keys for one hour, shares concurrent refreshes, and throttles repeated unknown-key refreshes.
43
+ - Vault connectivity uses the core client; the `vault` gem is not required. Kerberos SPNEGO acquisition requires the optional `lex-kerberos` helper, and authentication preserves the configured Vault namespace.
44
+ - JWT/LDAP token renewal never falls back to Kerberos. Expiry or terminal rejection requires login again; Kerberos grants alone can automatically reauthenticate.
162
45
 
163
- # Authenticate and set Vault client token in one step
164
- Legion::Crypt::VaultJwtAuth.login!(jwt: token)
46
+ ## Framework Contracts
165
47
 
166
- # Issue a Legion JWT and use it to authenticate to Vault (convenience for workers)
167
- result = Legion::Crypt::VaultJwtAuth.worker_login(worker_id: 'abc', owner_msid: 'user@example.com')
168
- ```
48
+ - Defaults live in `lib/legion/crypt/settings.rb` and are registered when `legion/crypt` is required. Core code reads `Legion::Settings[:crypt][...]` directly: no settings-only accessor wrappers, duplicated defaults, or nil guards around registered keys.
49
+ - mTLS uses `crypt.mtls`; SPIFFE still reads the optional Core-owned `security.spiffe` section. Do not treat the two settings paths as interchangeable.
50
+ - Include `Legion::Logging::Helper` explicitly in core modules/classes that need logging. Rescues must re-raise or call `handle_exception` at `:warn` or higher. Programming errors must propagate; narrow recovery to known operational failures.
51
+ - Use fully qualified Legion constants across module boundaries. New public APIs use kwargs and trailing `**`; preserve existing positional contracts unless all callers are migrated.
52
+ - Use `Legion::JSON.load` / `dump`; loaded JSON has symbol keys. Never log tokens, passwords, private keys, or decrypted plaintext.
53
+ - `start(mode: nil, **)` selects `postgresql/creds/infra` for an existing `postgresql` lease in infra mode without mutating configured leases. Fixed mode-to-role mappings remain frozen constants.
54
+ - `Legion::Crypt::Mtls.issue_cert(common_name:, alt_names: '', ttl: nil, **)` preserves explicit SAN/TTL inputs; absent TTL uses the registered default. Interface-discovery programming errors must not become loopback fallback.
55
+ - Preserve bounded shutdown, lease ownership, and generation checks: late worker results must not reinstall credentials after shutdown.
169
56
 
170
- Vault prerequisites: `vault auth enable jwt` + configure `auth/jwt/config` with JWKS URL or bound issuer.
57
+ ## Specs and Verification
171
58
 
172
- ### JWT Usage
59
+ - Mirror literal library paths and filenames in specs: `lib/legion/crypt/vault/client.rb` maps to `spec/legion/crypt/vault/client.rb`, not an `_spec.rb` file. Keep examples with the implementation they exercise; root Crypt API groups share `spec/legion/crypt.rb`.
60
+ - `.rspec` discovers `spec/legion/**/*.rb` and explicitly loads `spec/spec_helper.rb`. Preserve `lib/` precedence over `spec/` on `$LOAD_PATH` to prevent mirrored files from shadowing production requires. Keep support files outside the discovered tree.
61
+ - Require and use gemspec dependencies; do not mock them. In particular, never stub `Legion::Settings`, its loader, resolver, or returned settings hashes.
62
+ - `spec/spec_helper.rb` loads the real Settings/Resolver, isolates the settings root, installs full Crypt defaults for each example, and restores the original state afterward.
63
+ - Configure values through real bracket access or `Legion::Settings.set_prop`. Replacement Crypt configurations must retain full defaults, including nested Vault settings. Raw loader-root mutation is permitted in test setup for publication/rebuild scenarios, not as a new production access pattern.
64
+ - Prefer real libraries and local service fixtures or supported test adapters at external I/O boundaries. Cover behavior, failure propagation, and state transitions rather than forwarding calls or implementation details.
173
65
 
174
- ```ruby
175
- # Convenience methods (auto-selects keys from settings)
176
- token = Legion::Crypt.issue_token({ node_id: 'abc' }, ttl: 3600)
177
- claims = Legion::Crypt.verify_token(token)
66
+ Run from this checkout:
178
67
 
179
- # Direct module usage (explicit keys)
180
- token = Legion::Crypt::JWT.issue(payload, signing_key: key, algorithm: 'RS256')
181
- claims = Legion::Crypt::JWT.verify(token, verification_key: pub_key, algorithm: 'RS256')
182
- decoded = Legion::Crypt::JWT.decode(token) # no verification, inspection only
68
+ ```bash
69
+ bundle exec rubocop -A
70
+ bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
183
71
  ```
184
72
 
185
- **Algorithms:**
186
- - `HS256` (default): Uses cluster secret. All cluster nodes can issue and verify.
187
- - `RS256`: Uses RSA keypair. Only the issuing node can sign; anyone with the public key can verify.
188
-
189
- ### External Token Verification (JWKS)
73
+ Use the full suite. After lint autocorrections, verify the corrected code; read failure details from the JSON report if specs fail.
190
74
 
191
- Verify tokens from external identity providers using their public JWKS endpoints:
75
+ ## JWT Usage
192
76
 
193
77
  ```ruby
194
- # Convenience method
195
- claims = Legion::Crypt.verify_external_token(
196
- token,
78
+ token = Legion::Crypt.issue_token({ node_id: 'abc' }, ttl: 3600)
79
+ claims = Legion::Crypt.verify_token(token)
80
+
81
+ # External provider (Entra ID)
82
+ claims = Legion::Crypt.verify_external_token(token,
197
83
  jwks_url: 'https://login.microsoftonline.com/TENANT/discovery/v2.0/keys',
198
84
  issuers: ['https://login.microsoftonline.com/TENANT/v2.0'],
199
- audience: 'app-client-id'
200
- )
201
-
202
- # Direct module usage
203
- claims = Legion::Crypt::JWT.verify_with_jwks(token, jwks_url: jwks_url)
85
+ audience: 'app-client-id')
204
86
  ```
205
87
 
206
- **Flow:** decode JWT header (unverified) to extract `kid` -> `JwksClient.find_key` fetches the matching public key from cache or JWKS endpoint -> verify JWT signature with the public key.
207
-
208
- **Options:** `issuers:` (array, multi-issuer support), `audience:` (string), `verify_expiration:` (bool, default true).
209
-
210
- **Error hierarchy:** `ExpiredTokenError`, `InvalidTokenError` (bad signature, wrong issuer, wrong audience), `DecodeError` (malformed token) — all inherit from `Legion::Crypt::JWT::Error`.
211
-
212
- **Used by:** `lex-identity` Entra runner for Digital Worker OIDC token validation.
213
-
214
- ---
215
-
216
- **Maintained By**: Matthew Iverson (@Esity)
88
+ Error hierarchy: `ExpiredTokenError`, `InvalidTokenError`, `DecodeError` — all inherit `Legion::Crypt::JWT::Error`.
data/CODEOWNERS CHANGED
@@ -11,9 +11,9 @@
11
11
 
12
12
  # Vault integration
13
13
  # lib/legion/crypt/vault.rb @Esity @future-security-team
14
- # lib/legion/crypt/vault_jwt_auth.rb @Esity @future-security-team
15
- # lib/legion/crypt/vault_renewer.rb @Esity @future-security-team
16
- # lib/legion/crypt/vault_cluster.rb @Esity @future-security-team
14
+ # lib/legion/crypt/vault/jwt_auth.rb @Esity @future-security-team
15
+ # lib/legion/crypt/vault/renewer.rb @Esity @future-security-team
16
+ # lib/legion/crypt/vault/cluster.rb @Esity @future-security-team
17
17
  # lib/legion/crypt/lease_manager.rb @Esity @future-security-team
18
18
 
19
19
  # JWT and JWKS
@@ -22,7 +22,7 @@
22
22
 
23
23
  # Auth integrations
24
24
  # lib/legion/crypt/ldap_auth.rb @Esity @future-security-team
25
- # lib/legion/crypt/vault_kerberos_auth.rb @Esity @future-security-team
25
+ # lib/legion/crypt/vault/kerberos_auth.rb @Esity @future-security-team
26
26
 
27
27
  # Cluster secret
28
28
  # lib/legion/crypt/cluster_secret.rb @Esity @future-security-team
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
  group :test do
7
+ gem 'legionio', '~> 1.9.52', require: false
7
8
  gem 'rake'
8
9
  gem 'rspec'
9
10
  gem 'rspec_junit_formatter'
@@ -12,4 +13,6 @@ group :test do
12
13
  gem 'simplecov'
13
14
  end
14
15
  gem 'legion-logging'
15
- gem 'legion-settings'
16
+ companion_root = File.directory?(File.expand_path('.ci-dependencies', __dir__)) ? '.ci-dependencies' : '..'
17
+ settings_path = File.expand_path("#{companion_root}/legion-settings", __dir__)
18
+ gem 'legion-settings', **(File.directory?(settings_path) ? { path: settings_path } : {})
data/README.md CHANGED
@@ -26,6 +26,10 @@ Legion::Crypt.decrypt(encrypted[:enciphered_message], encrypted[:iv])
26
26
 
27
27
  Authenticated ciphertext requires a full 16-byte GCM tag. Saved RSA private keys are created with `0600` permissions; existing files are restricted before writing.
28
28
 
29
+ `Legion::Crypt.start(mode: nil, **)` accepts extra keyword context. With `mode: :infra`, an existing PostgreSQL lease uses `postgresql/creds/infra` without mutating its configured path; other modes preserve the configured path. The mode-to-role mapping is a fixed framework convention, not an operator setting.
30
+
31
+ Vault connectivity inspection propagates errors rather than reporting broken configuration as disconnected. Startup orchestration and bootstrap credential handling recover only from their declared operational failures, using `handle_exception` at warning level; programming errors propagate to callers.
32
+
29
33
  ### Cluster secret readiness
30
34
 
31
35
  Discovery checks settings, Vault, and then peers. Random generation requires confirmed empty-cluster membership, the lite transport adapter, or standalone operation (no transport loaded or transport explicitly disabled). Disconnected or unknown cluster membership does not authorize a competing key.
@@ -72,10 +76,16 @@ Public keys are cached for 1 hour. Concurrent misses for one URL share a refresh
72
76
 
73
77
  ## Configuration
74
78
 
79
+ Crypt requires `legion-settings` >= 1.4.2. Requiring `legion/crypt` registers its defaults once; runtime reads use the live `Legion::Settings[:crypt]` tree, including replacement trees and request overlays. The partial configuration example below shows the `crypt` section; `lib/legion/crypt/settings.rb` is the authoritative source for all defaults. Configure it through `Legion::Settings`, not the removed `Legion::Crypt.settings`, `.vault_settings`, or `.jwt_settings` accessors. `VAULT_DEV_ROOT_TOKEN_ID` and `VAULT_TOKEN_ID` provide boot defaults; they do not overwrite an explicit live Vault token.
80
+
81
+ `crypt.mtls` owns the mTLS defaults registered by this gem. Certificate issuance reads `Legion::Settings[:crypt][:mtls][:vault_pki_path]` directly; the `Legion::Crypt::Mtls.pki_path` accessor is removed. `security.spiffe` remains Core-owned optional configuration with its existing absent-field defaults. Explicit false policy is never replaced by a truthy fallback. Renewal windows must be finite numbers from zero (inclusive) to one (exclusive).
82
+
83
+ After an explicit full `Legion::Settings.reset!`, standalone callers and tests must bootstrap again with `Legion::Settings.register_library(:crypt, Legion::Crypt::Settings.default)` before using configured crypto APIs. Runtime reads do not repair missing registration. Lease binding transactions and credential publication continue to mutate their intended raw settings roots, not request-overlay copies.
84
+
75
85
  ```json
76
86
  {
77
87
  "vault": {
78
- "enabled": false,
88
+ "enabled": true,
79
89
  "protocol": "http",
80
90
  "address": "localhost",
81
91
  "port": 8200,
@@ -84,8 +94,8 @@ Public keys are cached for 1 hour. Concurrent misses for one URL share a refresh
84
94
  "renewer_time": 5,
85
95
  "renewer": true,
86
96
  "operation_timeout": 5,
87
- "push_cluster_secret": true,
88
- "read_cluster_secret": true,
97
+ "push_cluster_secret": false,
98
+ "read_cluster_secret": false,
89
99
  "kv_path": "legion",
90
100
  "leases": {}
91
101
  },
@@ -97,6 +107,12 @@ Public keys are cached for 1 hour. Concurrent misses for one URL share a refresh
97
107
  "verify_expiration": true,
98
108
  "verify_issuer": true
99
109
  },
110
+ "mtls": {
111
+ "enabled": false,
112
+ "vault_pki_path": "pki/issue/legion-internal",
113
+ "cert_ttl": "24h",
114
+ "renewal_window": 0.5
115
+ },
100
116
  "cs_encrypt_ready": false,
101
117
  "dynamic_keys": true,
102
118
  "cluster_secret": null,
@@ -114,9 +130,9 @@ Public keys are cached for 1 hour. Concurrent misses for one URL share a refresh
114
130
 
115
131
  ### Vault Integration
116
132
 
117
- When `vault.token` is set (or via `VAULT_TOKEN_ID` env var), Crypt connects to Vault on `start`. Vault requests use the core-owned `Legion::Crypt::VaultClient`, built on Faraday and `Legion::JSON`; neither vault-ruby nor a LEX extension is required. Responses have symbol keys, including with JSON 3.
133
+ When `vault.token` is set (or via `VAULT_TOKEN_ID` env var), Crypt connects to Vault on `start`. Vault requests use the core-owned `Legion::Crypt::Vault::Client`, built on Faraday and `Legion::JSON`; neither vault-ruby nor a LEX extension is required. Responses have symbol keys, including with JSON 3.
118
134
 
119
- `Legion::Crypt.vault_client` returns the cached default client; pass a cluster name for an explicit client. Its logical, KV, and lease endpoints use keyword arguments and ignore extra keywords while still enforcing required arguments. Existing high-level Crypt and LeaseManager call signatures are unchanged:
135
+ `Legion::Crypt.vault_client` returns the cached default client; pass a cluster name for an explicit client. Its logical, KV, and lease endpoints use keyword arguments and ignore extra keywords while still enforcing required arguments:
120
136
 
121
137
  ```ruby
122
138
  client = Legion::Crypt.vault_client
@@ -143,15 +159,17 @@ The `LeaseManager` handles dynamic secrets from any Vault secrets engine (databa
143
159
  "crypt": {
144
160
  "vault": {
145
161
  "leases": {
146
- "rabbitmq": { "path": "rabbitmq/creds/legion-role" },
147
- "bedrock": { "path": "aws/creds/bedrock-role" },
148
- "postgres": { "path": "database/creds/apollo-rw" }
162
+ "rabbitmq": { "path": "rabbitmq/creds/legion-role" },
163
+ "bedrock": { "path": "aws/creds/bedrock-role" },
164
+ "postgresql": { "path": "postgresql/creds/node" }
149
165
  }
150
166
  }
151
167
  }
152
168
  }
153
169
  ```
154
170
 
171
+ `postgresql` is the lease name recognized by `Legion::Crypt.start(mode: :infra)` for role selection. Other lease names retain their configured paths.
172
+
155
173
  Other settings files reference lease data using `lease://name#key`:
156
174
 
157
175
  ```json
@@ -193,7 +211,7 @@ Release Crypt before Settings. Older `register_ref` callers remain supported, bu
193
211
 
194
212
  ## Multi-Cluster Vault
195
213
 
196
- `VaultCluster` supports connecting to multiple Vault clusters simultaneously. Each cluster has its own `Legion::Crypt::VaultClient` instance.
214
+ `Legion::Crypt::Vault::Cluster` supports connecting to multiple Vault clusters simultaneously. Each cluster has its own `Legion::Crypt::Vault::Client` instance.
197
215
 
198
216
  Clients are reused only while their endpoint, namespace, TLS verification, and token match the current configuration. Connection changes replace the cached client without disabling lease or token owners retaining its predecessor. Displaced clients are weakly tracked, so the cache does not keep unused clients alive; shutdown permanently closes all surviving clients after owner cleanup. In-place authentication token refreshes preserve the current client.
199
217
 
@@ -245,8 +263,7 @@ JWT and Kerberos `login!` use the same grant lifecycle: token, TTL, expiry, rene
245
263
 
246
264
  When `crypt.vault.auth_method` is set to `kerberos`, `Crypt.start` performs Kerberos auto-auth to Vault using `KerberosAuth`:
247
265
 
248
- ```ruby
249
- # Settings
266
+ ```json
250
267
  {
251
268
  "crypt": {
252
269
  "vault": {
@@ -262,29 +279,56 @@ When `crypt.vault.auth_method` is set to `kerberos`, `Crypt.start` performs Kerb
262
279
 
263
280
  The SPNEGO token is sent as an HTTP `Authorization` header (not JSON body). Kerberos auth uses the configured namespace, where the auth mount resides; configure the cluster with `"namespace": null` for a root-mounted auth method. The client namespace is not cleared or temporarily changed during authentication. Requires Homebrew MIT Kerberos (`brew install krb5`) on macOS — the system Heimdal library is not compatible.
264
281
 
265
- `TokenRenewer` keeps the Vault token alive: renews at 75% TTL, re-auths via Kerberos if renewal fails, uses exponential backoff.
282
+ `Legion::Crypt::TokenRenewer` renews renewable tokens at 75% TTL and retries transient failures with backoff. Only Kerberos-authenticated grants can reauthenticate through Kerberos. JWT and LDAP grants require another login after expiry or terminal rejection.
266
283
 
267
284
  ## SPIFFE Workload API
268
285
 
269
286
  The local Unix-socket Workload API exchange has a shared 10-second monotonic deadline. Connection, HTTP/2 handshake, request writes, and flushes additionally share a 5-second setup budget; response reads use the remaining operation time. Availability probes are bounded to 5 seconds, and failed exchanges close acquired sockets.
270
287
 
271
- Responses may split the gRPC prefix or body across HTTP/2 DATA frames and socket reads. JWT-SVID audiences use protobuf varint lengths, including audiences of 128 bytes or longer. X.509 fallback remains opt-in; JWT-SVID failures raise `Spiffe::SvidError`.
288
+ Responses may split the gRPC prefix or body across HTTP/2 DATA frames and socket reads. JWT-SVID audiences use protobuf varint lengths, including audiences of 128 bytes or longer. X.509 fallback remains opt-in; JWT-SVID failures raise `Legion::Crypt::Spiffe::SvidError`.
272
289
 
273
290
  ## mTLS
274
291
 
275
- `Crypt::Mtls` issues mTLS certificates from Vault PKI. When `security.mtls.enabled` is true, `Legion::Service` owns a `Crypt::CertRotation` worker. The worker renews at expiry minus the certificate lifetime multiplied by `security.mtls.renewal_window` (default `0.5`), rather than waiting for a fixed polling interval. Initial issuance and renewal failures retry with exponential backoff from 1 to 60 seconds; a failed renewal retains the last installed certificate. Shutdown interrupts scheduled waits, bounds in-flight issuance with the Vault operation timeout, and prevents late results from replacing the certificate.
292
+ `Legion::Crypt::Mtls` issues mTLS certificates from Vault PKI using `crypt.mtls` configuration. `issue_cert(common_name:, alt_names: '', ttl: nil, **)` preserves caller-supplied SANs and TTL overrides while accepting extra keyword context. `Legion::Crypt::CertRotation` renews at expiry minus the certificate lifetime multiplied by `crypt.mtls.renewal_window` (default `0.5`), rather than waiting for a fixed polling interval. Initial issuance and renewal failures retry with exponential backoff from 1 to 60 seconds; a failed renewal retains the last installed certificate. Shutdown interrupts scheduled waits, bounds in-flight issuance with the Vault operation timeout, and prevents late results from replacing the certificate.
276
293
 
277
- `Transport::Connection::Vault` independently acquires and applies tempfile-based Bunny mTLS credentials. Certificate rotation does not itself close transport connections when a certificate expires. mTLS is disabled by default.
294
+ `Legion::Transport::Connection::Vault` independently acquires and applies tempfile-based Bunny mTLS credentials. Certificate rotation does not itself close transport connections when a certificate expires. mTLS is disabled by default.
278
295
 
279
296
  ## Requirements
280
297
 
281
298
  - Ruby >= 3.4
299
+ - `concurrent-ruby` (~> 1.3)
282
300
  - `ed25519` (~> 1.3)
283
301
  - `jwt` gem (>= 2.7)
284
302
  - `faraday` gem (~> 2.0)
285
303
  - `legion-json` gem (>= 1.2.0; JSON 3 supported)
304
+ - `legion-logging` (>= 1.5.0)
305
+ - `legion-settings` (>= 1.4.2)
286
306
  - HashiCorp Vault (optional, for secrets management)
287
- - `gssapi` gem (optional, required for Kerberos auth)
307
+ - `lex-kerberos` (optional, supplies the SPNEGO helper for Kerberos auth)
308
+
309
+ ## Development
310
+
311
+ Use the existing checkout and the full repository checks:
312
+
313
+ ```bash
314
+ bundle install
315
+ bundle exec rubocop -A
316
+ bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
317
+ ```
318
+
319
+ Run specs against the corrected code if RuboCop changes files. On failure, inspect the failing examples in `tmp/rspec_results.json`; do not substitute a narrower suite for the full check.
320
+
321
+ Specs mirror implementation paths and filenames exactly: `lib/legion/crypt/vault/client.rb` is covered by `spec/legion/crypt/vault/client.rb`, without an `_spec` suffix. Keep each spec focused on its corresponding implementation; startup, JWT convenience, and credential-scoping groups belong in `spec/legion/crypt.rb`.
322
+
323
+ `.rspec` explicitly loads `spec/spec_helper.rb` and discovers `spec/legion/**/*.rb`. The helper puts `lib/` ahead of `spec/` on Ruby's load path so production requires cannot resolve to mirrored spec files. Keep support files outside the discovered implementation-spec tree.
324
+
325
+ Require and use real gemspec dependencies rather than mocking them. `spec/spec_helper.rb` requires `legion-settings` and its resolver, isolates the settings root, and installs complete Crypt defaults for each example. The original settings state is restored afterward, including when an example replaces the loader root.
326
+
327
+ The test bundle also declares `legionio` for the legacy `Vault::Renewer` specs. Isolated Ruby subprocesses exercise the real periodic actor, cancellation lifecycle, and Vault HTTP client against a local service fixture without loading optional Core/transport components into unrelated examples. This is a development/test dependency only; Crypt's runtime gemspec does not depend on Core, and no running Vault service is required.
328
+
329
+ Set individual properties through `Legion::Settings[:crypt][...]`. For replacement-tree scenarios, start from `Legion::Crypt::Settings.default`, modify the needed nested properties, then install it with `Legion::Settings.set_prop(:crypt, configuration)`. Do not replace nested defaults with partial hashes, or stub Settings, its loader, resolver, or returned settings hashes. Keep settings changes within the example lifecycle.
330
+
331
+ Use real libraries with local service fixtures or supported test adapters for external I/O. Preserve security-sensitive behavior: test failure propagation and lifecycle transitions, never expose secrets in test output.
288
332
 
289
333
  ## License
290
334
 
data/legion-crypt.gemspec CHANGED
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency 'jwt', '>= 2.7'
32
32
  spec.add_dependency 'legion-json', '>= 1.2.0'
33
33
  spec.add_dependency 'legion-logging', '>= 1.5.0'
34
+ spec.add_dependency 'legion-settings', '>= 1.4.2'
34
35
  end