Skip to content

Commit 12d0b17

Browse files
authored
fix: KVNO lookup from Active Directory (#571)
* working lookup * throw error when kvno not found * use a function to derive data from principal * changelog * log/comment cleanup * linting
1 parent 0f2b84a commit 12d0b17

File tree

2 files changed

+108
-28
lines changed

2 files changed

+108
-28
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ All notable changes to this project will be documented in this file.
2626
- Underscores are now allowed in Kerberos principal names ([#563]).
2727
- The issuer in generated TLS certificates is set to the subject of the issuing
2828
certificate ([#566]).
29+
- Lookup KVNO from Active Directory rather than hard coding it ([#571]).
2930

3031
[#528]: https://github.com/stackabletech/secret-operator/pull/528
3132
[#544]: https://github.com/stackabletech/secret-operator/pull/544
@@ -36,6 +37,7 @@ All notable changes to this project will be documented in this file.
3637
[#564]: https://github.com/stackabletech/secret-operator/pull/564
3738
[#566]: https://github.com/stackabletech/secret-operator/pull/566
3839
[#569]: https://github.com/stackabletech/secret-operator/pull/569
40+
[#571]: https://github.com/stackabletech/secret-operator/pull/571
3941

4042
## [24.11.1] - 2025-01-10
4143

Diff for: rust/krb5-provision-keytab/src/active_directory.rs

+106-28
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66

77
use byteorder::{LittleEndian, WriteBytesExt};
88
use krb5::{Keyblock, Keytab, KrbContext, Principal, PrincipalUnparseOptions};
9-
use ldap3::{Ldap, LdapConnAsync, LdapConnSettings};
9+
use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
1010
use rand::{seq::IndexedRandom, CryptoRng};
1111
use snafu::{OptionExt, ResultExt, Snafu};
1212
use stackable_krb5_provision_keytab::ActiveDirectorySamAccountNameRules;
@@ -70,6 +70,15 @@ pub enum Error {
7070

7171
#[snafu(display("configured samAccountName prefix is longer than the requested length"))]
7272
SamAccountNamePrefixLongerThanRequestedLength,
73+
74+
#[snafu(display("failed to execute LDAP search"))]
75+
SearchLdap { source: ldap3::LdapError },
76+
77+
#[snafu(display("failed to extract successful LDAP search"))]
78+
SearchLdapSuccess { source: ldap3::LdapError },
79+
80+
#[snafu(display("the user did not have an associated kvno"))]
81+
KvnoNotFound,
7382
}
7483
pub type Result<T, E = Error> = std::result::Result<T, E>;
7584

@@ -136,9 +145,7 @@ impl<'a> AdAdmin<'a> {
136145
principal: &Principal<'_>,
137146
kt: &mut Keytab<'_>,
138147
) -> Result<()> {
139-
let princ_name = principal
140-
.unparse(PrincipalUnparseOptions::default())
141-
.context(UnparsePrincipalSnafu)?;
148+
let princ_name = get_principal_data(principal)?.princ_name;
142149
let password_cache_key = princ_name.replace(['/', '@'], "__");
143150
let password = self
144151
.password_cache
@@ -162,18 +169,27 @@ impl<'a> AdAdmin<'a> {
162169
// FIXME: What about cases where ldap.add() succeeds but not the cache write?
163170
.context(PasswordCacheSnafu)??;
164171
let password_c = CString::new(password).context(DecodePasswordSnafu)?;
165-
principal
166-
.default_salt()
167-
.and_then(|salt| {
168-
Keyblock::from_password(
169-
self.krb,
170-
krb5::enctype::AES256_CTS_HMAC_SHA1_96,
171-
&password_c,
172-
&salt,
173-
)
174-
})
175-
.and_then(|key| kt.add(principal, 0, &key.as_ref()))
176-
.context(AddToKeytabSnafu)?;
172+
173+
let kvno = get_user_kvno(&mut self.ldap, principal, &self.user_distinguished_name).await?;
174+
if let Some(kvno) = kvno {
175+
principal
176+
.default_salt()
177+
.and_then(|salt| {
178+
Keyblock::from_password(
179+
self.krb,
180+
krb5::enctype::AES256_CTS_HMAC_SHA1_96,
181+
&password_c,
182+
&salt,
183+
)
184+
})
185+
.and_then(|key| kt.add(principal, kvno, &key.as_ref()))
186+
.context(AddToKeytabSnafu)?;
187+
} else {
188+
// If we can't detect the kvno then some applications may not
189+
// authenticate if the keytab/kvno does not match the kvno of the
190+
// ticket from the KDC. So always throw an exception.
191+
return Err(Error::KvnoNotFound);
192+
}
177193
Ok(())
178194
}
179195
}
@@ -262,18 +278,12 @@ async fn create_ad_user(
262278
const AD_ENCTYPE_AES256_HMAC_SHA1: u32 = 0x10;
263279

264280
tracing::info!("creating principal");
265-
let princ_name = principal
266-
.unparse(PrincipalUnparseOptions::default())
267-
.context(UnparsePrincipalSnafu)?;
268-
let princ_name_realmless = principal
269-
.unparse(PrincipalUnparseOptions {
270-
realm: krb5::PrincipalRealmDisplayMode::Never,
271-
..Default::default()
272-
})
273-
.context(UnparsePrincipalSnafu)?;
274-
let principal_cn = ldap3::dn_escape(&princ_name);
275-
// FIXME: AD restricts RDNs to 64 characters
276-
let principal_cn = principal_cn.get(..64).unwrap_or(&*principal_cn);
281+
282+
let principal_data = get_principal_data(principal)?;
283+
let princ_name = principal_data.princ_name;
284+
let principal_cn = principal_data.principal_cn;
285+
let princ_name_realmless = principal_data.princ_name_realmless;
286+
277287
let sam_account_name = generate_sam_account_name
278288
.map(|sam_rules| {
279289
let mut name = sam_rules.prefix.clone();
@@ -350,3 +360,71 @@ async fn create_ad_user(
350360
};
351361
Ok(())
352362
}
363+
364+
pub struct PrincipalData {
365+
princ_name: String,
366+
principal_cn: String,
367+
princ_name_realmless: String,
368+
}
369+
370+
fn get_principal_data(principal: &Principal<'_>) -> Result<PrincipalData> {
371+
let princ_name = principal
372+
.unparse(PrincipalUnparseOptions::default())
373+
.context(UnparsePrincipalSnafu)?;
374+
let principal_cn = ldap3::dn_escape(&princ_name);
375+
// AD restricts RDNs to 64 characters
376+
let principal_cn = principal_cn.get(..64).unwrap_or(&*principal_cn).to_string();
377+
let princ_name_realmless = principal
378+
.unparse(PrincipalUnparseOptions {
379+
realm: krb5::PrincipalRealmDisplayMode::Never,
380+
..Default::default()
381+
})
382+
.context(UnparsePrincipalSnafu)?;
383+
Ok(PrincipalData {
384+
princ_name,
385+
principal_cn,
386+
princ_name_realmless,
387+
})
388+
}
389+
390+
#[tracing::instrument(skip(ldap), fields(%principal, %user_dn_base))]
391+
async fn get_user_kvno(
392+
ldap: &mut Ldap,
393+
principal: &Principal<'_>,
394+
user_dn_base: &str,
395+
) -> Result<Option<u32>> {
396+
let principal_cn = get_principal_data(principal)?.principal_cn;
397+
let distinguished_name = &format!("CN={principal_cn},{user_dn_base}");
398+
tracing::info!("searching for kvno using DN {distinguished_name}");
399+
400+
// Perform search with KVNO attribute
401+
let (search_results, _) = ldap
402+
.search(
403+
distinguished_name,
404+
Scope::Base,
405+
"(objectClass=user)",
406+
vec!["msDS-KeyVersionNumber"],
407+
)
408+
.await
409+
.context(SearchLdapSnafu)?
410+
.success()
411+
.context(SearchLdapSuccessSnafu)?;
412+
413+
let mut kvno = None;
414+
415+
// Extract KVNO from first result
416+
if let Some(entry) = search_results.into_iter().next() {
417+
let entry = SearchEntry::construct(entry);
418+
tracing::debug!("detected search result entry {:?}", entry);
419+
kvno = entry
420+
.attrs
421+
.get("msDS-KeyVersionNumber")
422+
.and_then(|v| v.first())
423+
.and_then(|s| s.parse::<u32>().ok());
424+
tracing::debug!("detected kvno {:?} for DN {distinguished_name}", kvno);
425+
} else {
426+
tracing::info!("no kvno detected for DN {distinguished_name}");
427+
}
428+
429+
Ok(kvno)
430+
}

0 commit comments

Comments
 (0)