Loading plugin/ad/ad.cpp +2 −0 Changes for plugin/ad/ad.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -440,6 +440,8 @@ void ActiveDirectory::loadDomains() { d.dir = std::make_unique<dsdb::Directory>(*bck, d.meta); try { d.dir->provision(_AdminPassword); if (!_DcHost.empty()) d.dir->provisionDc(_DcHost); /* this DC's own account + SPNs */ } catch (std::exception &ex) { std::cerr << "AD: provision '" << e.name << "' failed: " << ex.what() << std::endl; } Loading src/dsdb/directory.h +7 −0 Changes for src/dsdb/directory.h: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -91,6 +91,13 @@ namespace fenrirds { * Administrator without credentials. */ void provision(const std::string &adminPassword = ""); /* create this DC's own computer account (CN=<DC>,OU=Domain * Controllers) with the SPNs a client needs (host/ cifs/ ldap/) * and a random machine password, so the KDC can issue service * tickets for it. dcHost is the DC's FQDN (e.g. dc1.example.com); * idempotent. Does nothing if dcHost is not within this domain. */ void provisionDc(const std::string &dcHost); /* all entries at/under base matching the scope (0 base,1 one,2 sub) */ void search(const Dn &base, int scope, const Filter &filter, const std::function<bool(const Entry &)> &sink); Loading src/dsdb/provision.cpp +80 −0 Changes for src/dsdb/provision.cpp: 80 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -232,5 +232,85 @@ void Directory::provision(const std::string &adminPassword) { } } void Directory::provisionDc(const std::string &dcHost) { std::lock_guard<std::mutex> lk(_Mtx); ensureFresh(); if (dcHost.empty()) return; std::string lowHost = dsdb::lower(dcHost); std::string suffix = "." + dsdb::lower(_Meta.dnsName); if (lowHost.size() <= suffix.size() || lowHost.compare(lowHost.size() - suffix.size(), suffix.size(), suffix) != 0) return; /* this DC is not in this domain */ std::string shortName = dcHost.substr(0, dcHost.find('.')); std::string sam = dsdb::upper(shortName) + "$"; Dn dn = _Meta.baseDn.child("OU", "Domain Controllers").child("CN", shortName); if (findByDnLocked(dn)) return; /* already provisioned */ const Dn &base = _Meta.baseDn; Sid domSid = _Meta.sid; if (domSid.empty()) if (Entry *head = findByDnLocked(base)) domSid = head->objectSid; uint32_t rid = uint32_t(intValue(findByDnLocked(base)->attrs, "@nextRid", RID_FIRST_FREE)); /* bump the pool on the head */ if (Entry *head = findByDnLocked(base)) { AttrMap h = head->attrs; setInt(h, "@nextRid", rid + 1); storeObject(_Backend, head->objectGuid, head->dn, h, objectSecrets(_Backend, head->objectGuid), false); head->attrs = h; } std::string fqdn = dcHost; std::string up = dsdb::upper(shortName); AttrMap a; a["objectClass"] = {"top", "person", "organizationalPerson", "user", "computer"}; a["objectCategory"] = {"CN=Computer,CN=Schema,CN=Configuration," + base.str()}; a["objectSid"] = {domSid.withRid(rid).binary()}; a["sAMAccountName"] = {sam}; setInt(a, "sAMAccountType", SAM_MACHINE_ACCOUNT); setInt(a, "userAccountControl", UAC_SERVER_TRUST | UAC_TRUSTED_FOR_DELEGATION); setInt(a, "primaryGroupID", RID_DOMAIN_CONTROLLERS); setInt(a, "accountExpires", NEVER); a["dNSHostName"] = {fqdn}; setInt(a, "msDS-SupportedEncryptionTypes", 0x1F); /* AES256|AES128|RC4|DES */ a["servicePrincipalName"] = { "HOST/" + fqdn, "HOST/" + up, "ldap/" + fqdn, "ldap/" + up, "ldap/" + fqdn + "/" + _Meta.dnsName, "cifs/" + fqdn, "cifs/" + up, "RestrictedKrbHost/" + fqdn, "RestrictedKrbHost/" + up, "GC/" + fqdn + "/" + _Meta.dnsName, }; a["memberOf"] = {"CN=Domain Controllers,CN=Users," + base.str()}; a["isCriticalSystemObject"] = {"TRUE"}; std::string pw = crypto::base64(crypto::randomBytes(24)); Secrets sec = deriveSecrets(pw, kerberosSalt(PrincipalKind::Computer, _Meta.realm, sam, _Meta.dnsName), 0); setInt(a, "msDS-KeyVersionNumber", sec.kvno); /* inline create (we hold the lock) */ uuid::uuid g; g.generate(); a["name"] = {shortName}; a["cn"] = {shortName}; a["distinguishedName"] = {dn.str()}; setInt(a, "instanceType", 4); a["whenCreated"] = {generalizedTime(unixFromFileTime(fileTimeNow()))}; a["whenChanged"] = a["whenCreated"]; storeObject(_Backend, g, dn, a, sec.serialize(), true); Entry e; e.objectGuid = g; e.dn = dn; e.attrs = a; e.objectSid = domSid.withRid(rid); size_t idx = _Entries.size(); _Entries.push_back(e); addEntryToIndex(_Entries.back(), idx); _Revision = _Backend.getRevesion(); } } } tests/proto/test_directory.cpp +34 −0 Changes for tests/proto/test_directory.cpp: 34 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -121,4 +121,38 @@ TEST(create_modify_delete) { unlink((path + ".lock").c_str()); } TEST(provision_dc_account) { std::string path = "/build/fds-dc-" + std::to_string(getpid()) + ".db"; unlink(path.c_str()); unlink((path + ".lock").c_str()); { AuthBackend backend(fenrirds::File, path, "example.com"); Directory dir(backend, meta()); dir.provision(); dir.provisionDc("dc1.example.com"); /* the DC machine account exists with its SPNs and a key */ auto dc = dir.bySamAccountName("DC1$"); CHECK(dc.has_value()); CHECK(dc->isClass("computer")); CHECK(dc->dn.under(dnFromDomain("example.com").child("OU","Domain Controllers"))); /* the KDC resolves these SPNs to the DC account */ CHECK(dir.bySpn("cifs/dc1.example.com").has_value()); CHECK(dir.bySpn("ldap/dc1.example.com").has_value()); CHECK(dir.bySpn("HOST/dc1.example.com").has_value()); /* and it has Kerberos keys to decrypt service tickets */ Secrets sec = dir.secretsOf(dc->objectGuid); CHECK(!sec.empty()); CHECK(sec.key(fenrirds::crypto::krb5::AES256_CTS_HMAC_SHA1_96) != nullptr); /* idempotent */ dir.provisionDc("dc1.example.com"); int n = 0; Filter f = Filter::equal("sAMAccountName", "DC1$"); dir.search(dnFromDomain("example.com"), 2, f, [&](const Entry&){ ++n; return true; }); CHECK_EQ(n, 1); } unlink(path.c_str()); unlink((path + ".lock").c_str()); } CHECK_MAIN Loading
plugin/ad/ad.cpp +2 −0 Changes for plugin/ad/ad.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -440,6 +440,8 @@ void ActiveDirectory::loadDomains() { d.dir = std::make_unique<dsdb::Directory>(*bck, d.meta); try { d.dir->provision(_AdminPassword); if (!_DcHost.empty()) d.dir->provisionDc(_DcHost); /* this DC's own account + SPNs */ } catch (std::exception &ex) { std::cerr << "AD: provision '" << e.name << "' failed: " << ex.what() << std::endl; } Loading
src/dsdb/directory.h +7 −0 Changes for src/dsdb/directory.h: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -91,6 +91,13 @@ namespace fenrirds { * Administrator without credentials. */ void provision(const std::string &adminPassword = ""); /* create this DC's own computer account (CN=<DC>,OU=Domain * Controllers) with the SPNs a client needs (host/ cifs/ ldap/) * and a random machine password, so the KDC can issue service * tickets for it. dcHost is the DC's FQDN (e.g. dc1.example.com); * idempotent. Does nothing if dcHost is not within this domain. */ void provisionDc(const std::string &dcHost); /* all entries at/under base matching the scope (0 base,1 one,2 sub) */ void search(const Dn &base, int scope, const Filter &filter, const std::function<bool(const Entry &)> &sink); Loading
src/dsdb/provision.cpp +80 −0 Changes for src/dsdb/provision.cpp: 80 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -232,5 +232,85 @@ void Directory::provision(const std::string &adminPassword) { } } void Directory::provisionDc(const std::string &dcHost) { std::lock_guard<std::mutex> lk(_Mtx); ensureFresh(); if (dcHost.empty()) return; std::string lowHost = dsdb::lower(dcHost); std::string suffix = "." + dsdb::lower(_Meta.dnsName); if (lowHost.size() <= suffix.size() || lowHost.compare(lowHost.size() - suffix.size(), suffix.size(), suffix) != 0) return; /* this DC is not in this domain */ std::string shortName = dcHost.substr(0, dcHost.find('.')); std::string sam = dsdb::upper(shortName) + "$"; Dn dn = _Meta.baseDn.child("OU", "Domain Controllers").child("CN", shortName); if (findByDnLocked(dn)) return; /* already provisioned */ const Dn &base = _Meta.baseDn; Sid domSid = _Meta.sid; if (domSid.empty()) if (Entry *head = findByDnLocked(base)) domSid = head->objectSid; uint32_t rid = uint32_t(intValue(findByDnLocked(base)->attrs, "@nextRid", RID_FIRST_FREE)); /* bump the pool on the head */ if (Entry *head = findByDnLocked(base)) { AttrMap h = head->attrs; setInt(h, "@nextRid", rid + 1); storeObject(_Backend, head->objectGuid, head->dn, h, objectSecrets(_Backend, head->objectGuid), false); head->attrs = h; } std::string fqdn = dcHost; std::string up = dsdb::upper(shortName); AttrMap a; a["objectClass"] = {"top", "person", "organizationalPerson", "user", "computer"}; a["objectCategory"] = {"CN=Computer,CN=Schema,CN=Configuration," + base.str()}; a["objectSid"] = {domSid.withRid(rid).binary()}; a["sAMAccountName"] = {sam}; setInt(a, "sAMAccountType", SAM_MACHINE_ACCOUNT); setInt(a, "userAccountControl", UAC_SERVER_TRUST | UAC_TRUSTED_FOR_DELEGATION); setInt(a, "primaryGroupID", RID_DOMAIN_CONTROLLERS); setInt(a, "accountExpires", NEVER); a["dNSHostName"] = {fqdn}; setInt(a, "msDS-SupportedEncryptionTypes", 0x1F); /* AES256|AES128|RC4|DES */ a["servicePrincipalName"] = { "HOST/" + fqdn, "HOST/" + up, "ldap/" + fqdn, "ldap/" + up, "ldap/" + fqdn + "/" + _Meta.dnsName, "cifs/" + fqdn, "cifs/" + up, "RestrictedKrbHost/" + fqdn, "RestrictedKrbHost/" + up, "GC/" + fqdn + "/" + _Meta.dnsName, }; a["memberOf"] = {"CN=Domain Controllers,CN=Users," + base.str()}; a["isCriticalSystemObject"] = {"TRUE"}; std::string pw = crypto::base64(crypto::randomBytes(24)); Secrets sec = deriveSecrets(pw, kerberosSalt(PrincipalKind::Computer, _Meta.realm, sam, _Meta.dnsName), 0); setInt(a, "msDS-KeyVersionNumber", sec.kvno); /* inline create (we hold the lock) */ uuid::uuid g; g.generate(); a["name"] = {shortName}; a["cn"] = {shortName}; a["distinguishedName"] = {dn.str()}; setInt(a, "instanceType", 4); a["whenCreated"] = {generalizedTime(unixFromFileTime(fileTimeNow()))}; a["whenChanged"] = a["whenCreated"]; storeObject(_Backend, g, dn, a, sec.serialize(), true); Entry e; e.objectGuid = g; e.dn = dn; e.attrs = a; e.objectSid = domSid.withRid(rid); size_t idx = _Entries.size(); _Entries.push_back(e); addEntryToIndex(_Entries.back(), idx); _Revision = _Backend.getRevesion(); } } }
tests/proto/test_directory.cpp +34 −0 Changes for tests/proto/test_directory.cpp: 34 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -121,4 +121,38 @@ TEST(create_modify_delete) { unlink((path + ".lock").c_str()); } TEST(provision_dc_account) { std::string path = "/build/fds-dc-" + std::to_string(getpid()) + ".db"; unlink(path.c_str()); unlink((path + ".lock").c_str()); { AuthBackend backend(fenrirds::File, path, "example.com"); Directory dir(backend, meta()); dir.provision(); dir.provisionDc("dc1.example.com"); /* the DC machine account exists with its SPNs and a key */ auto dc = dir.bySamAccountName("DC1$"); CHECK(dc.has_value()); CHECK(dc->isClass("computer")); CHECK(dc->dn.under(dnFromDomain("example.com").child("OU","Domain Controllers"))); /* the KDC resolves these SPNs to the DC account */ CHECK(dir.bySpn("cifs/dc1.example.com").has_value()); CHECK(dir.bySpn("ldap/dc1.example.com").has_value()); CHECK(dir.bySpn("HOST/dc1.example.com").has_value()); /* and it has Kerberos keys to decrypt service tickets */ Secrets sec = dir.secretsOf(dc->objectGuid); CHECK(!sec.empty()); CHECK(sec.key(fenrirds::crypto::krb5::AES256_CTS_HMAC_SHA1_96) != nullptr); /* idempotent */ dir.provisionDc("dc1.example.com"); int n = 0; Filter f = Filter::equal("sAMAccountName", "DC1$"); dir.search(dnFromDomain("example.com"), 2, f, [&](const Entry&){ ++n; return true; }); CHECK_EQ(n, 1); } unlink(path.c_str()); unlink((path + ".lock").c_str()); } CHECK_MAIN