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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault/client'
|
|
5
|
+
require 'legion/crypt/lease_manager'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Crypt
|
|
9
|
+
module Vault
|
|
10
|
+
module Entity
|
|
11
|
+
extend Legion::Logging::Helper
|
|
12
|
+
|
|
13
|
+
# Create or lookup a Vault entity for a Legion principal.
|
|
14
|
+
# Returns the Vault entity ID string, or nil on failure.
|
|
15
|
+
def self.ensure_entity(principal_id:, canonical_name:, metadata: {})
|
|
16
|
+
existing = find_by_name(canonical_name: canonical_name)
|
|
17
|
+
return existing if existing
|
|
18
|
+
|
|
19
|
+
response = Legion::Crypt::LeaseManager.instance.vault_logical.write(
|
|
20
|
+
path: 'identity/entity',
|
|
21
|
+
data: {
|
|
22
|
+
name: "legion-#{canonical_name}",
|
|
23
|
+
metadata: metadata.merge(
|
|
24
|
+
legion_principal_id: principal_id,
|
|
25
|
+
legion_canonical_name: canonical_name,
|
|
26
|
+
managed_by: 'legion'
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
extract_id(response)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_entity', canonical_name: canonical_name)
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Create an alias linking an auth method mount to the entity.
|
|
37
|
+
# Idempotent — swallows "already exists" 4xx errors.
|
|
38
|
+
def self.ensure_alias(entity_id:, mount_accessor:, alias_name:)
|
|
39
|
+
Legion::Crypt::LeaseManager.instance.vault_logical.write(
|
|
40
|
+
path: 'identity/entity-alias',
|
|
41
|
+
data: { name: alias_name, canonical_id: entity_id, mount_accessor: mount_accessor }
|
|
42
|
+
)
|
|
43
|
+
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
44
|
+
if e.message.include?('already exists')
|
|
45
|
+
log.debug 'Vault entity alias already exists (idempotent)'
|
|
46
|
+
else
|
|
47
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_alias', alias_name: alias_name)
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.ensure_alias', alias_name: alias_name)
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Look up a Vault entity by its Legion canonical name.
|
|
56
|
+
# Returns the Vault entity ID string, or nil if not found.
|
|
57
|
+
def self.find_by_name(canonical_name:)
|
|
58
|
+
response = Legion::Crypt::LeaseManager.instance.vault_logical.read(path: "identity/entity/name/legion-#{canonical_name}")
|
|
59
|
+
extract_id(response)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_entity.find_by_name', canonical_name: canonical_name)
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Private helpers
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
# Extract the entity ID from the client's symbol-keyed response data.
|
|
70
|
+
def self.extract_id(response)
|
|
71
|
+
data = response&.data
|
|
72
|
+
return nil unless data
|
|
73
|
+
|
|
74
|
+
data[:id]
|
|
75
|
+
end
|
|
76
|
+
private_class_method :extract_id
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault/client'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Crypt
|
|
8
|
+
# Vault JWT auth backend integration.
|
|
9
|
+
#
|
|
10
|
+
# Allows Legion workers to authenticate to Vault using JWT tokens
|
|
11
|
+
# via Vault's JWT/OIDC auth method. The worker presents a signed JWT
|
|
12
|
+
# and receives a Vault token with policies scoped to the worker's role.
|
|
13
|
+
#
|
|
14
|
+
# Vault config prerequisites:
|
|
15
|
+
# vault auth enable jwt
|
|
16
|
+
# vault write auth/jwt/config jwks_url="..." (or bound_issuer + jwt_validation_pubkeys)
|
|
17
|
+
# vault write auth/jwt/role/legion-worker bound_audiences="legion" ...
|
|
18
|
+
module Vault
|
|
19
|
+
module JwtAuth
|
|
20
|
+
DEFAULT_AUTH_PATH = 'auth/jwt/login'
|
|
21
|
+
DEFAULT_ROLE = 'legion-worker'
|
|
22
|
+
|
|
23
|
+
class AuthError < StandardError; end
|
|
24
|
+
|
|
25
|
+
extend Legion::Logging::Helper
|
|
26
|
+
|
|
27
|
+
# Authenticate to Vault using a JWT token.
|
|
28
|
+
# Returns a Vault token string on success.
|
|
29
|
+
#
|
|
30
|
+
# @param jwt [String] Signed JWT token (issued by Legion or Entra ID)
|
|
31
|
+
# @param role [String] Vault JWT auth role name (default: 'legion-worker')
|
|
32
|
+
# @param auth_path [String] Vault auth mount path (default: 'auth/jwt/login')
|
|
33
|
+
# @return [Hash] { token:, lease_duration:, policies:, metadata: }
|
|
34
|
+
def self.login(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
|
|
35
|
+
raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
|
|
36
|
+
|
|
37
|
+
log.info "[crypt:vault_jwt] authenticating role=#{role} auth_path=#{auth_path}"
|
|
38
|
+
response = Legion::Crypt.vault_client.logical.write(path: auth_path, data: { role: role, jwt: jwt })
|
|
39
|
+
|
|
40
|
+
raise AuthError, 'Vault JWT auth returned no auth data' unless response&.auth
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
token: response.auth.client_token,
|
|
44
|
+
lease_duration: response.auth.lease_duration,
|
|
45
|
+
renewable: response.auth.renewable?,
|
|
46
|
+
policies: response.auth.policies,
|
|
47
|
+
metadata: response.auth.metadata
|
|
48
|
+
}
|
|
49
|
+
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
50
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
|
|
51
|
+
category: 'client_error')
|
|
52
|
+
raise AuthError, "Vault JWT auth failed: #{e.message}"
|
|
53
|
+
rescue Legion::Crypt::Vault::HTTPServerError => e
|
|
54
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path,
|
|
55
|
+
category: 'server_error')
|
|
56
|
+
raise AuthError, "Vault server error during JWT auth: #{e.message}"
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
handle_exception(e, level: :error, operation: 'crypt.vault_jwt_auth.login', role: role, auth_path: auth_path)
|
|
59
|
+
raise if e.is_a?(AuthError)
|
|
60
|
+
|
|
61
|
+
raise AuthError, "Vault JWT auth failed: #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Authenticate and set the Vault client token for subsequent operations.
|
|
65
|
+
# This replaces the current Vault token with the JWT-authenticated one.
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash] Same as login
|
|
68
|
+
def self.login!(jwt:, role: DEFAULT_ROLE, auth_path: DEFAULT_AUTH_PATH)
|
|
69
|
+
generation = Legion::Crypt.vault_auth_generation
|
|
70
|
+
client = Legion::Crypt.vault_client
|
|
71
|
+
config = Legion::Crypt.cluster || Legion::Settings[:crypt][:vault]
|
|
72
|
+
result = login(jwt: jwt, role: role, auth_path: auth_path)
|
|
73
|
+
Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'jwt', generation: generation)
|
|
74
|
+
log.info "[crypt:vault_jwt] authenticated via JWT auth, policies=#{result[:policies].join(',')}"
|
|
75
|
+
result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Issue a Legion JWT and use it to authenticate to Vault in one step.
|
|
79
|
+
# Convenience method for workers that need Vault access.
|
|
80
|
+
#
|
|
81
|
+
# @param worker_id [String] Digital worker ID
|
|
82
|
+
# @param owner_msid [String] Worker's owner MSID
|
|
83
|
+
# @param role [String] Vault JWT auth role name
|
|
84
|
+
# @return [Hash] Same as login
|
|
85
|
+
def self.worker_login(worker_id:, owner_msid:, role: DEFAULT_ROLE)
|
|
86
|
+
log.info "[crypt:vault_jwt] worker login requested role=#{role} worker_id=#{worker_id}"
|
|
87
|
+
jwt = Legion::Crypt::JWT.issue(
|
|
88
|
+
{ worker_id: worker_id, sub: owner_msid, scope: 'vault', aud: 'legion' },
|
|
89
|
+
signing_key: Legion::Crypt.cluster_secret,
|
|
90
|
+
ttl: 300,
|
|
91
|
+
issuer: 'legion'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
login(jwt: jwt, role: role)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
require 'legion/crypt/vault/client'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Crypt
|
|
8
|
+
module Vault
|
|
9
|
+
module KerberosAuth
|
|
10
|
+
DEFAULT_AUTH_PATH = 'auth/kerberos/login'
|
|
11
|
+
|
|
12
|
+
class AuthError < StandardError; end
|
|
13
|
+
|
|
14
|
+
extend Legion::Logging::Helper
|
|
15
|
+
|
|
16
|
+
def self.login(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
|
|
17
|
+
raise AuthError, 'Vault is not connected' unless Legion::Crypt.vault_connected?
|
|
18
|
+
|
|
19
|
+
log.info "[crypt:vault_kerberos] login requested auth_path=#{auth_path}"
|
|
20
|
+
data = Legion::Crypt.vault_client.put(
|
|
21
|
+
path: auth_path, body: '{}', headers: { 'Authorization' => "Negotiate #{spnego_token}" }
|
|
22
|
+
)
|
|
23
|
+
response = Legion::Crypt::Vault::Client::Secret.decode(data: data)
|
|
24
|
+
raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
token: response.auth.client_token,
|
|
28
|
+
lease_duration: response.auth.lease_duration,
|
|
29
|
+
renewable: response.auth.renewable?,
|
|
30
|
+
policies: response.auth.policies,
|
|
31
|
+
metadata: response.auth.metadata
|
|
32
|
+
}
|
|
33
|
+
rescue Legion::Crypt::Vault::HTTPClientError => e
|
|
34
|
+
handle_exception(e, level: :warn, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
|
|
35
|
+
raise AuthError, "Vault Kerberos auth failed: #{e.message}"
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
handle_exception(e, level: :error, operation: 'crypt.vault_kerberos_auth.login', auth_path: auth_path)
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.login!(spnego_token:, auth_path: DEFAULT_AUTH_PATH)
|
|
42
|
+
generation = Legion::Crypt.vault_auth_generation
|
|
43
|
+
client = Legion::Crypt.vault_client
|
|
44
|
+
config = Legion::Crypt.cluster || Legion::Settings[:crypt][:vault]
|
|
45
|
+
result = login(spnego_token: spnego_token, auth_path: auth_path)
|
|
46
|
+
Legion::Crypt.install_vault_grant(client: client, config: config, grant: result, auth_method: 'kerberos', generation: generation)
|
|
47
|
+
log.info "[crypt:vault_kerberos] authenticated via Kerberos auth, policies=#{result[:policies].join(',')}"
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'legion/crypt/settings'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Crypt
|
|
8
|
+
module Vault
|
|
9
|
+
module Operation
|
|
10
|
+
DEFAULT_TIMEOUT = 5
|
|
11
|
+
SHUTDOWN_TIMEOUT = 10
|
|
12
|
+
|
|
13
|
+
def self.with_timeout(deadline: nil, **)
|
|
14
|
+
timeout = Legion::Settings[:crypt][:vault][:operation_timeout]
|
|
15
|
+
raise ArgumentError, 'Vault operation_timeout must be a finite positive number' unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
16
|
+
|
|
17
|
+
if deadline
|
|
18
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
19
|
+
raise Timeout::Error, 'Vault operation deadline expired' unless remaining.positive?
|
|
20
|
+
|
|
21
|
+
timeout = [timeout, remaining].min
|
|
22
|
+
end
|
|
23
|
+
operation_deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
|
|
24
|
+
Timeout.timeout(timeout) { yield(operation_deadline) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/legion/crypt/vault.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/logging/helper'
|
|
4
|
-
require 'legion/crypt/
|
|
5
|
-
require 'legion/crypt/
|
|
4
|
+
require 'legion/crypt/vault/operation'
|
|
5
|
+
require 'legion/crypt/vault/client'
|
|
6
6
|
|
|
7
7
|
module Legion
|
|
8
8
|
module Crypt
|
|
@@ -11,15 +11,10 @@ module Legion
|
|
|
11
11
|
|
|
12
12
|
attr_accessor :sessions
|
|
13
13
|
|
|
14
|
-
def settings
|
|
15
|
-
Legion::Settings[:crypt][:vault]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
14
|
def connect_vault
|
|
19
15
|
return unless start_session_tracking
|
|
20
16
|
|
|
21
17
|
configuration = Legion::Settings[:crypt][:vault]
|
|
22
|
-
configuration[:token] = ENV['VAULT_DEV_ROOT_TOKEN_ID'] if ENV.key? 'VAULT_DEV_ROOT_TOKEN_ID'
|
|
23
18
|
return nil if configuration[:token].nil?
|
|
24
19
|
|
|
25
20
|
client = vault_client
|
|
@@ -119,7 +114,7 @@ module Legion
|
|
|
119
114
|
end
|
|
120
115
|
|
|
121
116
|
def close_sessions(deadline: nil)
|
|
122
|
-
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) +
|
|
117
|
+
deadline ||= ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::Vault::Operation::SHUTDOWN_TIMEOUT
|
|
123
118
|
pending = session_mutex.synchronize do
|
|
124
119
|
@sessions_stopped = true
|
|
125
120
|
@session_generation = (@session_generation || 0) + 1
|
|
@@ -132,7 +127,7 @@ module Legion
|
|
|
132
127
|
pending.each do |session|
|
|
133
128
|
break if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) >= deadline
|
|
134
129
|
|
|
135
|
-
|
|
130
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { close_session(session: session) }
|
|
136
131
|
closed << session
|
|
137
132
|
rescue StandardError => e
|
|
138
133
|
handle_exception(e, level: :error, operation: 'crypt.vault.close_session')
|
|
@@ -175,7 +170,7 @@ module Legion
|
|
|
175
170
|
pending.each do |session|
|
|
176
171
|
break unless session_maintenance_active?(generation: generation)
|
|
177
172
|
|
|
178
|
-
|
|
173
|
+
Legion::Crypt::Vault::Operation.with_timeout { renew_session(session: session) }
|
|
179
174
|
renewed << session
|
|
180
175
|
rescue StandardError => e
|
|
181
176
|
handle_exception(e, level: :error, operation: 'crypt.vault.renew_session')
|
|
@@ -192,7 +187,7 @@ module Legion
|
|
|
192
187
|
connected_clusters.each_key do |name|
|
|
193
188
|
break unless session_maintenance_active?(generation: generation)
|
|
194
189
|
|
|
195
|
-
|
|
190
|
+
Legion::Crypt::Vault::Operation.with_timeout { vault_client(name).auth_token.renew_self }
|
|
196
191
|
renewed << name
|
|
197
192
|
log.info "Vault token renewed for cluster #{name}"
|
|
198
193
|
rescue StandardError => e
|
data/lib/legion/crypt/version.rb
CHANGED
data/lib/legion/crypt.rb
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
require 'openssl'
|
|
4
4
|
require 'base64'
|
|
5
5
|
require 'legion/logging/helper'
|
|
6
|
+
require 'legion/settings'
|
|
7
|
+
|
|
6
8
|
require 'legion/crypt/version'
|
|
7
9
|
require 'legion/crypt/settings'
|
|
8
10
|
require 'legion/crypt/cipher'
|
|
9
11
|
require 'legion/crypt/jwt'
|
|
10
|
-
require 'legion/crypt/
|
|
12
|
+
require 'legion/crypt/vault/jwt_auth'
|
|
11
13
|
require 'legion/crypt/lease_manager'
|
|
12
|
-
require 'legion/crypt/
|
|
13
|
-
require 'legion/crypt/
|
|
14
|
+
require 'legion/crypt/vault/operation'
|
|
15
|
+
require 'legion/crypt/vault/cluster'
|
|
14
16
|
require 'legion/crypt/vault'
|
|
15
17
|
require 'legion/crypt/ldap_auth'
|
|
16
18
|
require 'legion/crypt/token_renewer'
|
|
@@ -18,13 +20,10 @@ require 'legion/crypt/helper'
|
|
|
18
20
|
require 'legion/crypt/mtls'
|
|
19
21
|
require 'legion/crypt/cert_rotation'
|
|
20
22
|
require 'legion/crypt/spiffe'
|
|
21
|
-
require 'legion/crypt/spiffe/workload_api_client'
|
|
22
|
-
require 'legion/crypt/spiffe/svid_rotation'
|
|
23
|
-
require 'legion/crypt/spiffe/identity_helpers'
|
|
24
23
|
|
|
25
24
|
module Legion
|
|
26
25
|
module Crypt
|
|
27
|
-
extend Legion::Crypt::
|
|
26
|
+
extend Legion::Crypt::Vault::Cluster
|
|
28
27
|
extend Legion::Crypt::LdapAuth
|
|
29
28
|
|
|
30
29
|
RMQ_ROLE_MAP = {
|
|
@@ -33,6 +32,10 @@ module Legion
|
|
|
33
32
|
infra: 'legionio-infra'
|
|
34
33
|
}.freeze
|
|
35
34
|
|
|
35
|
+
POSTGRES_ROLE_MAP = {
|
|
36
|
+
infra: 'infra'
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
36
39
|
class << self
|
|
37
40
|
attr_reader :sessions, :stopping
|
|
38
41
|
|
|
@@ -41,29 +44,25 @@ module Legion
|
|
|
41
44
|
|
|
42
45
|
include Legion::Crypt::Vault
|
|
43
46
|
|
|
44
|
-
def
|
|
45
|
-
Legion::
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def kerberos_principal
|
|
49
|
-
KerberosAuth.kerberos_principal
|
|
47
|
+
def kerberos_principal(**)
|
|
48
|
+
Legion::Crypt::KerberosAuth.kerberos_principal
|
|
50
49
|
end
|
|
51
50
|
|
|
52
51
|
def spiffe_svid
|
|
53
52
|
@svid_rotation&.current_svid
|
|
54
53
|
end
|
|
55
54
|
|
|
56
|
-
def fetch_svid
|
|
57
|
-
@workload_client ||= Spiffe::WorkloadApiClient.new
|
|
55
|
+
def fetch_svid(**)
|
|
56
|
+
@workload_client ||= Legion::Crypt::Spiffe::WorkloadApiClient.new
|
|
58
57
|
@workload_client.fetch_x509_svid
|
|
59
58
|
end
|
|
60
59
|
|
|
61
|
-
def fetch_jwt_svid(audience
|
|
62
|
-
@workload_client ||= Spiffe::WorkloadApiClient.new
|
|
60
|
+
def fetch_jwt_svid(audience:, **)
|
|
61
|
+
@workload_client ||= Legion::Crypt::Spiffe::WorkloadApiClient.new
|
|
63
62
|
@workload_client.fetch_jwt_svid(audience: audience)
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
def start
|
|
65
|
+
def start(mode: nil, **)
|
|
67
66
|
lifecycle_mutex.synchronize do
|
|
68
67
|
if @started || @stopping
|
|
69
68
|
log.info 'Legion::Crypt start ignored because the lifecycle is running or stopping'
|
|
@@ -72,7 +71,7 @@ module Legion
|
|
|
72
71
|
|
|
73
72
|
log.info 'Legion::Crypt startup initiated'
|
|
74
73
|
log.debug 'Legion::Crypt start requested'
|
|
75
|
-
if
|
|
74
|
+
if Legion::Settings[:crypt][:save_private_key]
|
|
76
75
|
key = private_key
|
|
77
76
|
::File.open('./legionio.key', ::File::WRONLY | ::File::CREAT, 0o600) do |file|
|
|
78
77
|
file.chmod(0o600)
|
|
@@ -83,15 +82,15 @@ module Legion
|
|
|
83
82
|
@token_renewers ||= []
|
|
84
83
|
start_session_tracking
|
|
85
84
|
|
|
86
|
-
if
|
|
87
|
-
log.info "Legion::Crypt connecting #{
|
|
85
|
+
if Legion::Settings[:crypt][:vault][:clusters].any?
|
|
86
|
+
log.info "Legion::Crypt connecting #{Legion::Settings[:crypt][:vault][:clusters].size} Vault cluster(s)"
|
|
88
87
|
connect_all_clusters
|
|
89
88
|
else
|
|
90
|
-
log.info 'Legion::Crypt connecting primary Vault client' unless
|
|
91
|
-
connect_vault unless
|
|
89
|
+
log.info 'Legion::Crypt connecting primary Vault client' unless Legion::Settings[:crypt][:vault][:token].nil?
|
|
90
|
+
connect_vault unless Legion::Settings[:crypt][:vault][:token].nil?
|
|
92
91
|
end
|
|
93
92
|
start_token_renewers
|
|
94
|
-
start_lease_manager
|
|
93
|
+
start_lease_manager(mode: mode)
|
|
95
94
|
start_svid_rotation
|
|
96
95
|
@started = true
|
|
97
96
|
log.info 'Legion::Crypt startup completed'
|
|
@@ -121,31 +120,19 @@ module Legion
|
|
|
121
120
|
end
|
|
122
121
|
end
|
|
123
122
|
|
|
124
|
-
def settings
|
|
125
|
-
if Legion.const_defined?('Settings')
|
|
126
|
-
Legion::Settings[:crypt]
|
|
127
|
-
else
|
|
128
|
-
Legion::Crypt::Settings.default
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def jwt_settings
|
|
133
|
-
settings[:jwt] || Legion::Crypt::Settings.jwt
|
|
134
|
-
end
|
|
135
|
-
|
|
136
123
|
def dynamic_rmq_creds?
|
|
137
|
-
Legion::Settings
|
|
124
|
+
Legion::Settings[:crypt][:vault][:dynamic_rmq_creds] == true
|
|
138
125
|
end
|
|
139
126
|
|
|
140
|
-
def fetch_bootstrap_rmq_creds
|
|
127
|
+
def fetch_bootstrap_rmq_creds(**)
|
|
141
128
|
return unless vault_connected? && dynamic_rmq_creds?
|
|
142
129
|
|
|
143
130
|
Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default) if defined?(Legion::Transport::Settings)
|
|
144
131
|
|
|
145
|
-
response = LeaseManager.instance.vault_logical.read(path: 'rabbitmq/creds/legionio-bootstrap')
|
|
132
|
+
response = Legion::Crypt::LeaseManager.instance.vault_logical.read(path: 'rabbitmq/creds/legionio-bootstrap')
|
|
146
133
|
return unless response&.data
|
|
147
134
|
|
|
148
|
-
bootstrap_lease_ttl = Legion::Settings
|
|
135
|
+
bootstrap_lease_ttl = Legion::Settings[:crypt][:vault][:bootstrap_lease_ttl].to_i
|
|
149
136
|
bootstrap_lease_ttl = 300 if bootstrap_lease_ttl <= 0
|
|
150
137
|
|
|
151
138
|
@bootstrap_lease_id = response.lease_id
|
|
@@ -167,18 +154,17 @@ module Legion
|
|
|
167
154
|
conn[:password] = password
|
|
168
155
|
|
|
169
156
|
log.info "Bootstrap RMQ credentials acquired (lease: #{@bootstrap_lease_id[0..7]}...)"
|
|
170
|
-
rescue
|
|
157
|
+
rescue IOError, SystemCallError, Timeout::Error, Faraday::Error, Legion::Crypt::Vault::HTTPError => e
|
|
171
158
|
handle_exception(e, level: :warn, operation: 'crypt.fetch_bootstrap_rmq_creds')
|
|
172
|
-
log.warn "Bootstrap RMQ credential fetch failed: #{e.message}"
|
|
173
159
|
end
|
|
174
160
|
|
|
175
|
-
def swap_to_identity_creds(mode
|
|
161
|
+
def swap_to_identity_creds(mode:, **)
|
|
176
162
|
return unless vault_connected? && dynamic_rmq_creds?
|
|
177
163
|
return if mode == :lite
|
|
178
164
|
|
|
179
165
|
role = RMQ_ROLE_MAP.fetch(mode, "legionio-#{mode}")
|
|
180
|
-
manager = LeaseManager.instance
|
|
181
|
-
response = Legion::Crypt::
|
|
166
|
+
manager = Legion::Crypt::LeaseManager.instance
|
|
167
|
+
response = Legion::Crypt::Vault::Operation.with_timeout { manager.vault_logical.read(path: "rabbitmq/creds/#{role}") }
|
|
182
168
|
raise "Failed to fetch identity-scoped RMQ creds for role #{role}" unless response
|
|
183
169
|
|
|
184
170
|
settings = Legion::Settings.loader.settings
|
|
@@ -199,56 +185,47 @@ module Legion
|
|
|
199
185
|
)
|
|
200
186
|
raise 'Identity credential handoff failed — bootstrap lease NOT revoked' unless installed
|
|
201
187
|
|
|
202
|
-
Legion::Crypt::
|
|
188
|
+
Legion::Crypt::Vault::Operation.with_timeout { revoke_bootstrap_lease }
|
|
203
189
|
end
|
|
204
190
|
|
|
205
|
-
def revoke_bootstrap_lease
|
|
191
|
+
def revoke_bootstrap_lease(**)
|
|
206
192
|
return unless @bootstrap_lease_id
|
|
207
193
|
|
|
208
|
-
LeaseManager.instance.vault_sys.revoke(lease_id: @bootstrap_lease_id)
|
|
194
|
+
Legion::Crypt::LeaseManager.instance.vault_sys.revoke(lease_id: @bootstrap_lease_id)
|
|
209
195
|
log.info "Bootstrap RMQ lease revoked (#{@bootstrap_lease_id[0..7]}...)"
|
|
210
196
|
@bootstrap_lease_id = nil
|
|
211
197
|
@bootstrap_lease_expires = nil
|
|
212
|
-
rescue
|
|
198
|
+
rescue IOError, SystemCallError, Timeout::Error, Faraday::Error, Legion::Crypt::Vault::HTTPError => e
|
|
213
199
|
handle_exception(e, level: :warn, operation: 'crypt.revoke_bootstrap_lease')
|
|
214
|
-
log.warn "Bootstrap lease revocation failed: #{e.message} — lease will expire naturally"
|
|
215
200
|
@bootstrap_lease_id = nil
|
|
216
201
|
@bootstrap_lease_expires = nil
|
|
217
202
|
end
|
|
218
203
|
|
|
219
|
-
def vault_connected?
|
|
220
|
-
return connected_clusters.any? if
|
|
204
|
+
def vault_connected?(**)
|
|
205
|
+
return connected_clusters.any? if Legion::Settings[:crypt][:vault][:clusters].any?
|
|
221
206
|
|
|
222
|
-
|
|
223
|
-
return true if respond_to?(:connected_clusters) && connected_clusters.any?
|
|
224
|
-
|
|
225
|
-
false
|
|
226
|
-
rescue StandardError => e
|
|
227
|
-
handle_exception(e, level: :debug, operation: 'crypt.vault_connected?')
|
|
228
|
-
false
|
|
207
|
+
Legion::Settings[:crypt][:vault][:connected] == true
|
|
229
208
|
end
|
|
230
209
|
|
|
231
210
|
def issue_token(payload = {}, ttl: nil, algorithm: nil)
|
|
232
|
-
jwt =
|
|
211
|
+
jwt = Legion::Settings[:crypt][:jwt]
|
|
233
212
|
algo = algorithm || jwt[:default_algorithm]
|
|
234
213
|
token_ttl = ttl || jwt[:default_ttl]
|
|
214
|
+
signing_key = algo == 'RS256' ? private_key : Legion::Settings[:crypt][:cluster_secret]
|
|
235
215
|
|
|
236
|
-
signing_key
|
|
237
|
-
|
|
238
|
-
Legion::Crypt::JWT.issue(payload, signing_key: signing_key, algorithm: algo, ttl: token_ttl,
|
|
239
|
-
issuer: jwt[:issuer])
|
|
216
|
+
Legion::Crypt::JWT.issue(payload, signing_key: signing_key, algorithm: algo, ttl: token_ttl, issuer: jwt[:issuer])
|
|
240
217
|
end
|
|
241
218
|
|
|
242
219
|
def verify_token(token, algorithm: nil)
|
|
243
|
-
jwt =
|
|
220
|
+
jwt = Legion::Settings[:crypt][:jwt]
|
|
244
221
|
algo = algorithm || jwt[:default_algorithm]
|
|
245
222
|
|
|
246
|
-
verification_key = algo == 'RS256' ? OpenSSL::PKey::RSA.new(public_key) :
|
|
223
|
+
verification_key = algo == 'RS256' ? OpenSSL::PKey::RSA.new(public_key) : Legion::Settings[:crypt][:cluster_secret]
|
|
247
224
|
|
|
248
225
|
Legion::Crypt::JWT.verify(token, verification_key: verification_key, algorithm: algo,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
226
|
+
verify_expiration: jwt[:verify_expiration],
|
|
227
|
+
verify_issuer: jwt[:verify_issuer],
|
|
228
|
+
issuer: jwt[:issuer])
|
|
252
229
|
end
|
|
253
230
|
|
|
254
231
|
def verify_external_token(token, jwks_url:, **)
|
|
@@ -265,7 +242,7 @@ module Legion
|
|
|
265
242
|
@started = false
|
|
266
243
|
true
|
|
267
244
|
end
|
|
268
|
-
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::
|
|
245
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + Legion::Crypt::Vault::Operation::SHUTDOWN_TIMEOUT
|
|
269
246
|
manager = Legion::Crypt::LeaseManager.instance
|
|
270
247
|
[manager, *(@token_renewers || [])].each do |worker|
|
|
271
248
|
worker.request_stop
|
|
@@ -298,9 +275,9 @@ module Legion
|
|
|
298
275
|
|
|
299
276
|
private
|
|
300
277
|
|
|
301
|
-
def start_lease_manager
|
|
302
|
-
leases =
|
|
303
|
-
vault_ok = connected_clusters.any? ||
|
|
278
|
+
def start_lease_manager(mode: nil)
|
|
279
|
+
leases = postgresql_leases_for_mode(leases: Legion::Settings[:crypt][:vault][:leases], mode: mode)
|
|
280
|
+
vault_ok = connected_clusters.any? || Legion::Settings[:crypt][:vault][:connected]
|
|
304
281
|
|
|
305
282
|
return if leases.empty? && !dynamic_rmq_creds?
|
|
306
283
|
return unless vault_ok
|
|
@@ -310,7 +287,7 @@ module Legion
|
|
|
310
287
|
if connected_clusters.any?
|
|
311
288
|
selected_cluster = selected_connected_cluster_name
|
|
312
289
|
client = selected_cluster ? vault_client(selected_cluster) : nil
|
|
313
|
-
elsif
|
|
290
|
+
elsif Legion::Settings[:crypt][:vault][:connected]
|
|
314
291
|
client = vault_client
|
|
315
292
|
end
|
|
316
293
|
|
|
@@ -327,13 +304,25 @@ module Legion
|
|
|
327
304
|
log.warn "LeaseManager: #{fetched}/#{defined} lease(s) initialized (#{defined - fetched} failed)"
|
|
328
305
|
end
|
|
329
306
|
end
|
|
330
|
-
rescue
|
|
307
|
+
rescue IOError, SystemCallError, Timeout::Error, Faraday::Error, Legion::Crypt::Vault::HTTPError => e
|
|
331
308
|
handle_exception(e, level: :warn, operation: 'crypt.start_lease_manager')
|
|
332
309
|
end
|
|
333
310
|
|
|
311
|
+
def postgresql_leases_for_mode(leases:, mode:)
|
|
312
|
+
role = POSTGRES_ROLE_MAP[mode]
|
|
313
|
+
return leases unless role
|
|
314
|
+
|
|
315
|
+
postgresql_key = leases.key?(:postgresql) ? :postgresql : 'postgresql'
|
|
316
|
+
definition = leases[postgresql_key]
|
|
317
|
+
return leases unless definition.is_a?(Hash)
|
|
318
|
+
|
|
319
|
+
path_key = definition.key?(:path) ? :path : 'path'
|
|
320
|
+
leases.merge(postgresql_key => definition.merge(path_key => "postgresql/creds/#{role}"))
|
|
321
|
+
end
|
|
322
|
+
|
|
334
323
|
def start_token_renewers
|
|
335
324
|
started = 0
|
|
336
|
-
configurations = clusters.empty? ? { nil =>
|
|
325
|
+
configurations = clusters.empty? ? { nil => Legion::Settings[:crypt][:vault] } : clusters
|
|
337
326
|
configurations.each do |name, config|
|
|
338
327
|
next unless %w[kerberos ldap jwt].include?(config[:auth_method].to_s) && config[:connected]
|
|
339
328
|
|
|
@@ -364,19 +353,19 @@ module Legion
|
|
|
364
353
|
end
|
|
365
354
|
|
|
366
355
|
def start_svid_rotation
|
|
367
|
-
return unless Spiffe.enabled?
|
|
356
|
+
return unless Legion::Crypt::Spiffe.enabled?
|
|
368
357
|
|
|
369
358
|
log.info 'Legion::Crypt starting SPIFFE SVID rotation'
|
|
370
|
-
@svid_rotation = Spiffe::SvidRotation.new
|
|
359
|
+
@svid_rotation = Legion::Crypt::Spiffe::SvidRotation.new
|
|
371
360
|
@svid_rotation.start
|
|
372
|
-
rescue
|
|
361
|
+
rescue IOError, SystemCallError, Timeout::Error, Legion::Crypt::Spiffe::Error => e
|
|
373
362
|
handle_exception(e, level: :warn, operation: 'crypt.start_svid_rotation')
|
|
374
363
|
end
|
|
375
364
|
|
|
376
365
|
def stop_svid_rotation(deadline: nil)
|
|
377
366
|
return unless @svid_rotation
|
|
378
367
|
|
|
379
|
-
Legion::Crypt::
|
|
368
|
+
Legion::Crypt::Vault::Operation.with_timeout(deadline: deadline) { @svid_rotation.stop }
|
|
380
369
|
ensure
|
|
381
370
|
@svid_rotation = nil
|
|
382
371
|
end
|