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
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-crypt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.5.
|
|
4
|
+
version: 1.5.42
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -93,6 +93,20 @@ dependencies:
|
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: 1.5.0
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: legion-settings
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 1.4.2
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 1.4.2
|
|
96
110
|
description: A gem used by the LegionIO framework for encryption
|
|
97
111
|
email:
|
|
98
112
|
- matthewdiverson@gmail.com
|
|
@@ -108,6 +122,7 @@ files:
|
|
|
108
122
|
- ".github/workflows/ci.yml"
|
|
109
123
|
- ".gitignore"
|
|
110
124
|
- ".pre-commit-config.yaml"
|
|
125
|
+
- ".rspec"
|
|
111
126
|
- ".rubocop.yml"
|
|
112
127
|
- AGENTS.md
|
|
113
128
|
- CHANGELOG.md
|
|
@@ -141,13 +156,13 @@ files:
|
|
|
141
156
|
- lib/legion/crypt/tls.rb
|
|
142
157
|
- lib/legion/crypt/token_renewer.rb
|
|
143
158
|
- lib/legion/crypt/vault.rb
|
|
144
|
-
- lib/legion/crypt/
|
|
145
|
-
- lib/legion/crypt/
|
|
146
|
-
- lib/legion/crypt/
|
|
147
|
-
- lib/legion/crypt/
|
|
148
|
-
- lib/legion/crypt/
|
|
149
|
-
- lib/legion/crypt/
|
|
150
|
-
- lib/legion/crypt/
|
|
159
|
+
- lib/legion/crypt/vault/client.rb
|
|
160
|
+
- lib/legion/crypt/vault/cluster.rb
|
|
161
|
+
- lib/legion/crypt/vault/entity.rb
|
|
162
|
+
- lib/legion/crypt/vault/jwt_auth.rb
|
|
163
|
+
- lib/legion/crypt/vault/kerberos_auth.rb
|
|
164
|
+
- lib/legion/crypt/vault/operation.rb
|
|
165
|
+
- lib/legion/crypt/vault/renewer.rb
|
|
151
166
|
- lib/legion/crypt/version.rb
|
|
152
167
|
- scripts/pre-commit-rubocop.sh
|
|
153
168
|
- sonar-project.properties
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'faraday'
|
|
4
|
-
require 'legion/json'
|
|
5
|
-
require 'openssl'
|
|
6
|
-
require 'uri'
|
|
7
|
-
|
|
8
|
-
module Legion
|
|
9
|
-
module Crypt
|
|
10
|
-
module Vault
|
|
11
|
-
class HTTPError < StandardError
|
|
12
|
-
attr_reader :code, :errors, :address
|
|
13
|
-
|
|
14
|
-
def initialize(code:, errors: [], address: nil, **)
|
|
15
|
-
@code = Integer(code)
|
|
16
|
-
@errors = Array(errors)
|
|
17
|
-
@address = address
|
|
18
|
-
super("Vault HTTP #{@code}#{": #{@errors.join('; ')}" unless @errors.empty?}")
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
class HTTPClientError < HTTPError; end
|
|
23
|
-
class HTTPServerError < HTTPError; end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
class VaultClient
|
|
27
|
-
CONNECTION_ATTRIBUTES = %i[address ssl_verify ca cert key timeout].freeze
|
|
28
|
-
|
|
29
|
-
attr_reader(*CONNECTION_ATTRIBUTES, :logical, :sys, :auth_token, :token)
|
|
30
|
-
attr_accessor :namespace
|
|
31
|
-
|
|
32
|
-
def initialize(address:, token: nil, namespace: nil, tls: {}, timeout: 5, **)
|
|
33
|
-
@address = validate_address(address: address)
|
|
34
|
-
@timeout = validate_timeout(timeout: timeout)
|
|
35
|
-
@token = token
|
|
36
|
-
@namespace = namespace
|
|
37
|
-
@ssl_verify = tls[:verify] != false && tls[:verify].to_s != 'none'
|
|
38
|
-
@ca = tls[:ca]
|
|
39
|
-
@cert = tls[:cert]
|
|
40
|
-
@key = tls[:key]
|
|
41
|
-
@mutex = Mutex.new
|
|
42
|
-
@retired = false
|
|
43
|
-
@logical = Logical.new(client: self)
|
|
44
|
-
@sys = Sys.new(client: self)
|
|
45
|
-
@auth_token = AuthToken.new(client: self)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
CONNECTION_ATTRIBUTES.each do |attribute|
|
|
49
|
-
define_method(:"#{attribute}=") do |value|
|
|
50
|
-
value = validate_address(address: value) if attribute == :address
|
|
51
|
-
value = validate_timeout(timeout: value) if attribute == :timeout
|
|
52
|
-
@mutex.synchronize do
|
|
53
|
-
return value if instance_variable_get(:"@#{attribute}") == value
|
|
54
|
-
|
|
55
|
-
previous = @connection
|
|
56
|
-
@connection = nil
|
|
57
|
-
instance_variable_set(:"@#{attribute}", value)
|
|
58
|
-
previous&.close
|
|
59
|
-
end
|
|
60
|
-
value
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def token=(value)
|
|
65
|
-
@mutex.synchronize do
|
|
66
|
-
raise IOError, 'Vault client has been shut down' if @retired
|
|
67
|
-
|
|
68
|
-
@token = value
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def kv(mount:, **)
|
|
73
|
-
KV.new(client: self, mount: mount)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def put(path:, body:, headers: {}, **)
|
|
77
|
-
request(method: :put, path: path, body: body, headers: headers) || {}
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def request(method:, path:, body: nil, headers: {}, nil_on_not_found: false, **)
|
|
81
|
-
connection, request_headers, address = @mutex.synchronize do
|
|
82
|
-
raise IOError, 'Vault client has been shut down' if @retired
|
|
83
|
-
|
|
84
|
-
[@connection ||= build_connection, vault_headers(headers: headers), @address]
|
|
85
|
-
end
|
|
86
|
-
payload = body.is_a?(String) || body.nil? ? body : Legion::JSON.dump(body)
|
|
87
|
-
response = connection.run_request(method, request_path(path: path), payload, request_headers)
|
|
88
|
-
return nil if nil_on_not_found && response.status == 404
|
|
89
|
-
|
|
90
|
-
decode_response(response: response, address: address)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def shutdown
|
|
94
|
-
@mutex.synchronize do
|
|
95
|
-
@retired = true
|
|
96
|
-
previous = @connection
|
|
97
|
-
@connection = nil
|
|
98
|
-
previous&.close
|
|
99
|
-
end
|
|
100
|
-
nil
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def inspect
|
|
104
|
-
"#<#{self.class}>"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
class Logical
|
|
108
|
-
def initialize(client:, **)
|
|
109
|
-
@client = client
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def read(path:, **)
|
|
113
|
-
Secret.decode(data: @client.request(method: :get, path: path, nil_on_not_found: true))
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def write(path:, data:, **)
|
|
117
|
-
Secret.decode(data: @client.put(path: path, body: data))
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def delete(path:, **)
|
|
121
|
-
@client.request(method: :delete, path: path)
|
|
122
|
-
true
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
class Sys
|
|
127
|
-
class HealthStatus
|
|
128
|
-
def initialize(data:, **)
|
|
129
|
-
@initialized = data[:initialized]
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def initialized?
|
|
133
|
-
@initialized == true
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def initialize(client:, **)
|
|
138
|
-
@client = client
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def health_status
|
|
142
|
-
HealthStatus.new(data: @client.request(method: :get, path: 'sys/health'))
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def mounts
|
|
146
|
-
@client.request(method: :get, path: 'sys/mounts').fetch(:data)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def renew(lease_id:, **)
|
|
150
|
-
Secret.decode(data: @client.put(path: 'sys/leases/renew', body: { lease_id: lease_id }))
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def revoke(lease_id:, **)
|
|
154
|
-
@client.put(path: 'sys/leases/revoke', body: { lease_id: lease_id })
|
|
155
|
-
true
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
class AuthToken
|
|
160
|
-
def initialize(client:, **)
|
|
161
|
-
@client = client
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def renew_self
|
|
165
|
-
Secret.decode(data: @client.put(path: 'auth/token/renew-self', body: {}))
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def revoke_self
|
|
169
|
-
@client.put(path: 'auth/token/revoke-self', body: {})
|
|
170
|
-
true
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
class KV
|
|
175
|
-
def initialize(client:, mount:, **)
|
|
176
|
-
@client = client
|
|
177
|
-
mount = mount.to_s
|
|
178
|
-
first = mount.index(%r{[^/]})
|
|
179
|
-
@mount = first ? mount[first..mount.rindex(%r{[^/]})] : ''
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def read(path:, **)
|
|
183
|
-
response = @client.request(method: :get, path: "#{@mount}/data/#{path}", nil_on_not_found: true)
|
|
184
|
-
return nil if response.nil?
|
|
185
|
-
|
|
186
|
-
Secret.decode(data: response.merge(data: response.dig(:data, :data)))
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def write(path:, data:, **)
|
|
190
|
-
Secret.decode(data: @client.put(path: "#{@mount}/data/#{path}", body: { data: data }))
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def read_metadata(path:, **)
|
|
194
|
-
Secret.decode(data: @client.request(method: :get, path: "#{@mount}/metadata/#{path}", nil_on_not_found: true))
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
class Secret
|
|
199
|
-
attr_reader :data, :auth, :lease_id, :lease_duration
|
|
200
|
-
|
|
201
|
-
def self.decode(data:, **)
|
|
202
|
-
return nil if data.nil? || data.empty?
|
|
203
|
-
|
|
204
|
-
new(data: data)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def initialize(data:, **)
|
|
208
|
-
@data = data[:data]
|
|
209
|
-
@auth = Auth.new(data: data[:auth]) unless data[:auth].nil?
|
|
210
|
-
@lease_id = data[:lease_id]
|
|
211
|
-
@lease_duration = data[:lease_duration]
|
|
212
|
-
@renewable = data[:renewable]
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def renewable?
|
|
216
|
-
@renewable == true
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def inspect
|
|
220
|
-
"#<#{self.class}>"
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
class Auth
|
|
225
|
-
attr_reader :client_token, :lease_duration, :policies, :metadata
|
|
226
|
-
|
|
227
|
-
def initialize(data:, **)
|
|
228
|
-
@client_token = data[:client_token]
|
|
229
|
-
@lease_duration = data[:lease_duration]
|
|
230
|
-
@policies = data[:policies]
|
|
231
|
-
@metadata = data[:metadata]
|
|
232
|
-
@renewable = data[:renewable]
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def renewable?
|
|
236
|
-
@renewable == true
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def inspect
|
|
240
|
-
"#<#{self.class}>"
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
private
|
|
245
|
-
|
|
246
|
-
def validate_address(address:, **)
|
|
247
|
-
uri = URI.parse(address.to_s)
|
|
248
|
-
unless uri.is_a?(URI::HTTP) && uri.host && !uri.userinfo && !uri.query && !uri.fragment
|
|
249
|
-
raise ArgumentError, 'Vault address must be an HTTP(S) URL without credentials, query, or fragment'
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
uri.to_s.freeze
|
|
253
|
-
rescue URI::InvalidURIError
|
|
254
|
-
raise ArgumentError, 'Vault address must be a valid HTTP(S) URL'
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def validate_timeout(timeout:, **)
|
|
258
|
-
seconds = Float(timeout)
|
|
259
|
-
raise ArgumentError, 'Vault timeout must be positive and finite' unless seconds.positive? && seconds.finite?
|
|
260
|
-
|
|
261
|
-
seconds
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def vault_headers(headers:, **)
|
|
265
|
-
result = Faraday::Utils::Headers.new('Accept' => 'application/json', 'Content-Type' => 'application/json')
|
|
266
|
-
result.update(headers)
|
|
267
|
-
result.delete('X-Vault-Token')
|
|
268
|
-
result.delete('X-Vault-Namespace')
|
|
269
|
-
result['X-Vault-Token'] = @token unless @token.nil?
|
|
270
|
-
result['X-Vault-Namespace'] = @namespace unless @namespace.nil?
|
|
271
|
-
result
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def request_path(path:, **)
|
|
275
|
-
relative = path.to_s.sub(%r{\A/v1(?:/|\z)}, '').sub(%r{\A/+}, '')
|
|
276
|
-
encoded = relative.split('/', -1).map do |segment|
|
|
277
|
-
if ['.', '..'].include?(segment)
|
|
278
|
-
segment.gsub('.', '%2E')
|
|
279
|
-
else
|
|
280
|
-
URI.encode_www_form_component(segment).gsub('+', '%20')
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
"/v1/#{encoded.join('/')}"
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def build_connection
|
|
287
|
-
Faraday.new(url: @address, ssl: tls_options) do |connection|
|
|
288
|
-
connection.options.timeout = @timeout
|
|
289
|
-
connection.options.open_timeout = @timeout
|
|
290
|
-
connection.options.read_timeout = @timeout
|
|
291
|
-
connection.options.write_timeout = @timeout
|
|
292
|
-
connection.adapter :net_http
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def tls_options
|
|
297
|
-
options = { verify: @ssl_verify != false && @ssl_verify.to_s != 'none' }
|
|
298
|
-
if @ca
|
|
299
|
-
if @ca.include?('-----BEGIN')
|
|
300
|
-
store = OpenSSL::X509::Store.new
|
|
301
|
-
certificates = @ca.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
|
|
302
|
-
raise ArgumentError, 'Vault CA PEM contains no certificates' if certificates.empty?
|
|
303
|
-
|
|
304
|
-
certificates.each { |certificate| store.add_cert(OpenSSL::X509::Certificate.new(certificate)) }
|
|
305
|
-
options[:cert_store] = store
|
|
306
|
-
else
|
|
307
|
-
options[:ca_file] = @ca
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
options[:client_cert] = tls_certificate(certificate: @cert) if @cert
|
|
311
|
-
options[:client_key] = tls_key(key: @key) if @key
|
|
312
|
-
options
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def tls_certificate(certificate:, **)
|
|
316
|
-
return certificate if certificate.is_a?(OpenSSL::X509::Certificate)
|
|
317
|
-
|
|
318
|
-
pem = certificate.include?('-----BEGIN') ? certificate : File.read(certificate)
|
|
319
|
-
certificates = pem.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
|
|
320
|
-
return OpenSSL::X509::Certificate.new(pem) if certificates.length < 2
|
|
321
|
-
|
|
322
|
-
certificates.map { |entry| OpenSSL::X509::Certificate.new(entry) }
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def tls_key(key:, **)
|
|
326
|
-
return key if key.is_a?(OpenSSL::PKey::PKey)
|
|
327
|
-
|
|
328
|
-
OpenSSL::PKey.read(key.include?('-----BEGIN') ? key : File.read(key))
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def decode_response(response:, address:, **)
|
|
332
|
-
status = response.status
|
|
333
|
-
unless (200..299).cover?(status)
|
|
334
|
-
error_class = if (400..499).cover?(status)
|
|
335
|
-
Legion::Crypt::Vault::HTTPClientError
|
|
336
|
-
elsif (500..599).cover?(status)
|
|
337
|
-
Legion::Crypt::Vault::HTTPServerError
|
|
338
|
-
else
|
|
339
|
-
Legion::Crypt::Vault::HTTPError
|
|
340
|
-
end
|
|
341
|
-
begin
|
|
342
|
-
decoded = response.body.nil? || response.body.empty? ? {} : Legion::JSON.load(response.body)
|
|
343
|
-
rescue Legion::JSON::ParseError
|
|
344
|
-
raise error_class.new(code: status, errors: [response.body.to_s], address: address)
|
|
345
|
-
end
|
|
346
|
-
errors = decoded.is_a?(Hash) ? decoded[:errors] : [response.body.to_s]
|
|
347
|
-
raise error_class.new(code: status, errors: errors, address: address)
|
|
348
|
-
end
|
|
349
|
-
return nil if status == 204 || response.body.nil? || response.body.empty?
|
|
350
|
-
|
|
351
|
-
Legion::JSON.load(response.body)
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
end
|