legion-crypt 1.4.12 → 1.4.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 228df771abf660b303d986d0df69c3cdf7a8a16ab68ebe7eec2712f99607e162
4
- data.tar.gz: 2ea96c31efeed7cad8286a74b6cfd3cdfdeafcbad8bd34bac7dc61f4fe3ecb83
3
+ metadata.gz: 59b6f53757eb825da7b0c635589149793c015d59c422b90916706bae8e796e1d
4
+ data.tar.gz: 4879d7e405f5bf0da54b488ac38c1bdba708280bd63d112da3ef487c467039f2
5
5
  SHA512:
6
- metadata.gz: c844d151b90c635e5f8c78ffac965f833ea33e9137ef86042f1cd6ad5627f045d66b44dc2e63343839c1909248a83494d2057945db33e2b80045094cec92939d
7
- data.tar.gz: 6938d92c6bd45111c3bcaae795731dff242b21609c3849fc3f9e99f27a61ed5865b6873482532009820e64dfd08b196b4c93cc0450c65afab5a9f8c6c049f3eb
6
+ metadata.gz: 6fee90f414f31a34f7f75713c9856b2644a4a74986182d19921aa2731db4a13b6a346417bacccbf449ab2a29fe5b6562d1fd4ba77a6adbba1a9b03c8c04c93e3
7
+ data.tar.gz: 36042a488a032df98488493270a9976d526400981ecb62277f9454dd5d6cdeac2be9425c1c9b755004fd3c8fe5814347a55659eb32baa95fd53437859150720b
@@ -0,0 +1,7 @@
1
+ # Auto-generated from team-config.yml
2
+ # Team: core
3
+ #
4
+ # To apply: scripts/apply-codeowners.sh legion-crypt
5
+
6
+ * @LegionIO/maintainers
7
+ * @LegionIO/core
@@ -0,0 +1,18 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ day: monday
8
+ open-pull-requests-limit: 5
9
+ labels:
10
+ - "type:dependencies"
11
+ - package-ecosystem: github-actions
12
+ directory: /
13
+ schedule:
14
+ interval: weekly
15
+ day: monday
16
+ open-pull-requests-limit: 5
17
+ labels:
18
+ - "type:dependencies"
@@ -3,14 +3,32 @@ on:
3
3
  push:
4
4
  branches: [main]
5
5
  pull_request:
6
+ schedule:
7
+ - cron: '0 9 * * 1'
6
8
 
7
9
  jobs:
8
10
  ci:
9
11
  uses: LegionIO/.github/.github/workflows/ci.yml@main
10
12
 
13
+ lint:
14
+ uses: LegionIO/.github/.github/workflows/lint-patterns.yml@main
15
+
16
+ security:
17
+ uses: LegionIO/.github/.github/workflows/security-scan.yml@main
18
+
19
+ version-changelog:
20
+ uses: LegionIO/.github/.github/workflows/version-changelog.yml@main
21
+
22
+ dependency-review:
23
+ uses: LegionIO/.github/.github/workflows/dependency-review.yml@main
24
+
25
+ stale:
26
+ if: github.event_name == 'schedule'
27
+ uses: LegionIO/.github/.github/workflows/stale.yml@main
28
+
11
29
  release:
12
- needs: ci
30
+ needs: [ci, lint]
13
31
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
32
  uses: LegionIO/.github/.github/workflows/release.yml@main
15
33
  secrets:
16
- rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
34
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.4.13] - 2026-03-25
4
+
5
+ ### Added
6
+ - Kerberos auto-auth to Vault on boot (`auth_method: 'kerberos'` per cluster)
7
+ - `KerberosAuth` module: client-side SPNEGO token acquisition via lex-kerberos, Vault token exchange
8
+ - `TokenRenewer`: plain-Thread token lifecycle (renew at 75% TTL, re-auth via Kerberos, exponential backoff 30s-10min)
9
+ - `kerberos` settings block in vault cluster config (`service_principal`, `auth_path`)
10
+ - `auth_method` dispatch in `connect_all_clusters` (kerberos, ldap, token)
11
+
12
+ ### Changed
13
+ - Token renewal no longer depends on `Extensions::Actors::Every` (starts at boot, not after extensions load)
14
+ - Removed actor-dependent renewer guard from `connect_vault`
15
+
3
16
  ## [1.4.12] - 2026-03-25
