-
Notifications
You must be signed in to change notification settings - Fork 7
/
KerberosWorker.cs
155 lines (141 loc) · 5.39 KB
/
KerberosWorker.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
using Kerberos.NET.Client;
using Kerberos.NET.Crypto;
using Kerberos.NET.Entities;
using KerberosSidecar.HealthChecks;
using KerberosSidecar.Spn;
using Microsoft.Extensions.Options;
namespace KerberosSidecar;
public class KerberosWorker : BackgroundService
{
private readonly IOptionsMonitor<KerberosOptions> _options;
private readonly ILogger<KerberosWorker> _logger;
private readonly KerberosCredentialFactory _credentialFactory;
private readonly SpnProvider _spnProvider;
private readonly TgtHealthCheck _tgtHealthCheck;
private readonly CancellationToken _cancellationToken;
public KerberosWorker(
IOptionsMonitor<KerberosOptions> options,
KerberosCredentialFactory credentialFactory,
SpnProvider spnProvider,
TgtHealthCheck tgtHealthCheck,
IHostApplicationLifetime lifetime,
ILogger<KerberosWorker> logger)
{
_options = options;
_logger = logger;
_credentialFactory = credentialFactory;
_spnProvider = spnProvider;
_tgtHealthCheck = tgtHealthCheck;
_cancellationToken = lifetime.ApplicationStopping;
}
private void OnOptionsChange(KerberosOptions options)
{
Task.Run(async () =>
{
try
{
await SetupMitKerberos();
}
catch (Exception e)
{
_logger.LogError(e, "Error trying to authenticate after options change");
}
}, _cancellationToken);
}
private async Task SetupMitKerberos()
{
try
{
await CreateMitKerberosKrb5Config();
await CreateMitKerberosKeytab();
await EnsureTgt();
await _spnProvider.EnsureSpns(_cancellationToken);
_tgtHealthCheck.LastException = null;
}
catch (Exception e)
{
_logger.LogError(e, "Error initializing MIT Kerberos");
_tgtHealthCheck.LastException = e;
}
finally
{
_tgtHealthCheck.IsReady = true;
}
}
private async Task CreateMitKerberosKeytab()
{
var keytab = await GenerateKeytab();
await using var fs = new FileStream(_options.CurrentValue.KeytabFile, FileMode.OpenOrCreate);
await using var bw = new BinaryWriter(fs);
keytab.Write(bw);
}
private async Task CreateMitKerberosKrb5Config()
{
if (_options.CurrentValue.GenerateKrb5)
{
await File.WriteAllTextAsync(_options.CurrentValue.Kerb5ConfigFile, _options.CurrentValue.KerberosClient.Configuration.Serialize(), _cancellationToken);
}
}
/// <summary>
/// Authenticates the principal and populates ticket cache
/// </summary>
private async Task EnsureTgt()
{
var credentials = await _credentialFactory.Get(_options.CurrentValue, _cancellationToken);
await _options.CurrentValue.KerberosClient.Authenticate(credentials);
_tgtHealthCheck.LastException = null;
_logger.LogInformation("Service authenticated successfully as '{Principal}'", credentials.UserName);
}
/// <summary>
/// Generates keytab from user credentials for each SPN associated with the app
/// </summary>
/// <remarks>
/// Keytab is made up of Kerberos key entries. Each entry is made up of:
/// - Kerberos principal name, which is service account + all aliases (SPNs)
/// - Encryption key that derived from password
/// - Salt to go with the key
/// </remarks>
private async Task<KeyTable> GenerateKeytab()
{
var credentials = await _credentialFactory.Get(_options.CurrentValue, _cancellationToken);
var spns = await _spnProvider.GetSpnsForAppRoutes(_cancellationToken);
var realm = credentials.Domain;
List<KerberosKey> kerberosKeys = new();
foreach (var spn in spns)
{
foreach (var (encryptionType, salt) in credentials.Salts)
{
var key = new KerberosKey(_options.CurrentValue.Password, new PrincipalName(PrincipalNameType.NT_SRV_HST, realm, new[] { spn }), salt: salt, etype: encryptionType);
kerberosKeys.Add(key);
}
}
foreach (var (encryptionType, salt) in credentials.Salts)
{
var key = new KerberosKey(_options.CurrentValue.Password, new PrincipalName(PrincipalNameType.NT_PRINCIPAL, realm, new[] { $"{credentials.UserName}@{credentials.Domain.ToUpper()}" }), salt: salt, etype: encryptionType);
kerberosKeys.Add(key);
}
var keyTable = new KeyTable(kerberosKeys.ToArray());
return keyTable;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_options.OnChange(OnOptionsChange);
await SetupMitKerberos();
while (!stoppingToken.IsCancellationRequested)
{
await RefreshTicketIfExpiring();
await Task.Delay(1000, stoppingToken);
}
}
private async Task RefreshTicketIfExpiring()
{
var ticketCache = (Krb5TicketCache)_options.CurrentValue.KerberosClient.Cache;
var tgt = ticketCache.Krb5Cache.Credentials.FirstOrDefault(x => x.Server.Name.Contains("krbtgt"));
if(tgt == null)
return;
if (DateTimeOffset.UtcNow.AddMinutes(15) > tgt.RenewTill)
{
await _options.CurrentValue.KerberosClient.RenewTicket();
}
}
}