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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +30 -3
- data/.rspec +2 -0
- data/AGENTS.md +2 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +64 -192
- data/CODEOWNERS +4 -4
- data/Gemfile +4 -1
- data/README.md +60 -16
- data/legion-crypt.gemspec +1 -0
- data/lib/legion/crypt/cert_rotation.rb +7 -21
- data/lib/legion/crypt/cluster_secret.rb +3 -14
- data/lib/legion/crypt/ed25519.rb +2 -10
- data/lib/legion/crypt/erasure.rb +3 -13
- data/lib/legion/crypt/jwks_client.rb +4 -4
- data/lib/legion/crypt/kerberos_auth.rb +2 -2
- data/lib/legion/crypt/ldap_auth.rb +2 -2
- data/lib/legion/crypt/lease_manager.rb +7 -7
- data/lib/legion/crypt/mtls.rb +8 -46
- data/lib/legion/crypt/partition_keys.rb +2 -6
- data/lib/legion/crypt/settings.rb +24 -21
- data/lib/legion/crypt/spiffe/svid_rotation.rb +6 -9
- data/lib/legion/crypt/spiffe.rb +14 -39
- data/lib/legion/crypt/token_renewer.rb +6 -6
- data/lib/legion/crypt/vault/client.rb +355 -0
- data/lib/legion/crypt/vault/cluster.rb +330 -0
- data/lib/legion/crypt/vault/entity.rb +80 -0
- data/lib/legion/crypt/vault/jwt_auth.rb +99 -0
- data/lib/legion/crypt/vault/kerberos_auth.rb +53 -0
- data/lib/legion/crypt/vault/operation.rb +29 -0
- data/lib/legion/crypt/vault.rb +6 -11
- data/lib/legion/crypt/version.rb +1 -1
- data/lib/legion/crypt.rb +69 -80
- metadata +23 -8
- data/lib/legion/crypt/vault_client.rb +0 -355
- data/lib/legion/crypt/vault_cluster.rb +0 -331
- data/lib/legion/crypt/vault_entity.rb +0 -78
- data/lib/legion/crypt/vault_jwt_auth.rb +0 -97
- data/lib/legion/crypt/vault_kerberos_auth.rb +0 -51
- data/lib/legion/crypt/vault_operation.rb +0 -27
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfa9ecbf9d8e83066a57605aa0535a3b529a0a43cfaf3bca6efef509a9d2754a
|
|
4
|
+
data.tar.gz: 8e9107d222b6c35bc269c9d8d52f3c356a64e42b76b8108f9139286e42daa1c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a94e249cae525b2298465a0f9b2db67c16c75d517cd9a011675a63c63d532d3d128e671762847e32fc71fa18b9466883e7cfac0b12056260a6b22395e4b15e6
|
|
7
|
+
data.tar.gz: 511104bdfec4843ece4157715128f3d38a450194a401086f0626a8670cb0c27a62ca764e892eb4e6fd7cf28ada63c2c19c83e56000daf6d800113b20356a8085
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
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
|
|
1
|
+
# legion-crypt
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
18
|
-
├──
|
|
19
|
-
├──
|
|
20
|
-
├──
|
|
21
|
-
├──
|
|
22
|
-
│
|
|
23
|
-
├──
|
|
24
|
-
│ ├──
|
|
25
|
-
│ ├──
|
|
26
|
-
│
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
├──
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
├──
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
├──
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
Legion::Crypt::VaultJwtAuth.login!(jwt: token)
|
|
46
|
+
## Framework Contracts
|
|
165
47
|
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
57
|
+
## Specs and Verification
|
|
171
58
|
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
## JWT Usage
|
|
192
76
|
|
|
193
77
|
```ruby
|
|
194
|
-
|
|
195
|
-
claims = Legion::Crypt.
|
|
196
|
-
|
|
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
|
-
|
|
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/
|
|
15
|
-
# lib/legion/crypt/
|
|
16
|
-
# lib/legion/crypt/
|
|
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/
|
|
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
|
-
|
|
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":
|
|
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":
|
|
88
|
-
"read_cluster_secret":
|
|
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::
|
|
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
|
|
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":
|
|
147
|
-
"bedrock":
|
|
148
|
-
"
|
|
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
|
-
`
|
|
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
|
-
```
|
|
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`
|
|
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
|
|
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
|
-
- `
|
|
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
|
|