4
17
 
5
18
  ### Fixed
data/LICENSE CHANGED
@@ -1,7 +1,6 @@
1
-
2
1
  Apache License
3
2
  Version 2.0, January 2004
4
- https://www.apache.org/licenses/
3
+ http://www.apache.org/licenses/
5
4
 
6
5
  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
6
 
@@ -176,16 +175,27 @@
176
175
 
177
176
  END OF TERMS AND CONDITIONS
178
177
 
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
179
189
  Copyright 2021 Esity
180
190
 
181
191
  Licensed under the Apache License, Version 2.0 (the "License");
182
192
  you may not use this file except in compliance with the License.
183
193
  You may obtain a copy of the License at
184
194
 
185
- https://www.apache.org/licenses/LICENSE-2.0
195
+ http://www.apache.org/licenses/LICENSE-2.0
186
196
 
187
197
  Unless required by applicable law or agreed to in writing, software
188
198
  distributed under the License is distributed on an "AS IS" BASIS,
189
199
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
200
  See the License for the specific language governing permissions and
191
- limitations under the License.
201
+ limitations under the License.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Crypt
5
+ module KerberosAuth
6
+ class AuthError < StandardError; end
7
+ class GemMissingError < StandardError; end
8
+
9
+ DEFAULT_AUTH_PATH = 'auth/kerberos/login'
10
+
11
+ def self.login(vault_client:, service_principal:, auth_path: DEFAULT_AUTH_PATH)
12
+ raise GemMissingError, 'lex-kerberos gem is required for Kerberos auth' unless spnego_available?
13
+
14
+ token = obtain_token(service_principal)
15
+ exchange_token(vault_client, token, auth_path)
16
+ end
17
+
18
+ def self.spnego_available?
19
+ return @spnego_available unless @spnego_available.nil?
20
+
21
+ @spnego_available = begin
22
+ require 'legion/extensions/kerberos/helpers/spnego'
23
+ true
24
+ rescue LoadError
25
+ # check if constant was already defined (e.g. stubbed in tests or loaded via another path)
26
+ defined?(Legion::Extensions::Kerberos::Helpers::Spnego) ? true : false
27
+ end
28
+ end
29
+
30
+ def self.reset!
31
+ @spnego_available = nil
32
+ end
33
+
34
+ class << self
35
+ private
36
+
37
+ def obtain_token(service_principal)
38
+ helper = Object.new.extend(Legion::Extensions::Kerberos::Helpers::Spnego)
39
+ result = helper.obtain_spnego_token(service_principal: service_principal)
40
+ raise AuthError, "SPNEGO token acquisition failed: #{result[:error]}" unless result[:success]
41
+
42
+ result[:token]
43
+ end
44
+
45
+ def exchange_token(vault_client, spnego_token, auth_path)
46
+ response = vault_client.logical.write(auth_path, authorization: "Negotiate #{spnego_token}")
47
+ raise AuthError, 'Vault Kerberos auth returned no auth data' unless response&.auth
48
+
49
+ {
50
+ token: response.auth.client_token,
51
+ lease_duration: response.auth.lease_duration,
52
+ renewable: response.auth.renewable,
53
+ policies: response.auth.policies,
54
+ metadata: response.auth.metadata
55
+ }
56
+ rescue ::Vault::HTTPClientError => e
57
+ raise AuthError, "Vault Kerberos auth failed: #{e.message}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -52,6 +52,10 @@ module Legion
52
52
  kv_path: ENV['LEGION_VAULT_KV_PATH'] || 'legion',
53
53
  leases: {},
54
54
  default: nil,
55
+ kerberos: {
56
+ service_principal: nil,
57
+ auth_path: 'auth/kerberos/login'
58
+ },
55
59
  clusters: {}
