lex-kerberos 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 15ea37e6d645fc9dd2e3c6cb8efb0cde73d4b4fef19cf5d86107e31a82a31bc2
4
+ data.tar.gz: 9269c50bf9e6ef8b21f7f36ba515576404d522f3893c60903543b8de73564ab7
5
+ SHA512:
6
+ metadata.gz: e3cb6c6e00bee1659282fde1f92f718e748f145eb270f50d5d014cd203e584ec74cf01bd229404bf5cc340cd1a415d38cff0d0475f91d9afa7628e1f8573f9af
7
+ data.tar.gz: 6911ada268f1b63dadf7a909be989c49474448be7a7e081682f8956f4d70c85d6881294b3958c6d1c47d9f581badf8449f591a4ff1e11ab6b77dda620dbf45be
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.4
4
+ SuggestExtensions: false
5
+
6
+ Metrics/BlockLength:
7
+ Exclude:
8
+ - 'spec/**/*'
9
+ - '*.gemspec'
10
+
11
+ Metrics/ParameterLists:
12
+ Max: 10
13
+
14
+ Style/Documentation:
15
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2026-03-17
6
+
7
+ ### Added
8
+ - SPNEGO/GSSAPI token validation via `gssapi` gem (`Helpers::Spnego#accept_spnego_token`)
9
+ - LDAP group resolution via `net-ldap` gem (`Helpers::Ldap#lookup_groups`) with configurable filter and attribute
10
+ - Keytab management with Vault-primary, file-fallback resolution (`Helpers::Keytab#resolve_keytab`); supports `vault://` URIs, file paths, and Base64 blobs written to `~/.legionio/kerberos/legion.keytab`
11
+ - Standalone `Client` class with `authenticate(token:)` and `resolve_groups(username:)` for framework-independent usage
12
+ - `Runners::Authenticate#validate_spnego`: full pipeline combining keytab resolution, GSSAPI acceptance, and optional LDAP group lookup
13
+ - `Actor::KeytabRefresh`: interval actor (hourly) that re-fetches and caches the keytab from configured sources
14
+ - 43 specs, 91.67% coverage
data/CLAUDE.md ADDED
@@ -0,0 +1,122 @@
1
+ # lex-kerberos: Kerberos/SPNEGO Authentication for LegionIO
2
+
3
+ **Repository Level 3 Documentation**
4
+ - **Parent (Level 2)**: `/Users/miverso2/rubymine/legion/extensions/CLAUDE.md`
5
+ - **Parent (Level 1)**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
6
+
7
+ ## Purpose
8
+
9
+ Legion Extension that provides Kerberos/SPNEGO authentication. Validates SPNEGO tokens via GSSAPI, resolves LDAP group membership against Active Directory, and manages keytab files with Vault-primary, file-fallback sourcing. Designed for services that accept HTTP Negotiate authentication from AD-joined clients.
10
+
11
+ **GitHub**: https://github.com/LegionIO/lex-kerberos
12
+ **License**: MIT
13
+ **Version**: 0.1.0
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ Legion::Extensions::Kerberos
19
+ ├── Runners/
20
+ │ └── Authenticate # validate_spnego: keytab resolve + GSSAPI accept + LDAP groups
21
+ ├── Actors/
22
+ │ └── KeytabRefresh # Every actor (1hr): re-fetch keytab from Vault/sources
23
+ ├── Helpers/
24
+ │ ├── Spnego # GSSAPI token validation, principal/realm extraction
25
+ │ ├── Ldap # Net::LDAP group lookup via sAMAccountName filter
26
+ │ ├── Keytab # Multi-source keytab resolution (vault://, file path, Base64)
27
+ │ └── Client # Settings defaults and Legion::Settings merge
28
+ └── Client # Standalone client class (includes all helpers)
29
+ ```
30
+
31
+ ## File Map
32
+
33
+ | File | Purpose |
34
+ |------|---------|
35
+ | `lib/legion/extensions/kerberos.rb` | Entry point, requires all helpers/runners/actors, extends Core |
36
+ | `lib/legion/extensions/kerberos/helpers/spnego.rb` | GSSAPI token acceptance via `gssapi` gem; `accept_spnego_token`, `extract_username`, `extract_realm` |
37
+ | `lib/legion/extensions/kerberos/helpers/ldap.rb` | LDAP group lookup via `net-ldap`; `lookup_groups` with configurable filter/attribute |
38
+ | `lib/legion/extensions/kerberos/helpers/keytab.rb` | Multi-source keytab resolution; vault:// URI, file path, Base64 blob; writes to `~/.legionio/kerberos/legion.keytab` |
39
+ | `lib/legion/extensions/kerberos/helpers/client.rb` | `DEFAULTS` constant and `settings` method that merges with `Legion::Settings[:kerberos]` |
40
+ | `lib/legion/extensions/kerberos/runners/authenticate.rb` | `validate_spnego` runner: orchestrates keytab resolve → SPNEGO accept → optional LDAP lookup |
41
+ | `lib/legion/extensions/kerberos/actors/keytab_refresh.rb` | Hourly actor that calls `resolve_keytab` to re-cache from Vault; `run_now? false` (no immediate run at boot) |
42
+ | `lib/legion/extensions/kerberos/client.rb` | Standalone `Client` class with `authenticate(token:)` and `resolve_groups(username:)` |
43
+ | `lib/legion/extensions/kerberos/version.rb` | `VERSION = '0.1.0'` |
44
+
45
+ ## Key Patterns
46
+
47
+ ### GSSAPI Token Acceptance
48
+
49
+ `Helpers::Spnego#accept_spnego_token` decodes the Base64 SPNEGO token from the HTTP `Authorization: Negotiate` header, sets `KRB5_KTNAME` env var to the keytab path, then uses `GSSAPI::Simple.new(host, service)` to accept the security context. Returns `{ success:, principal:, username:, realm:, output_token: }`.
50
+
51
+ The service principal must be split as `service/host` (e.g., `HTTP/myapp.example.com`) — `GSSAPI::Simple` takes host and service name separately.
52
+
53
+ ### Keytab Resolution
54
+
55
+ `Helpers::Keytab#resolve_keytab(sources:)` tries each source in order:
56
+ 1. `vault://` URI — resolved via `Legion::Settings::Resolver.resolve_value`, then written as Base64 to cache
57
+ 2. File path — used as-is if `File.exist?`
58
+ 3. Base64 blob — decoded and written to `~/.legionio/kerberos/legion.keytab` (mode `0600`)
59
+
60
+ Returns `{ success: true, path:, source: (:file | :base64) }` or `{ success: false, error: }`.
61
+
62
+ ### LDAP Group Lookup
63
+
64
+ `Helpers::Ldap#lookup_groups` builds a `Net::LDAP` client with TLS, binds with the service account, and searches by `sAMAccountName` filter (format string `%<username>s`). Returns the full DN strings from the `memberOf` attribute (configurable via `group_attribute:`).
65
+
66
+ Requires `host:` in the ldap opts; if absent, `Runners::Authenticate` skips group lookup and returns an empty array.
67
+
68
+ ### Standalone Client Pattern
69
+
70
+ `Client.new` accepts `realm:`, `service_principal:`, `keytab:`, and `**opts` (where `opts[:ldap]` is passed to group lookup). Falls back to `settings[:kerberos]` defaults when kwargs are nil. Includes all four helpers. `authenticate(token:)` and `resolve_groups(username:)` are the primary interface methods.
71
+
72
+ ## Settings Reference
73
+
74
+ ```json
75
+ {
76
+ "kerberos": {
77
+ "enabled": true,
78
+ "realm": "EXAMPLE.COM",
79
+ "service_principal": "HTTP/myapp.example.com",
80
+ "keytab": ["vault://secret/kerberos#keytab", "/etc/legion/krb5.keytab"],
81
+ "mutual_auth": true,
82
+ "ldap": {
83
+ "host": "dc.example.com",
84
+ "port": 636,
85
+ "encryption": "simple_tls",
86
+ "base_dn": "DC=example,DC=com",
87
+ "bind_dn": "CN=svc-legion,OU=Service Accounts,DC=example,DC=com",
88
+ "bind_password": "vault://secret/kerberos/ldap_bind#password",
89
+ "user_filter": "(sAMAccountName=%<username>s)",
90
+ "group_attribute": "memberOf"
91
+ },
92
+ "role_map": {},
93
+ "fallback": "entra",
94
+ "cache_groups_ttl": 300
95
+ }
96
+ }
97
+ ```
98
+
99
+ Defaults defined in `Helpers::Client::DEFAULTS`. `settings` method merges `Legion::Settings[:kerberos]` on top when available, returning the full merged hash under `:kerberos`.
100
+
101
+ ## Dependencies
102
+
103
+ | Gem | Purpose |
104
+ |-----|---------|
105
+ | `gssapi` (~> 1.3) | GSSAPI/SPNEGO token validation; requires system MIT Kerberos libraries (`krb5`) |
106
+ | `net-ldap` (~> 0.19) | LDAP group lookup against Active Directory |
107
+
108
+ Optional framework dependencies (guarded with `defined?`, not in gemspec):
109
+ - `legion-settings` — `Legion::Settings::Resolver` for `vault://` keytab URI resolution
110
+ - `legion-logging` — logging in `KeytabRefresh` actor
111
+
112
+ ## Testing
113
+
114
+ ```bash
115
+ bundle install
116
+ bundle exec rspec # 43 specs, 91.67% coverage
117
+ bundle exec rubocop # Clean
118
+ ```
119
+
120
+ ---
121
+
122
+ **Maintained By**: Matthew Iverson (@Esity)
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75'
9
+ gem 'simplecov', '~> 0.22'
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,159 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lex-kerberos (0.1.0)
5
+ gssapi (~> 1.3)
6
+ net-ldap (~> 0.19)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.8.9)
12
+ public_suffix (>= 2.0.2, < 8.0)
13
+ ast (2.4.3)
14
+ base64 (0.3.0)
15
+ bigdecimal (4.0.1)
16
+ diff-lcs (1.6.2)
17
+ docile (1.4.1)
18
+ ffi (1.17.3)
19
+ ffi (1.17.3-aarch64-linux-gnu)
20
+ ffi (1.17.3-aarch64-linux-musl)
21
+ ffi (1.17.3-arm-linux-gnu)
22
+ ffi (1.17.3-arm-linux-musl)
23
+ ffi (1.17.3-arm64-darwin)
24
+ ffi (1.17.3-x86-linux-gnu)
25
+ ffi (1.17.3-x86-linux-musl)
26
+ ffi (1.17.3-x86_64-darwin)
27
+ ffi (1.17.3-x86_64-linux-gnu)
28
+ ffi (1.17.3-x86_64-linux-musl)
29
+ gssapi (1.3.1)
30
+ ffi (>= 1.0.1)
31
+ json (2.19.1)
32
+ json-schema (6.2.0)
33
+ addressable (~> 2.8)
34
+ bigdecimal (>= 3.1, < 5)
35
+ language_server-protocol (3.17.0.5)
36
+ lint_roller (1.1.0)
37
+ mcp (0.8.0)
38
+ json-schema (>= 4.1)
39
+ net-ldap (0.20.0)
40
+ base64
41
+ ostruct
42
+ ostruct (0.6.3)
43
+ parallel (1.27.0)
44
+ parser (3.3.10.2)
45
+ ast (~> 2.4.1)
46
+ racc
47
+ prism (1.9.0)
48
+ public_suffix (7.0.5)
49
+ racc (1.8.1)
50
+ rainbow (3.1.1)
51
+ regexp_parser (2.11.3)
52
+ rspec (3.13.2)
53
+ rspec-core (~> 3.13.0)
54
+ rspec-expectations (~> 3.13.0)
55
+ rspec-mocks (~> 3.13.0)
56
+ rspec-core (3.13.6)
57
+ rspec-support (~> 3.13.0)
58
+ rspec-expectations (3.13.5)
59
+ diff-lcs (>= 1.2.0, < 2.0)
60
+ rspec-support (~> 3.13.0)
61
+ rspec-mocks (3.13.8)
62
+ diff-lcs (>= 1.2.0, < 2.0)
63
+ rspec-support (~> 3.13.0)
64
+ rspec-support (3.13.7)
65
+ rubocop (1.85.1)
66
+ json (~> 2.3)
67
+ language_server-protocol (~> 3.17.0.2)
68
+ lint_roller (~> 1.1.0)
69
+ mcp (~> 0.6)
70
+ parallel (~> 1.10)
71
+ parser (>= 3.3.0.2)
72
+ rainbow (>= 2.2.2, < 4.0)
73
+ regexp_parser (>= 2.9.3, < 3.0)
74
+ rubocop-ast (>= 1.49.0, < 2.0)
75
+ ruby-progressbar (~> 1.7)
76
+ unicode-display_width (>= 2.4.0, < 4.0)
77
+ rubocop-ast (1.49.1)
78
+ parser (>= 3.3.7.2)
79
+ prism (~> 1.7)
80
+ ruby-progressbar (1.13.0)
81
+ simplecov (0.22.0)
82
+ docile (~> 1.1)
83
+ simplecov-html (~> 0.11)
84
+ simplecov_json_formatter (~> 0.1)
85
+ simplecov-html (0.13.2)
86
+ simplecov_json_formatter (0.1.4)
87
+ unicode-display_width (3.2.0)
88
+ unicode-emoji (~> 4.1)
89
+ unicode-emoji (4.2.0)
90
+
91
+ PLATFORMS
92
+ aarch64-linux-gnu
93
+ aarch64-linux-musl
94
+ arm-linux-gnu
95
+ arm-linux-musl
96
+ arm64-darwin
97
+ ruby
98
+ x86-linux-gnu
99
+ x86-linux-musl
100
+ x86_64-darwin
101
+ x86_64-linux-gnu
102
+ x86_64-linux-musl
103
+
104
+ DEPENDENCIES
105
+ lex-kerberos!
106
+ rspec (~> 3.13)
107
+ rubocop (~> 1.75)
108
+ simplecov (~> 0.22)
109
+
110
+ CHECKSUMS
111
+ addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485
112
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
113
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
114
+ bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
115
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
116
+ docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
117
+ ffi (1.17.3) sha256=0e9f39f7bb3934f77ad6feab49662be77e87eedcdeb2a3f5c0234c2938563d4c
118
+ ffi (1.17.3-aarch64-linux-gnu) sha256=28ad573df26560f0aedd8a90c3371279a0b2bd0b4e834b16a2baa10bd7a97068
119
+ ffi (1.17.3-aarch64-linux-musl) sha256=020b33b76775b1abacc3b7d86b287cef3251f66d747092deec592c7f5df764b2
120
+ ffi (1.17.3-arm-linux-gnu) sha256=5bd4cea83b68b5ec0037f99c57d5ce2dd5aa438f35decc5ef68a7d085c785668
121
+ ffi (1.17.3-arm-linux-musl) sha256=0d7626bb96265f9af78afa33e267d71cfef9d9a8eb8f5525344f8da6c7d76053
122
+ ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f
123
+ ffi (1.17.3-x86-linux-gnu) sha256=868a88fcaf5186c3a46b7c7c2b2c34550e1e61a405670ab23f5b6c9971529089
124
+ ffi (1.17.3-x86-linux-musl) sha256=f0286aa6ef40605cf586e61406c446de34397b85dbb08cc99fdaddaef8343945
125
+ ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
126
+ ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
127
+ ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
128
+ gssapi (1.3.1) sha256=c51cf30842ee39bd93ce7fc33e20405ff8a04cda9dec6092071b61258284aee1
129
+ json (2.19.1) sha256=dd94fdc59e48bff85913829a32350b3148156bc4fd2a95a2568a78b11344082d
130
+ json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666
131
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
132
+ lex-kerberos (0.1.0)
133
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
134
+ mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb
135
+ net-ldap (0.20.0) sha256=b2080b350753a9ac4930869ded8e61a1d2151c01e03b0bf07b4675cbd9ce5372
136
+ ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
137
+ parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
138
+ parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
139
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
140
+ public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
141
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
142
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
143
+ regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
144
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
145
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
146
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
147
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
148
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
149
+ rubocop (1.85.1) sha256=3dbcf9e961baa4c376eeeb2a03913dca5e3987033b04d38fa538aa1e7406cc77
150
+ rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
151
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
152
+ simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
153
+ simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
154
+ simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
155
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
156
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
157
+
158
+ BUNDLED WITH
159
+ 4.0.8
data/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # lex-kerberos
2
+
3
+ Kerberos/SPNEGO authentication integration for [LegionIO](https://github.com/LegionIO/LegionIO). Validates SPNEGO tokens via GSSAPI, resolves LDAP group membership, and manages keytab files with Vault-primary, file-fallback sourcing.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem 'lex-kerberos'
9
+ ```
10
+
11
+ Or install directly:
12
+
13
+ ```bash
14
+ gem install lex-kerberos
15
+ ```
16
+
17
+ ## Prerequisites
18
+
19
+ - MIT Kerberos libraries (`krb5`) installed on the host (`brew install krb5` / `apt install libkrb5-dev`)
20
+ - A keytab file for the service principal (see [Keytab Provisioning](#keytab-provisioning))
21
+ - Active Directory / Kerberos realm reachable from the host
22
+ - For LDAP group resolution: an LDAP bind account with read access to the directory
23
+
24
+ ## Configuration
25
+
26
+ Settings live under `Legion::Settings[:kerberos]`:
27
+
28
+ ```json
29
+ {
30
+ "kerberos": {
31
+ "enabled": true,
32
+ "realm": "EXAMPLE.COM",
33
+ "service_principal": "HTTP/myapp.example.com",
34
+ "keytab": [
35
+ "vault://secret/kerberos/keytab#data",
36
+ "/etc/legion/krb5.keytab"
37
+ ],
38
+ "mutual_auth": true,
39
+ "ldap": {
40
+ "host": "dc.example.com",
41
+ "port": 636,
42
+ "encryption": "simple_tls",
43
+ "base_dn": "DC=example,DC=com",
44
+ "bind_dn": "CN=svc-legion,OU=Service Accounts,DC=example,DC=com",
45
+ "bind_password": "vault://secret/kerberos/ldap_bind#password",
46
+ "user_filter": "(sAMAccountName=%<username>s)",
47
+ "group_attribute": "memberOf"
48
+ },
49
+ "role_map": {},
50
+ "fallback": "entra",
51
+ "cache_groups_ttl": 300
52
+ }
53
+ }
54
+ ```
55
+
56
+ The `keytab` value is an array of sources tried in order. Each source can be:
57
+ - A `vault://` URI resolved via `Legion::Settings::Resolver`
58
+ - An absolute file path (used as-is if the file exists)
59
+ - A Base64-encoded keytab blob written to `~/.legionio/kerberos/legion.keytab`
60
+
61
+ ## Standalone Usage
62
+
63
+ Use the gem outside the LegionIO framework:
64
+
65
+ ```ruby
66
+ require 'legion/extensions/kerberos'
67
+
68
+ client = Legion::Extensions::Kerberos::Client.new(
69
+ realm: 'EXAMPLE.COM',
70
+ service_principal: 'HTTP/myapp.example.com',
71
+ keytab: ['/etc/legion/krb5.keytab'],
72
+ ldap: {
73
+ host: 'dc.example.com',
74
+ port: 636,
75
+ encryption: :simple_tls,
76
+ base_dn: 'DC=example,DC=com',
77
+ bind_dn: 'CN=svc-legion,OU=Service Accounts,DC=example,DC=com',
78
+ bind_password: 'password'
79
+ }
80
+ )
81
+
82
+ # Validate a SPNEGO token from an HTTP Negotiate header
83
+ authorization_header = request.env['HTTP_AUTHORIZATION']
84
+ token = authorization_header.delete_prefix('Negotiate ')
85
+ result = client.authenticate(token: token)
86
+ # => { success: true, principal: "user@EXAMPLE.COM", username: "user",
87
+ # realm: "EXAMPLE.COM", output_token: "...", ... }
88
+
89
+ # Resolve LDAP groups for a username
90
+ groups = client.resolve_groups(username: 'user')
91
+ # => { success: true, groups: ["CN=Domain Users,..."], username: "user" }
92
+ ```
93
+
94
+ ### Using helpers directly
95
+
96
+ ```ruby
97
+ helper = Object.new.extend(Legion::Extensions::Kerberos::Helpers::Spnego)
98
+ result = helper.accept_spnego_token(
99
+ token: token,
100
+ keytab: '/etc/legion/krb5.keytab',
101
+ service_principal: 'HTTP/myapp.example.com'
102
+ )
103
+
104
+ keytab_helper = Object.new.extend(Legion::Extensions::Kerberos::Helpers::Keytab)
105
+ kt = keytab_helper.resolve_keytab(sources: ['/etc/legion/krb5.keytab'])
106
+ # => { success: true, path: "/etc/legion/krb5.keytab", source: :file }
107
+ ```
108
+
109
+ ## CLI Usage
110
+
111
+ When running within LegionIO with the `legion-crypt` Vault Kerberos auth integration:
112
+
113
+ ```bash
114
+ legion auth kerberos
115
+ ```
116
+
117
+ This uses the configured service principal and keytab to authenticate via Vault's Kerberos auth method.
118
+
119
+ ## API Usage
120
+
121
+ When the LegionIO REST API is running, the Negotiate challenge/response endpoint is available:
122
+
123
+ ```
124
+ GET /api/auth/negotiate
125
+ Authorization: Negotiate <base64-spnego-token>
126
+ ```
127
+
128
+ Successful response:
129
+
130
+ ```json
131
+ {
132
+ "success": true,
133
+ "principal": "user@EXAMPLE.COM",
134
+ "username": "user",
135
+ "realm": "EXAMPLE.COM",
136
+ "groups": ["CN=Domain Users,DC=example,DC=com"],
137
+ "auth_method": "kerberos"
138
+ }
139
+ ```
140
+
141
+ ## Vault Integration
142
+
143
+ Store the keytab as a Base64-encoded secret in Vault:
144
+
145
+ ```bash
146
+ base64 -i /etc/legion/krb5.keytab | vault kv put secret/kerberos keytab=-
147
+ ```
148
+
149
+ Configure lex-kerberos to resolve it:
150
+
151
+ ```json
152
+ {
153
+ "kerberos": {
154
+ "keytab": ["vault://secret/kerberos#keytab", "/etc/legion/krb5.keytab"]
155
+ }
156
+ }
157
+ ```
158
+
159
+ The Vault URI is resolved via `Legion::Settings::Resolver`. The keytab bytes are written to `~/.legionio/kerberos/legion.keytab` with permissions `0600` and used for the current session. The `KeytabRefresh` actor runs hourly to re-fetch the keytab from Vault, ensuring the cached file stays current when Vault secrets are rotated.
160
+
161
+ ## Keytab Provisioning
162
+
163
+ Create a keytab for a Windows AD service principal using `ktpass` on a domain controller:
164
+
165
+ ```cmd
166
+ ktpass -princ HTTP/myapp.example.com@EXAMPLE.COM ^
167
+ -mapuser svc-legion@EXAMPLE.COM ^
168
+ -crypto AES256-SHA1 ^
169
+ -ptype KRB5_NT_PRINCIPAL ^
170
+ -pass * ^
171
+ -out legion.keytab
172
+ ```
173
+
174
+ Verify the keytab:
175
+
176
+ ```bash
177
+ klist -k -t /etc/legion/krb5.keytab
178
+ ```
179
+
180
+ Ensure `/etc/krb5.conf` references the correct realm and KDC:
181
+
182
+ ```ini
183
+ [libdefaults]
184
+ default_realm = EXAMPLE.COM
185
+
186
+ [realms]
187
+ EXAMPLE.COM = {
188
+ kdc = kdc.example.com
189
+ admin_server = kdc.example.com
190
+ }
191
+ ```
192
+
193
+ ## Requirements
194
+
195
+ - Ruby >= 3.4
196
+ - [LegionIO](https://github.com/LegionIO/LegionIO) framework (optional for standalone client usage)
197
+ - `gssapi` ~> 1.3 (MIT Kerberos system libraries required)
198
+ - `net-ldap` ~> 0.19
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/kerberos/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-kerberos'
7
+ spec.version = Legion::Extensions::Kerberos::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Kerberos'
12
+ spec.description = 'Connects LegionIO to Kerberos/SPNEGO authentication and LDAP group resolution'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-kerberos'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-kerberos'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-kerberos'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-kerberos'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-kerberos/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'gssapi', '~> 1.3'
30
+ spec.add_dependency 'net-ldap', '~> 0.19'
31
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every' if defined?(Legion::Extensions::Actors)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Kerberos
8
+ module Actor
9
+ class KeytabRefresh < Legion::Extensions::Actors::Every
10
+ def initialize(**opts)
11
+ return unless enabled?
12
+
13
+ super
14
+ end
15
+
16
+ def time = 3600
17
+ def run_now? = false
18
+ def use_runner? = false
19
+ def check_subtask? = false
20
+ def generate_task? = false
21
+
22
+ def enabled?
23
+ defined?(Legion::Extensions::Kerberos::Helpers::Keytab)
24
+ rescue StandardError
25
+ false
26
+ end
27
+
28
+ def manual
29
+ result = keytab_helper.resolve_keytab(sources: keytab_sources)
30
+ log_result(result)
31
+ rescue StandardError => e
32
+ log_error(e)
33
+ end
34
+
35
+ private
36
+
37
+ def keytab_helper
38
+ Object.new.extend(Legion::Extensions::Kerberos::Helpers::Keytab)
39
+ end
40
+
41
+ def keytab_sources
42
+ return [] unless defined?(Legion::Settings)
43
+
44
+ Legion::Settings.dig(:kerberos, :keytab) || []
45
+ end
46
+
47
+ def log_result(result)
48
+ return unless defined?(Legion::Logging)
49
+
50
+ if result[:success]
51
+ Legion::Logging.debug("KeytabRefresh: refreshed keytab from #{result[:source]}")
52
+ else
53
+ Legion::Logging.warn("KeytabRefresh: #{result[:error]}")
54
+ end
55
+ end
56
+
57
+ def log_error(err)
58
+ Legion::Logging.error("KeytabRefresh: #{err.message}") if defined?(Legion::Logging)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/kerberos/helpers/client'
4
+ require 'legion/extensions/kerberos/helpers/spnego'
5
+ require 'legion/extensions/kerberos/helpers/ldap'
6
+ require 'legion/extensions/kerberos/helpers/keytab'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Kerberos
11
+ class Client
12
+ include Helpers::Client
13
+ include Helpers::Spnego
14
+ include Helpers::Ldap
15
+ include Helpers::Keytab
16
+
17
+ attr_reader :realm, :service_principal, :keytab_sources, :opts
18
+
19
+ def initialize(realm: nil, service_principal: nil, keytab: nil, **opts)
20
+ defaults = settings[:kerberos]
21
+ @realm = realm || defaults[:realm]
22
+ @service_principal = service_principal || defaults[:service_principal]
23
+ @keytab_sources = keytab || defaults[:keytab]
24
+ @opts = opts
25
+ end
26
+
27
+ def authenticate(token:)
28
+ kt = resolve_keytab(sources: @keytab_sources)
29
+ return kt unless kt[:success]
30
+
31
+ accept_spnego_token(
32
+ token: token,
33
+ keytab: kt[:path],
34
+ service_principal: @service_principal
35
+ )
36
+ end
37
+
38
+ def resolve_groups(username:)
39
+ ldap_opts = @opts[:ldap] || settings[:kerberos][:ldap] || {}
40
+ lookup_groups(username: username, **ldap_opts)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Kerberos
6
+ module Helpers
7
+ module Client
8
+ DEFAULTS = {
9
+ kerberos: {
10
+ enabled: true,
11
+ realm: 'MS.DS.UHC.COM',
12
+ service_principal: 'HTTP/legion.uhg.com',
13
+ keytab: ['/etc/legion/krb5.keytab'],
14
+ mutual_auth: true,
15
+ ldap: {
16
+ port: 636, encryption: :simple_tls,
17
+ group_attribute: 'memberOf',
18
+ user_filter: '(sAMAccountName=%<username>s)'
19
+ },
20
+ role_map: {},
21
+ fallback: :entra,
22
+ cache_groups_ttl: 300
23
+ }
24
+ }.freeze
25
+
26
+ def settings
27
+ if defined?(Legion::Settings) && Legion::Settings.respond_to?(:dig)
28
+ krb = Legion::Settings[:kerberos] || {}
29
+ { kerberos: DEFAULTS[:kerberos].merge(krb) }
30
+ else
31
+ DEFAULTS
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'fileutils'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Kerberos
9
+ module Helpers
10
+ module Keytab
11
+ DEFAULT_CACHE_DIR = File.join(Dir.home, '.legionio', 'kerberos')
12
+
13
+ def resolve_keytab(sources:, cache_dir: DEFAULT_CACHE_DIR, **)
14
+ Array(sources).each do |source|
15
+ next if source.nil? || source.to_s.empty?
16
+
17
+ result = resolve_source(source, cache_dir)
18
+ return result if result
19
+ end
20
+
21
+ { success: false, error: 'no valid keytab source found' }
22
+ end
23
+
24
+ private
25
+
26
+ def resolve_source(source, cache_dir)
27
+ return resolve_vault_source(source, cache_dir) if vault_uri?(source)
28
+ return { success: true, path: source, source: :file } if File.exist?(source)
29
+ return write_keytab_cache(source, cache_dir) if base64?(source)
30
+
31
+ nil
32
+ end
33
+
34
+ def vault_uri?(source)
35
+ source.start_with?('vault://') &&
36
+ defined?(Legion::Settings) &&
37
+ defined?(Legion::Settings::Resolver)
38
+ end
39
+
40
+ def resolve_vault_source(source, cache_dir)
41
+ resolved = Legion::Settings::Resolver.resolve_value(source)
42
+ return nil unless resolved
43
+
44
+ write_keytab_cache(resolved, cache_dir)
45
+ end
46
+
47
+ def base64?(str)
48
+ str.match?(%r{\A[A-Za-z0-9+/\n]+=*\n?\z}) && str.length > 20
49
+ end
50
+
51
+ def write_keytab_cache(base64_data, cache_dir)
52
+ FileUtils.mkdir_p(cache_dir)
53
+ path = File.join(cache_dir, 'legion.keytab')
54
+ File.binwrite(path, Base64.strict_decode64(base64_data.strip))
55
+ File.chmod(0o600, path)
56
+ { success: true, path: path, source: :base64 }
57
+ rescue ArgumentError => e
58
+ { success: false, error: "keytab decode failed: #{e.message}" }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net-ldap'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Kerberos
8
+ module Helpers
9
+ module Ldap
10
+ def lookup_groups(username:, host:, base_dn:, bind_dn:, bind_password:,
11
+ port: 636, encryption: :simple_tls,
12
+ user_filter: '(sAMAccountName=%<username>s)',
13
+ group_attribute: 'memberOf', **)
14
+ ldap = build_ldap_client(host: host, port: port, encryption: encryption,
15
+ bind_dn: bind_dn, bind_password: bind_password)
16
+ return { success: false, error: 'LDAP bind failed' } unless ldap.bind
17
+
18
+ groups = search_groups(ldap: ldap, username: username, base_dn: base_dn,
19
+ user_filter: user_filter, group_attribute: group_attribute)
20
+ { success: true, groups: groups, username: username }
21
+ rescue Net::LDAP::Error => e
22
+ { success: false, error: "LDAP error: #{e.message}" }
23
+ end
24
+
25
+ private
26
+
27
+ def build_ldap_client(host:, port:, encryption:, bind_dn:, bind_password:)
28
+ Net::LDAP.new(
29
+ host: host, port: port,
30
+ encryption: { method: encryption },
31
+ auth: { method: :simple, username: bind_dn, password: bind_password }
32
+ )
33
+ end
34
+
35
+ def search_groups(ldap:, username:, base_dn:, user_filter:, group_attribute:)
36
+ filter = Net::LDAP::Filter.construct(format(user_filter, username: username))
37
+ groups = []
38
+ ldap.search(base: base_dn, filter: filter, attributes: [group_attribute]) do |entry|
39
+ groups.concat(Array(entry[group_attribute]).map(&:to_s))
40
+ end
41
+ groups
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gssapi'
4
+ require 'base64'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Kerberos
9
+ module Helpers
10
+ module Spnego
11
+ def accept_spnego_token(token:, keytab:, service_principal:, _mutual_auth: true, **)
12
+ ENV['KRB5_KTNAME'] = keytab if keytab
13
+
14
+ input_bytes = Base64.strict_decode64(token)
15
+ principal, output_bytes = negotiate(input_bytes, service_principal)
16
+ build_token_result(principal, output_bytes)
17
+ rescue GSSAPI::GssApiError => e
18
+ { success: false, error: e.message }
19
+ rescue ArgumentError => e
20
+ { success: false, error: "token decode failed: #{e.message}" }
21
+ end
22
+
23
+ def extract_username(principal)
24
+ principal.split('@', 2).first
25
+ end
26
+
27
+ def extract_realm(principal)
28
+ parts = principal.split('@', 2)
29
+ parts.length > 1 ? parts.last : nil
30
+ end
31
+
32
+ private
33
+
34
+ def negotiate(input_bytes, service_principal)
35
+ service, host = service_principal.split('/', 2)
36
+ ctx = GSSAPI::Simple.new(host, service)
37
+ ctx.acquire_credentials
38
+ output_bytes = ctx.accept_context(input_bytes)
39
+ [ctx.display_name, output_bytes]
40
+ end
41
+
42
+ def build_token_result(principal, output_bytes)
43
+ {
44
+ success: true,
45
+ principal: principal,
46
+ output_token: output_bytes ? Base64.strict_encode64(output_bytes) : nil,
47
+ username: extract_username(principal),
48
+ realm: extract_realm(principal)
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/kerberos/helpers/spnego'
4
+ require 'legion/extensions/kerberos/helpers/ldap'
5
+ require 'legion/extensions/kerberos/helpers/keytab'
6
+ require 'legion/extensions/kerberos/helpers/client'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Kerberos
11
+ module Runners
12
+ module Authenticate
13
+ include Helpers::Spnego
14
+ include Helpers::Ldap
15
+ include Helpers::Keytab
16
+ include Helpers::Client
17
+
18
+ def validate_spnego(token:, keytab: nil, service_principal: nil, ldap: nil, **)
19
+ s = settings[:kerberos]
20
+ keytab ||= s[:keytab]
21
+ service_principal ||= s[:service_principal]
22
+
23
+ kt = resolve_keytab(sources: keytab)
24
+ return { result: kt } unless kt[:success]
25
+
26
+ spnego = accept_spnego_token(token: token, keytab: kt[:path],
27
+ service_principal: service_principal)
28
+ return { result: spnego } unless spnego[:success]
29
+
30
+ groups, ldap_error = resolve_groups(ldap: ldap, cfg: s, username: spnego[:username])
31
+
32
+ { result: build_result(spnego: spnego, groups: groups, ldap_error: ldap_error) }
33
+ end
34
+
35
+ private
36
+
37
+ def resolve_groups(ldap:, cfg:, username:)
38
+ ldap_opts = ldap || cfg[:ldap] || {}
39
+ if ldap_opts[:host]
40
+ groups_result = lookup_groups(username: username, **ldap_opts)
41
+ groups = groups_result[:success] ? groups_result[:groups] : []
42
+ ldap_error = groups_result[:success] ? nil : groups_result[:error]
43
+ else
44
+ groups = []
45
+ ldap_error = nil
46
+ end
47
+ [groups, ldap_error]
48
+ end
49
+
50
+ def build_result(spnego:, groups:, ldap_error:)
51
+ {
52
+ success: true,
53
+ principal: spnego[:principal],
54
+ username: spnego[:username],
55
+ realm: spnego[:realm],
56
+ groups: groups,
57
+ output_token: spnego[:output_token],
58
+ auth_method: 'kerberos',
59
+ ldap_error: ldap_error
60
+ }.compact
61
+ end
62
+
63
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
64
+ Legion::Extensions::Helpers.const_defined?(:Lex)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Kerberos
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/kerberos/version'
4
+ require 'legion/extensions/kerberos/helpers/spnego'
5
+ require 'legion/extensions/kerberos/helpers/ldap'
6
+ require 'legion/extensions/kerberos/helpers/keytab'
7
+ require 'legion/extensions/kerberos/helpers/client'
8
+ require 'legion/extensions/kerberos/runners/authenticate'
9
+ require 'legion/extensions/kerberos/actors/keytab_refresh' if defined?(Legion::Extensions::Actors)
10
+ require 'legion/extensions/kerberos/client'
11
+
12
+ module Legion
13
+ module Extensions
14
+ module Kerberos
15
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-kerberos
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: gssapi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: net-ldap
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.19'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.19'
40
+ description: Connects LegionIO to Kerberos/SPNEGO authentication and LDAP group resolution
41
+ email:
42
+ - matthewdiverson@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - ".github/workflows/ci.yml"
48
+ - ".gitignore"
49
+ - ".rubocop.yml"
50
+ - CHANGELOG.md
51
+ - CLAUDE.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - README.md
55
+ - lex-kerberos.gemspec
56
+ - lib/legion/extensions/kerberos.rb
57
+ - lib/legion/extensions/kerberos/actors/keytab_refresh.rb
58
+ - lib/legion/extensions/kerberos/client.rb
59
+ - lib/legion/extensions/kerberos/helpers/client.rb
60
+ - lib/legion/extensions/kerberos/helpers/keytab.rb
61
+ - lib/legion/extensions/kerberos/helpers/ldap.rb
62
+ - lib/legion/extensions/kerberos/helpers/spnego.rb
63
+ - lib/legion/extensions/kerberos/runners/authenticate.rb
64
+ - lib/legion/extensions/kerberos/version.rb
65
+ homepage: https://github.com/LegionIO/lex-kerberos
66
+ licenses:
67
+ - MIT
68
+ metadata:
69
+ homepage_uri: https://github.com/LegionIO/lex-kerberos
70
+ source_code_uri: https://github.com/LegionIO/lex-kerberos
71
+ documentation_uri: https://github.com/LegionIO/lex-kerberos
72
+ changelog_uri: https://github.com/LegionIO/lex-kerberos
73
+ bug_tracker_uri: https://github.com/LegionIO/lex-kerberos/issues
74
+ rubygems_mfa_required: 'true'
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.6.9
90
+ specification_version: 4
91
+ summary: LEX Kerberos
92
+ test_files: []