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 +7 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +14 -0
- data/CLAUDE.md +122 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +159 -0
- data/README.md +202 -0
- data/lex-kerberos.gemspec +31 -0
- data/lib/legion/extensions/kerberos/actors/keytab_refresh.rb +64 -0
- data/lib/legion/extensions/kerberos/client.rb +45 -0
- data/lib/legion/extensions/kerberos/helpers/client.rb +38 -0
- data/lib/legion/extensions/kerberos/helpers/keytab.rb +64 -0
- data/lib/legion/extensions/kerberos/helpers/ldap.rb +47 -0
- data/lib/legion/extensions/kerberos/helpers/spnego.rb +55 -0
- data/lib/legion/extensions/kerberos/runners/authenticate.rb +69 -0
- data/lib/legion/extensions/kerberos/version.rb +9 -0
- data/lib/legion/extensions/kerberos.rb +18 -0
- metadata +92 -0
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
data/.rubocop.yml
ADDED
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
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,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: []
|