56
60
  }
57
61
  end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/crypt/kerberos_auth'
4
+
5
+ module Legion
6
+ module Crypt
7
+ class TokenRenewer
8
+ INITIAL_BACKOFF = 30
9
+ MAX_BACKOFF = 600
10
+ MIN_SLEEP = 30
11
+ RENEWAL_RATIO = 0.75
12
+
13
+ attr_reader :cluster_name
14
+
15
+ def initialize(cluster_name:, config:, vault_client:)
16
+ @cluster_name = cluster_name
17
+ @config = config
18
+ @vault_client = vault_client
19
+ @thread = nil
20
+ @stop = false
21
+ @backoff = INITIAL_BACKOFF
22
+ end
23
+
24
+ def start
25
+ @stop = false
26
+ @thread = Thread.new { renewal_loop }
27
+ @thread.name = "vault-renewer-#{@cluster_name}"
28
+ log_debug('token renewal thread started')
29
+ end
30
+
31
+ def stop
32
+ @stop = true
33
+ @thread&.wakeup
34
+ rescue ThreadError
35
+ nil
36
+ ensure
37
+ @thread&.join(5)
38
+ @thread = nil
39
+ log_debug('token renewal thread stopped')
40
+ end
41
+
42
+ def running?
43
+ @thread&.alive? == true
44
+ end
45
+
46
+ def renew_token
47
+ result = @vault_client.auth_token.renew_self
48
+ @config[:lease_duration] = result.auth.lease_duration
49
+ log_debug("token renewed, ttl=#{result.auth.lease_duration}s")
50
+ true
51
+ rescue StandardError => e
52
+ log_warn("token renewal failed: #{e.message}")
53
+ false
54
+ end
55
+
56
+ def reauth_kerberos
57
+ krb_config = @config[:kerberos] || {}
58
+ result = Legion::Crypt::KerberosAuth.login(
59
+ vault_client: @vault_client,
60
+ service_principal: krb_config[:service_principal],
61
+ auth_path: krb_config[:auth_path] || KerberosAuth::DEFAULT_AUTH_PATH
62
+ )
63
+
64
+ @config[:token] = result[:token]
65
+ @config[:lease_duration] = result[:lease_duration]
66
+ @config[:renewable] = result[:renewable]
67
+ @config[:connected] = true
68
+ @vault_client.token = result[:token]
69
+ log_info('re-authenticated via Kerberos')
70
+ true
71
+ rescue StandardError => e
72
+ log_warn("Kerberos re-auth failed: #{e.message}")
73
+ false
74
+ end
75
+
76
+ def sleep_duration
77
+ duration = (@config[:lease_duration].to_i * RENEWAL_RATIO).to_i
78
+ [duration, MIN_SLEEP].max
79
+ end
80
+
81
+ def next_backoff
82
+ current = @backoff
83
+ @backoff = [@backoff * 2, MAX_BACKOFF].min
84
+ current
85
+ end
86
+
87
+ def reset_backoff
88
+ @backoff = INITIAL_BACKOFF
89
+ end
90
+
91
+ private
92
+
93
+ def renewal_loop
94
+ interruptible_sleep(sleep_duration)
95
+
96
+ until @stop
97
+ if renew_token || reauth_kerberos
98
+ on_renewal_success
99
+ else
100
+ on_renewal_failure
101
+ end
102
+ end
103
+ rescue StandardError => e
104
+ log_warn("renewal loop error: #{e.message}")
105
+ retry unless @stop
106
+ end
107
+
108
+ def on_renewal_success
109
+ reset_backoff
110
+ interruptible_sleep(sleep_duration)
111
+ end
112
+
113
+ def on_renewal_failure
114
+ @config[:connected] = false
115
+ delay = next_backoff
116
+ log_warn("backoff retry in #{delay}s")
117
+ interruptible_sleep(delay)
118
+ end
119
+
120
+ def interruptible_sleep(seconds)
121
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + seconds
122
+ loop do
123
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
124
+ break if remaining <= 0 || @stop
125
+
126
+ sleep([remaining, 1.0].min)
127
+ end
128
+ end
129
+
130
+ def log_debug(message)
131
+ Legion::Logging.debug("TokenRenewer[#{@cluster_name}]: #{message}") if defined?(Legion::Logging)
132
+ end
133
+
134
+ def log_info(message)
135
+ Legion::Logging.info("TokenRenewer[#{@cluster_name}]: #{message}") if defined?(Legion::Logging)
136
+ end
137
+
138
+ def log_warn(message)
139
+ Legion::Logging.warn("TokenRenewer[#{@cluster_name}]: #{message}") if defined?(Legion::Logging)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -36,10 +36,6 @@ module Legion
36
36
  Legion::Settings[:crypt][:vault][:connected] = true
