legion-crypt
Encryption, secrets management, JWT token management, and HashiCorp Vault integration for the 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.
Version: 1.5.6
Installation
gem install legion-crypt
Or add to your Gemfile:
gem 'legion-crypt'
Usage
require 'legion/crypt'
Legion::Crypt.start
Legion::Crypt.encrypt('this is my string')
Legion::Crypt.decrypt()
JWT Tokens
# Issue a token (defaults to HS256 using cluster secret)
token = Legion::Crypt.issue_token({ node_id: 'abc' }, ttl: 3600)
# Verify and decode a token
claims = Legion::Crypt.verify_token(token)
# Use RS256 (RSA keypair) instead
token = Legion::Crypt.issue_token({ node_id: 'abc' }, algorithm: 'RS256')
claims = Legion::Crypt.verify_token(token, algorithm: 'RS256')
# Inspect a token without verification
decoded = Legion::Crypt::JWT.decode(token)
External Token Verification (JWKS)
Verify tokens from external identity providers (Entra ID, Bot Framework) using their public JWKS endpoints:
# Verify an Entra ID OIDC token
claims = Legion::Crypt.verify_external_token(
token,
jwks_url: 'https://login.microsoftonline.com/TENANT/discovery/v2.0/keys',
issuers: ['https://login.microsoftonline.com/TENANT/v2.0'],
audience: 'app-client-id'
)
# Or use the JWT module directly
claims = Legion::Crypt::JWT.verify_with_jwks(token, jwks_url: jwks_url)
Public keys are cached for 1 hour and automatically re-fetched on cache miss (handles key rotation).
Configuration
{
"vault": {
"enabled": false,
"protocol": "http",
"address": "localhost",
"port": 8200,
"token": null,
"connected": false,
"renewer_time": 5,
"renewer": true,
"push_cluster_secret": true,
"read_cluster_secret": true,
"kv_path": "legion",
"leases": {}
},
"jwt": {
"enabled": true,
"default_algorithm": "HS256",
"default_ttl": 3600,
"issuer": "legion",
"verify_expiration": true,
"verify_issuer": true
},
"cs_encrypt_ready": false,
"dynamic_keys": true,
"cluster_secret": null,
"save_private_key": true,
"read_private_key": true
}
JWT Algorithms
| Algorithm | Key | Use Case |
|---|---|---|
HS256 (default) |
Cluster secret (symmetric) | Intra-cluster tokens — all nodes can issue and verify |
RS256 |
RSA key pair (asymmetric) | Tokens verifiable by external services without sharing the signing key |
Vault Integration
When vault.token is set (or via VAULT_TOKEN_ID env var), Crypt connects to Vault on start. The background VaultRenewer thread keeps the token alive. Vault is an optional runtime dependency — the Vault module is only included if the vault gem is available.
Dynamic Vault Leases
The LeaseManager handles dynamic secrets from any Vault secrets engine (database, RabbitMQ, AWS, PKI, etc.). Define named leases in crypt settings — each lease maps a stable name to a Vault path:
{
"crypt": {
"vault": {
"leases": {
"rabbitmq": { "path": "rabbitmq/creds/legion-role" },
"bedrock": { "path": "aws/creds/bedrock-role" },
"postgres": { "path": "database/creds/apollo-rw" }
}
}
}
}
Other settings files reference lease data using lease://name#key:
{
"transport": {
"connection": {
"username": "lease://rabbitmq#username",
"password": "lease://rabbitmq#password"
}
}
}
Both username and password come from a single Vault read — one lease, one credential pair. The LeaseManager:
- Fetches all leases at boot (during
Crypt.start, beforeresolve_secrets!) - Caches response data and lease metadata
- Renews leases in the background at 50% TTL
- Detects credential rotation and pushes new values into
Legion::Settingsin-place - Revokes all leases on
Crypt.shutdown
Lease names are stable across environments. The actual Vault paths are deployment-specific config.
Multi-Cluster Vault
VaultCluster supports connecting to multiple Vault clusters simultaneously. Each cluster has its own ::Vault::Client instance.
{
"crypt": {
"vault": {
"default": "primary",
"clusters": {
"primary": {
"protocol": "https",
"address": "vault.example.com",
"port": 8200,
"namespace": "my-namespace",
"auth_method": "ldap"
},
"secondary": {
"protocol": "https",
"address": "vault2.example.com",
"port": 8200,
"auth_method": "ldap"
}
}
}
}
}
# Authenticate to all LDAP-configured clusters at once
Legion::Crypt.ldap_login_all(username: 'user', password: 'pass')
# Read from specific cluster
Legion::Crypt.read('secret/data/mykey', cluster: :secondary)
# Get a Vault client for a specific cluster
client = Legion::Crypt.vault_client(:primary)
When clusters is empty, the legacy single-cluster path is used (backward compatible).
Kerberos Authentication
When crypt.vault.auth_method is set to kerberos, Crypt.start performs Kerberos auto-auth to Vault using KerberosAuth:
# Settings
{
"crypt": {
"vault": {
"auth_method": "kerberos",
"kerberos": {
"service_principal": "HTTP/vault.example.com@REALM",
"auth_path": "auth/kerberos/login"
}
}
}
}
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.
TokenRenewer keeps the Vault token alive: renews at 75% TTL, re-auths via Kerberos if renewal fails, uses exponential backoff.
mTLS
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.
Requirements
- Ruby >= 3.4
ed25519(~> 1.3)jwtgem (>= 2.7)vaultgem (>= 0.17, optional)- HashiCorp Vault (optional, for secrets management)
gssapigem (optional, required for Kerberos auth)
License
Apache-2.0