37
37
  Legion::Logging.info "Vault connected at #{::Vault.address}" if defined?(Legion::Logging)
38
38
  end
39
- return unless Legion.const_defined? 'Extensions::Actors::Every'
40
-
41
- require_relative 'vault_renewer'
42
- @renewer = Legion::Crypt::Vault::Renewer.new
43
39
  rescue StandardError => e
44
40
  Legion::Logging.error e.message
45
41
  Legion::Settings[:crypt][:vault][:connected] = false
@@ -42,12 +42,19 @@ module Legion
42
42
  def connect_all_clusters
43
43
  results = {}
44
44
  clusters.each do |name, config|
45
- next unless config[:token]
45
+ case config[:auth_method]&.to_s
46
+ when 'kerberos'
47
+ results[name] = connect_kerberos_cluster(name, config)
48
+ when 'ldap'
49
+ next # handled by ldap_login_all
50
+ else
51
+ next unless config[:token]
46
52
 
47
- client = vault_client(name)
48
- config[:connected] = client.sys.health_status.initialized?
49
- results[name] = config[:connected]
50
- Legion::Logging.info "Vault cluster connected: #{name} at #{config[:address]}" if config[:connected] && defined?(Legion::Logging)
53
+ client = vault_client(name)
54
+ config[:connected] = client.sys.health_status.initialized?
55
+ results[name] = config[:connected]
56
+ log_cluster_connected(name, config) if config[:connected]
57
+ end
51
58
  rescue StandardError => e
52
59
  config[:connected] = false
53
60
  results[name] = false
@@ -82,6 +89,51 @@ module Legion
82
89
  warn("Vault cluster #{name}: #{error.message}")
83
90
  end
84
91
  end
92
+
93
+ def connect_kerberos_cluster(name, config)
94
+ krb_config = config[:kerberos] || {}
95
+ spn = krb_config[:service_principal]
96
+
97
+ unless spn
98
+ log_vault_warn(name, 'Kerberos auth missing service_principal, skipping')
99
+ config[:connected] = false
100
+ return false
101
+ end
102
+
103
+ require 'legion/crypt/kerberos_auth'
104
+ result = Legion::Crypt::KerberosAuth.login(
105
+ vault_client: vault_client(name),
106
+ service_principal: spn,
107
+ auth_path: krb_config[:auth_path] || Legion::Crypt::KerberosAuth::DEFAULT_AUTH_PATH
108
+ )
109
+
110
+ config[:token] = result[:token]
111
+ config[:lease_duration] = result[:lease_duration]
112
+ config[:renewable] = result[:renewable]
113
+ config[:connected] = true
114
+ log_cluster_connected(name, config)
115
+ true
116
+ rescue Legion::Crypt::KerberosAuth::GemMissingError => e
117
+ log_vault_warn(name, e.message)
118
+ config[:connected] = false
119
+ false
120
+ rescue Legion::Crypt::KerberosAuth::AuthError => e
121
+ log_vault_warn(name, "Kerberos auth failed: #{e.message}")
122
+ config[:connected] = false
123
+ false
124
+ end
125
+
126
+ def log_cluster_connected(name, config)
127
+ Legion::Logging.info "Vault cluster connected: #{name} at #{config[:address]}" if defined?(Legion::Logging)
128
+ end
129
+
130
+ def log_vault_warn(name, message)
131
+ if defined?(Legion::Logging)
132
+ Legion::Logging.warn("Vault cluster #{name}: #{message}")
133
+ else
134
+ warn("Vault cluster #{name}: #{message}")
135
+ end
136
+ end
85
137
  end
86
138
  end
87
139
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.12'
5
+ VERSION = '1.4.13'
6
6
  end
7
7
  end
data/lib/legion/crypt.rb CHANGED
@@ -10,6 +10,7 @@ require 'legion/crypt/vault_jwt_auth'
10
10
  require 'legion/crypt/lease_manager'
11
11
  require 'legion/crypt/vault_cluster'
12
12
  require 'legion/crypt/ldap_auth'
13
+ require 'legion/crypt/token_renewer'
13
14
  require 'legion/crypt/helper'
14
15
  require 'legion/crypt/mtls'
15
16
  require 'legion/crypt/cert_rotation'
@@ -36,9 +37,11 @@ module Legion
36
37
  def start
37
38
  Legion::Logging.debug 'Legion::Crypt is running start'
38
39
  ::File.write('./legionio.key', private_key) if settings[:save_private_key]
40
+ @token_renewers ||= []
39
41
 
40
42
  if vault_settings[:clusters]&.any?
41
43
  connect_all_clusters
44
+ start_token_renewers
42
45
  else
43
46
  connect_vault unless settings[:vault][:token].nil?
44
47
  end
@@ -86,6 +89,7 @@ module Legion
86
89
 
87
90
  def shutdown
88
91
  Legion::Crypt::LeaseManager.instance.shutdown
92
+ stop_token_renewers
89
93
  shutdown_renewer
90
94
  close_sessions
91
95
  end
@@ -104,6 +108,27 @@ module Legion
104
108
  rescue StandardError => e
105
109
  Legion::Logging.warn "LeaseManager startup failed: #{e.message}"
106
110
  end
111
+
112
+ def start_token_renewers
113
+ clusters.each do |name, config|
114
+ next unless config[:auth_method]&.to_s == 'kerberos' && config[:connected]
115
+
116
+ renewer = Legion::Crypt::TokenRenewer.new(
117
+ cluster_name: name,
118
+ config: config,
119
+ vault_client: vault_client(name)
120
+ )
121
+ renewer.start
122
+ @token_renewers << renewer
123
+ end
124
+ end
125
+
126
+ def stop_token_renewers
127
+ return unless @token_renewers
128
+
129
+ @token_renewers.each(&:stop)
130
+ @token_renewers.clear
131
+ end
107
132
  end
108
133
  end
109
134
  end
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.4.12
4
+ version: 1.4.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -61,6 +61,8 @@ extra_rdoc_files:
61
61
  - LICENSE
62
62
  - README.md
63
63
  files:
64
+ - ".github/CODEOWNERS"
65
+ - ".github/dependabot.yml"
64
66
  - ".github/workflows/ci.yml"
65
67
  - ".gitignore"
66
68
  - ".rubocop.yml"
@@ -81,6 +83,7 @@ files:
81
83
  - lib/legion/crypt/helper.rb
82
84
  - lib/legion/crypt/jwks_client.rb
83
85
  - lib/legion/crypt/jwt.rb
86
+ - lib/legion/crypt/kerberos_auth.rb
84
87
  - lib/legion/crypt/ldap_auth.rb
85
88
  - lib/legion/crypt/lease_manager.rb
86
89
  - lib/legion/crypt/mock_vault.rb
@@ -88,6 +91,7 @@ files:
88
91
  - lib/legion/crypt/partition_keys.rb
89
92
  - lib/legion/crypt/settings.rb
90
93
  - lib/legion/crypt/tls.rb
94
+ - lib/legion/crypt/token_renewer.rb
91
95
  - lib/legion/crypt/vault.rb
92
96
  - lib/legion/crypt/vault_cluster.rb
93
97
  - lib/legion/crypt/vault_jwt_auth.rb