Loading README.md +22 −0 Changes for README.md: 22 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -11,6 +11,28 @@ See [docs/ROADMAP.md](docs/ROADMAP.md) for the AD compatibility work. HTTP is available without TLS configuration. To enable HTTPS, set both `FENRIRDS/SSL/KEY` and `FENRIRDS/SSL/CERT` in the config. ## Accounts Users and groups live in the directory of their domain, the accounts of the admin console in that of `admin.local`. That one is internal: it is not served over LDAP, CLDAP or Kerberos. The setup wizard creates it, so the `ad` plugin is required. Accounts of older versions move over at startup; a password stored as a web hash keeps working until it is changed. ## Login lockout Every way in counts failed passwords: web and API logins, LDAP binds, Kerberos, NTLM over LDAP and SMB, RADIUS, OIDC. Meanwhile the right password is refused too. ```yaml FENRIRDS: LOCKOUT: PER_SOURCE: 5 # failures of a user from one address PER_ACCOUNT: 50 # ... from anywhere, for guessers with many addresses SECONDS: 300 # how long they count; 0 in a limit turns it off ``` The counters are per node and in memory, a restart clears them. ## Master/slave replication A master can feed any number of read-only slaves. Master and slave can each be a single node (file backend) or a cluster (`FENRIRDS/CLUSTER/ENABLE`). Loading plugin/ad/ad.cpp +116 −33 Changes for plugin/ad/ad.cpp: 116 added lines, 33 removed lines. Original line number Diff line number Diff line Loading @@ -27,6 +27,9 @@ #include <ctime> #include <iostream> #include "loginguard.h" #include <cstring> #include <map> #include <algorithm> Loading Loading @@ -569,6 +572,27 @@ bool ActiveDirectory::createWindowsGroup(const std::string &domain, const std::s * stay consistent with the live cache and the shared RID allocator. *****************************************************************************/ static const char *const ADMIN_DOMAIN = "admin.local"; ActiveDirectory::DirLock::DirLock(ActiveDirectory &ad, const std::string &domain) : _Guard(dsdb::iequals(domain, ADMIN_DOMAIN) && DomainRegistry::instance().admin() ? std::make_unique<AuthBackend::Guard>(*DomainRegistry::instance().admin()) : nullptr), _Lock(ad._Lock) {} ActiveDirectory::Dom *ActiveDirectory::directoryByName(const std::string &dnsName) { if (dsdb::iequals(dnsName, ADMIN_DOMAIN)) { /* Not while the admin db is empty: that's what shows the setup wizard. * Later on demand, in a cluster it can arrive after startup. */ AuthBackend *bck = DomainRegistry::instance().admin(); std::string err; if (!_AdminDom && bck && bck->end() > sizeof(AuthHeader)) ensureAdminDom(err); return _AdminDom.get(); } return domainByName(dnsName); } ActiveDirectory::Dom *ActiveDirectory::domainByName(const std::string &dnsName) { for (auto &dd : _Domains) if (dsdb::iequals(dd.meta.dnsName, dnsName)) Loading @@ -593,8 +617,8 @@ bool ActiveDirectory::dirWith(const std::string &domain, bool ActiveDirectory::dirList(const std::string &domain, int kind, std::vector<dsdb::Entry> &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } dsdb::Filter f; Loading Loading @@ -629,8 +653,8 @@ bool ActiveDirectory::dirList(const std::string &domain, int kind, bool ActiveDirectory::dirGet(const std::string &domain, const uuid::uuid &guid, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading @@ -640,8 +664,8 @@ bool ActiveDirectory::dirGet(const std::string &domain, const uuid::uuid &guid, bool ActiveDirectory::dirGetBySam(const std::string &domain, const std::string &sam, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->bySamAccountName(sam); if (!e) { err = "object not found"; return false; } Loading @@ -651,13 +675,13 @@ bool ActiveDirectory::dirGetBySam(const std::string &domain, const std::string & bool ActiveDirectory::dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, std::string &err) { uuid::uuid &outGuid, std::string &err, const uuid::uuid *presetGuid) { if (g_Replication && g_Replication->isSlave()) { err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } std::string sam = dsdb::value(attrs, "sAMAccountName"); Loading Loading @@ -708,7 +732,7 @@ bool ActiveDirectory::dirCreate(const std::string &domain, int kind, dsdb::AttrM } try { outGuid = d->dir->create(dn, attrs, sec.get()); outGuid = d->dir->create(dn, attrs, sec.get(), presetGuid); } catch (std::exception &e) { err = e.what(); return false; Loading @@ -726,8 +750,8 @@ bool ActiveDirectory::dirModify(const std::string &domain, const uuid::uuid &gui err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading Loading @@ -763,8 +787,8 @@ bool ActiveDirectory::dirRemove(const std::string &domain, const uuid::uuid &gui err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } if (!d->dir->byGuid(guid)) { err = "object not found"; return false; } try { Loading @@ -783,8 +807,8 @@ bool ActiveDirectory::dirSetWindowsLogin(const std::string &domain, const uuid:: err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading Loading @@ -828,13 +852,16 @@ bool ActiveDirectory::dirSetWindowsLogin(const std::string &domain, const uuid:: bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::string &sam, const std::string &pw, uuid::uuid &outGuid, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } LoginGuard &guard = LoginGuard::instance(); if (guard.blocked(domain, sam)) { err = "too many failed logins"; return false; } auto e = d->dir->bySamAccountName(sam); if (!e) { err = "no such user"; return false; } if (!e) { guard.failed(domain, sam); err = "no such user"; return false; } dsdb::Secrets sec = d->dir->secretsOf(e->objectGuid); if (dsdb::verifyPassword(sec, pw)) { guard.succeeded(domain, sam); outGuid = e->objectGuid; return true; } Loading @@ -846,18 +873,20 @@ bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::stri std::string h; sha.hash(pw, h); if (h == wh) { guard.succeeded(domain, sam); outGuid = e->objectGuid; return true; } } guard.failed(domain, sam); err = "wrong password"; return false; } bool ActiveDirectory::dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading Loading @@ -891,8 +920,8 @@ bool ActiveDirectory::dirAddMember(const std::string &domain, const uuid::uuid & err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); Loading Loading @@ -928,8 +957,8 @@ bool ActiveDirectory::dirRemoveMember(const std::string &domain, const uuid::uui err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); Loading Loading @@ -964,11 +993,12 @@ bool ActiveDirectory::dirImportNative(const std::string &domain, int &migrated, err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); DirLock lock(*this, domain); migrated = 0; Dom *d = domainByName(domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } AuthBackend *bck = DomainRegistry::instance().getByName(domain, nullptr); AuthBackend *bck = dsdb::iequals(domain, ADMIN_DOMAIN) ? DomainRegistry::instance().admin() : DomainRegistry::instance().getByName(domain, nullptr); if (!bck) { err = "no backend for '" + domain + "'"; return false; } dsdb::Dn users = d->meta.baseDn.child("CN", "Users"); Loading Loading @@ -1085,6 +1115,11 @@ bool ActiveDirectory::dirImportNative(const std::string &domain, int &migrated, } } std::cerr << "[AD] dirImportNative: native groups=" << gids.size() << std::endl; if (groupMap) for (const auto &gid : gids) if (groupMap->count(gid.c_str()) && (*groupMap)[gid.c_str()] != gid.c_str()) { try { group.remove(*bck, gid); } catch (...) {} } } /* drop the migrated native users only now: the group pass above still Loading Loading @@ -1149,6 +1184,40 @@ void ActiveDirectory::loadDomains() { } } bool ActiveDirectory::provisionAdminDirectory(std::string &err) { DirLock lock(*this, ADMIN_DOMAIN); return ensureAdminDom(err); } /* caller holds a DirLock for admin.local */ bool ActiveDirectory::ensureAdminDom(std::string &err) { AuthBackend *bck = DomainRegistry::instance().admin(); if (!bck) { err = "no admin backend"; return false; } if (_AdminDom) return true; auto d = std::make_unique<Dom>(); d->meta.dnsName = ADMIN_DOMAIN; d->meta.netbios = "ADMIN"; d->meta.baseDn = dsdb::dnFromDomain(ADMIN_DOMAIN); d->meta.realm = dsdb::upper(ADMIN_DOMAIN); d->dir = std::make_unique<dsdb::Directory>(*bck, d->meta); if (!(g_Replication && g_Replication->isSlave())) { try { /* Nobody signs in as its Administrator: only the accounts of the * SuperAdmin group mean anything here. */ d->dir->provision(crypto::hex(crypto::randomBytes(32))); } catch (std::exception &ex) { err = ex.what(); return false; } } if (auto head = d->dir->byDn(d->meta.baseDn)) d->meta.sid = head->objectSid; _AdminDom = std::move(d); std::cerr << "AD: admin directory ready" << std::endl; return true; } ActiveDirectory::Dom *ActiveDirectory::domainForDn(const dsdb::Dn &dn) { Dom *best = nullptr; size_t bestLen = 0; Loading Loading @@ -1196,6 +1265,7 @@ void ActiveDirectory::ResponseEvent(netplus::con &, const int, ULONG_PTR) {} void ActiveDirectory::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { if (curcon.RecvData.empty()) return; LoginGuard::Scope peer(curcon.slots.empty() || !curcon.slots[0].csock ? -1 : curcon.slots[0].csock->fd()); std::cerr << "[LDAP] recv " << curcon.RecvData.size() << "B (first=0x" << std::hex << (curcon.RecvData.empty() ? 0 : (unsigned char)curcon.RecvData[0]) << std::dec << ")" << std::endl; Loading Loading @@ -1542,6 +1612,8 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, ntlm::Auth3 a; if (!ntlm::parseAuth3(mechTok, a)) return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); if (LoginGuard::instance().blocked(a.domain, a.user)) return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); std::vector<uint8_t> ntHash; std::string princ; if (!g_KdcPlugin->ntlmNtHash(a.user, a.domain, ntHash, princ)) { std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user Loading @@ -1550,10 +1622,12 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, } std::vector<uint8_t> exported; if (!ntlm::verifyV2(ntHash, a, session.ntlmChallenge, exported)) { LoginGuard::instance().failed(a.domain, a.user); std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> BAD PASSWORD" << std::endl; return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); } LoginGuard::instance().succeeded(a.domain, a.user); std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> SUCCESS (" << princ << ")" << std::endl; session.authenticated = true; Loading Loading @@ -1668,10 +1742,6 @@ bool ActiveDirectory::authenticateSimple(const std::string &bindDN, const std::s if (password.empty()) return false; /* unauthenticated bind: treat as anonymous, refuse here */ dsdb::Dn dn; if (!dsdb::Dn::parse(bindDN, dn)) return false; std::lock_guard<std::mutex> lock(_Lock); std::optional<dsdb::Entry> entry; dsdb::Directory *dir = nullptr; Loading @@ -1689,6 +1759,9 @@ bool ActiveDirectory::authenticateSimple(const std::string &bindDN, const std::s break; } } else { dsdb::Dn dn; if (!dsdb::Dn::parse(bindDN, dn)) return false; Dom *d = domainForDn(dn); if (d) { dir = d->dir.get(); Loading @@ -1699,9 +1772,19 @@ bool ActiveDirectory::authenticateSimple(const std::string &bindDN, const std::s if (!entry || !dir) return false; std::string bindDomain; for (auto &d : _Domains) if (d.dir.get() == dir) bindDomain = d.meta.dnsName; std::string bindSam = entry->str("sAMAccountName"); LoginGuard &guard = LoginGuard::instance(); if (guard.blocked(bindDomain, bindSam)) return false; dsdb::Secrets sec = dir->secretsOf(entry->objectGuid); if (!dsdb::verifyPassword(sec, password)) if (!dsdb::verifyPassword(sec, password)) { guard.failed(bindDomain, bindSam); return false; } guard.succeeded(bindDomain, bindSam); int64_t uac = dsdb::intValue(entry->attrs, "userAccountControl", 0); if (uac & dsdb::UAC_DISABLED) Loading plugin/ad/ad.h +18 −1 Changes for plugin/ad/ad.h: 18 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -320,7 +320,9 @@ namespace fenrirds { dsdb::Entry &out, std::string &err) override; bool dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, std::string &err) override; uuid::uuid &outGuid, std::string &err, const uuid::uuid *presetGuid = nullptr) override; bool provisionAdminDirectory(std::string &err) override; bool dirModify(const std::string &domain, const uuid::uuid &guid, const dsdb::AttrMap &attrs, const std::string &pw, std::string &err) override; Loading Loading @@ -394,6 +396,21 @@ namespace fenrirds { Dom *domainForDn(const dsdb::Dn &dn); /* the domain by dnsName (case-insensitive), or nullptr; caller holds _Lock */ Dom *domainByName(const std::string &dnsName); /* the same for the dir* calls: also finds admin.local, which stays out * of _Domains so that nothing on the network can reach it */ Dom *directoryByName(const std::string &dnsName); bool ensureAdminDom(std::string &err); std::unique_ptr<Dom> _AdminDom; /* The lock order of the dir* calls: for admin.local its backend comes * first. The admin console holds that backend's guard while it calls * them, the other way round would deadlock. */ class DirLock { public: DirLock(ActiveDirectory &ad, const std::string &domain); private: std::unique_ptr<AuthBackend::Guard> _Guard; std::lock_guard<std::mutex> _Lock; }; /* render a directory entry as an LdapEntry honoring the requested * attribute list (empty / "*" = all user attrs; "+" = operational; * "1.1" = none; secret attributes are never returned) */ Loading plugin/krb5/krb5.cpp +17 −1 Changes for plugin/krb5/krb5.cpp: 17 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ #include <netplus/crypto/sha.h> #include "krb5.h" #include "loginguard.h" #include "fenrirds.h" #include "backend.h" #include "user.h" Loading Loading @@ -2694,6 +2695,12 @@ void Krb5KDC::udpLoop(netplus::udp *sock) { } catch (...) { continue; } for (size_t i = 0; i < n; ++i) { std::vector<uint8_t> response; char peer[INET6_ADDRSTRLEN] = {}; if (addrs[i].ss_family == AF_INET) inet_ntop(AF_INET, &reinterpret_cast<sockaddr_in *>(&addrs[i])->sin_addr, peer, sizeof(peer)); else if (addrs[i].ss_family == AF_INET6) inet_ntop(AF_INET6, &reinterpret_cast<sockaddr_in6 *>(&addrs[i])->sin6_addr, peer, sizeof(peer)); LoginGuard::Scope loginPeer{std::string(peer)}; try { if (isKpasswd(dgrams[i].data(), dgrams[i].size())) response = handleKpasswd(dgrams[i].data(), dgrams[i].size(), _LocalIp); Loading Loading @@ -2921,6 +2928,7 @@ void Krb5KDC::ResponseEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR / } void Krb5KDC::RequestEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /*args*/) { LoginGuard::Scope loginPeer(curcon.slots.empty() || !curcon.slots[0].csock ? -1 : curcon.slots[0].csock->fd()); /* * Standard Kerberos TCP framing: 4-byte big-endian length prefix * followed by the ASN.1 DER encoded message. Loading Loading @@ -3284,6 +3292,11 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le << "' keylen=" << client_key.size() << std::endl; /* Check pre-authentication */ std::string guardUser = req.cname.name; LoginGuard &loginGuard = LoginGuard::instance(); if (loginGuard.blocked(req.cname.realm, guardUser)) return buildStandardError(static_cast<Krb5ErrorCode>(18) /* KDC_ERR_CLIENT_REVOKED */, "Too many failed logins", req.cname.realm, "krbtgt"); if (req.padata.empty()) { /* Send PREAUTH_REQUIRED with ETYPE-INFO2 */ auto etypeInfo = Asn1Der::encodeEtypeInfo2(selectedEtype, clientSalt); Loading Loading @@ -3382,8 +3395,10 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le time_t now = time(nullptr); std::cerr << "[KDC] preauth decrypt OK (paEtype=" << int(paEtype) << " skew=" << long(now - pa_time) << ")" << std::endl; if (std::abs(static_cast<long>(now - pa_time)) <= _MaxClockSkew) if (std::abs(static_cast<long>(now - pa_time)) <= _MaxClockSkew) { paVerified = true; loginGuard.succeeded(req.cname.realm, guardUser); } } else { std::cerr << "[KDC] preauth decrypt OK but timestamp parse FAILED" << std::endl; } Loading @@ -3402,6 +3417,7 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le * failed → wrong password. Return PREAUTH_FAILED so MIT * kinit reports an error instead of looping. */ loginGuard.failed(req.cname.realm, guardUser); return buildStandardError(KDC_ERR_PREAUTH_FAILED, "Pre-authentication failed", req.cname.realm, "krbtgt"); Loading plugin/oidc/oidc.cpp +2 −0 Changes for plugin/oidc/oidc.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ #include "domains.h" #include "dsdb/attrs.h" #include "oidc.h" #include "loginguard.h" namespace fenrirds { Loading Loading @@ -157,6 +158,7 @@ public: Http(OidcServer *srv, std::vector<netplus::socket *> socks) : HttpEvent(socks), _Srv(srv) {} void RequestEvent(libhttppp::HttpRequest &curreq, const int, ULONG_PTR) override { LoginGuard::Scope loginPeer(curreq.slots.empty() || !curreq.slots[0].csock ? -1 : curreq.slots[0].csock->fd()); Request rq; Response rs; try { Loading Loading
README.md +22 −0 Changes for README.md: 22 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -11,6 +11,28 @@ See [docs/ROADMAP.md](docs/ROADMAP.md) for the AD compatibility work. HTTP is available without TLS configuration. To enable HTTPS, set both `FENRIRDS/SSL/KEY` and `FENRIRDS/SSL/CERT` in the config. ## Accounts Users and groups live in the directory of their domain, the accounts of the admin console in that of `admin.local`. That one is internal: it is not served over LDAP, CLDAP or Kerberos. The setup wizard creates it, so the `ad` plugin is required. Accounts of older versions move over at startup; a password stored as a web hash keeps working until it is changed. ## Login lockout Every way in counts failed passwords: web and API logins, LDAP binds, Kerberos, NTLM over LDAP and SMB, RADIUS, OIDC. Meanwhile the right password is refused too. ```yaml FENRIRDS: LOCKOUT: PER_SOURCE: 5 # failures of a user from one address PER_ACCOUNT: 50 # ... from anywhere, for guessers with many addresses SECONDS: 300 # how long they count; 0 in a limit turns it off ``` The counters are per node and in memory, a restart clears them. ## Master/slave replication A master can feed any number of read-only slaves. Master and slave can each be a single node (file backend) or a cluster (`FENRIRDS/CLUSTER/ENABLE`). Loading
plugin/ad/ad.cpp +116 −33 Changes for plugin/ad/ad.cpp: 116 added lines, 33 removed lines. Original line number Diff line number Diff line Loading @@ -27,6 +27,9 @@ #include <ctime> #include <iostream> #include "loginguard.h" #include <cstring> #include <map> #include <algorithm> Loading Loading @@ -569,6 +572,27 @@ bool ActiveDirectory::createWindowsGroup(const std::string &domain, const std::s * stay consistent with the live cache and the shared RID allocator. *****************************************************************************/ static const char *const ADMIN_DOMAIN = "admin.local"; ActiveDirectory::DirLock::DirLock(ActiveDirectory &ad, const std::string &domain) : _Guard(dsdb::iequals(domain, ADMIN_DOMAIN) && DomainRegistry::instance().admin() ? std::make_unique<AuthBackend::Guard>(*DomainRegistry::instance().admin()) : nullptr), _Lock(ad._Lock) {} ActiveDirectory::Dom *ActiveDirectory::directoryByName(const std::string &dnsName) { if (dsdb::iequals(dnsName, ADMIN_DOMAIN)) { /* Not while the admin db is empty: that's what shows the setup wizard. * Later on demand, in a cluster it can arrive after startup. */ AuthBackend *bck = DomainRegistry::instance().admin(); std::string err; if (!_AdminDom && bck && bck->end() > sizeof(AuthHeader)) ensureAdminDom(err); return _AdminDom.get(); } return domainByName(dnsName); } ActiveDirectory::Dom *ActiveDirectory::domainByName(const std::string &dnsName) { for (auto &dd : _Domains) if (dsdb::iequals(dd.meta.dnsName, dnsName)) Loading @@ -593,8 +617,8 @@ bool ActiveDirectory::dirWith(const std::string &domain, bool ActiveDirectory::dirList(const std::string &domain, int kind, std::vector<dsdb::Entry> &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } dsdb::Filter f; Loading Loading @@ -629,8 +653,8 @@ bool ActiveDirectory::dirList(const std::string &domain, int kind, bool ActiveDirectory::dirGet(const std::string &domain, const uuid::uuid &guid, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading @@ -640,8 +664,8 @@ bool ActiveDirectory::dirGet(const std::string &domain, const uuid::uuid &guid, bool ActiveDirectory::dirGetBySam(const std::string &domain, const std::string &sam, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->bySamAccountName(sam); if (!e) { err = "object not found"; return false; } Loading @@ -651,13 +675,13 @@ bool ActiveDirectory::dirGetBySam(const std::string &domain, const std::string & bool ActiveDirectory::dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, std::string &err) { uuid::uuid &outGuid, std::string &err, const uuid::uuid *presetGuid) { if (g_Replication && g_Replication->isSlave()) { err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } std::string sam = dsdb::value(attrs, "sAMAccountName"); Loading Loading @@ -708,7 +732,7 @@ bool ActiveDirectory::dirCreate(const std::string &domain, int kind, dsdb::AttrM } try { outGuid = d->dir->create(dn, attrs, sec.get()); outGuid = d->dir->create(dn, attrs, sec.get(), presetGuid); } catch (std::exception &e) { err = e.what(); return false; Loading @@ -726,8 +750,8 @@ bool ActiveDirectory::dirModify(const std::string &domain, const uuid::uuid &gui err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading Loading @@ -763,8 +787,8 @@ bool ActiveDirectory::dirRemove(const std::string &domain, const uuid::uuid &gui err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } if (!d->dir->byGuid(guid)) { err = "object not found"; return false; } try { Loading @@ -783,8 +807,8 @@ bool ActiveDirectory::dirSetWindowsLogin(const std::string &domain, const uuid:: err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading Loading @@ -828,13 +852,16 @@ bool ActiveDirectory::dirSetWindowsLogin(const std::string &domain, const uuid:: bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::string &sam, const std::string &pw, uuid::uuid &outGuid, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } LoginGuard &guard = LoginGuard::instance(); if (guard.blocked(domain, sam)) { err = "too many failed logins"; return false; } auto e = d->dir->bySamAccountName(sam); if (!e) { err = "no such user"; return false; } if (!e) { guard.failed(domain, sam); err = "no such user"; return false; } dsdb::Secrets sec = d->dir->secretsOf(e->objectGuid); if (dsdb::verifyPassword(sec, pw)) { guard.succeeded(domain, sam); outGuid = e->objectGuid; return true; } Loading @@ -846,18 +873,20 @@ bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::stri std::string h; sha.hash(pw, h); if (h == wh) { guard.succeeded(domain, sam); outGuid = e->objectGuid; return true; } } guard.failed(domain, sam); err = "wrong password"; return false; } bool ActiveDirectory::dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } Loading Loading @@ -891,8 +920,8 @@ bool ActiveDirectory::dirAddMember(const std::string &domain, const uuid::uuid & err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); Loading Loading @@ -928,8 +957,8 @@ bool ActiveDirectory::dirRemoveMember(const std::string &domain, const uuid::uui err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); DirLock lock(*this, domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); Loading Loading @@ -964,11 +993,12 @@ bool ActiveDirectory::dirImportNative(const std::string &domain, int &migrated, err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); DirLock lock(*this, domain); migrated = 0; Dom *d = domainByName(domain); Dom *d = directoryByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } AuthBackend *bck = DomainRegistry::instance().getByName(domain, nullptr); AuthBackend *bck = dsdb::iequals(domain, ADMIN_DOMAIN) ? DomainRegistry::instance().admin() : DomainRegistry::instance().getByName(domain, nullptr); if (!bck) { err = "no backend for '" + domain + "'"; return false; } dsdb::Dn users = d->meta.baseDn.child("CN", "Users"); Loading Loading @@ -1085,6 +1115,11 @@ bool ActiveDirectory::dirImportNative(const std::string &domain, int &migrated, } } std::cerr << "[AD] dirImportNative: native groups=" << gids.size() << std::endl; if (groupMap) for (const auto &gid : gids) if (groupMap->count(gid.c_str()) && (*groupMap)[gid.c_str()] != gid.c_str()) { try { group.remove(*bck, gid); } catch (...) {} } } /* drop the migrated native users only now: the group pass above still Loading Loading @@ -1149,6 +1184,40 @@ void ActiveDirectory::loadDomains() { } } bool ActiveDirectory::provisionAdminDirectory(std::string &err) { DirLock lock(*this, ADMIN_DOMAIN); return ensureAdminDom(err); } /* caller holds a DirLock for admin.local */ bool ActiveDirectory::ensureAdminDom(std::string &err) { AuthBackend *bck = DomainRegistry::instance().admin(); if (!bck) { err = "no admin backend"; return false; } if (_AdminDom) return true; auto d = std::make_unique<Dom>(); d->meta.dnsName = ADMIN_DOMAIN; d->meta.netbios = "ADMIN"; d->meta.baseDn = dsdb::dnFromDomain(ADMIN_DOMAIN); d->meta.realm = dsdb::upper(ADMIN_DOMAIN); d->dir = std::make_unique<dsdb::Directory>(*bck, d->meta); if (!(g_Replication && g_Replication->isSlave())) { try { /* Nobody signs in as its Administrator: only the accounts of the * SuperAdmin group mean anything here. */ d->dir->provision(crypto::hex(crypto::randomBytes(32))); } catch (std::exception &ex) { err = ex.what(); return false; } } if (auto head = d->dir->byDn(d->meta.baseDn)) d->meta.sid = head->objectSid; _AdminDom = std::move(d); std::cerr << "AD: admin directory ready" << std::endl; return true; } ActiveDirectory::Dom *ActiveDirectory::domainForDn(const dsdb::Dn &dn) { Dom *best = nullptr; size_t bestLen = 0; Loading Loading @@ -1196,6 +1265,7 @@ void ActiveDirectory::ResponseEvent(netplus::con &, const int, ULONG_PTR) {} void ActiveDirectory::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { if (curcon.RecvData.empty()) return; LoginGuard::Scope peer(curcon.slots.empty() || !curcon.slots[0].csock ? -1 : curcon.slots[0].csock->fd()); std::cerr << "[LDAP] recv " << curcon.RecvData.size() << "B (first=0x" << std::hex << (curcon.RecvData.empty() ? 0 : (unsigned char)curcon.RecvData[0]) << std::dec << ")" << std::endl; Loading Loading @@ -1542,6 +1612,8 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, ntlm::Auth3 a; if (!ntlm::parseAuth3(mechTok, a)) return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); if (LoginGuard::instance().blocked(a.domain, a.user)) return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); std::vector<uint8_t> ntHash; std::string princ; if (!g_KdcPlugin->ntlmNtHash(a.user, a.domain, ntHash, princ)) { std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user Loading @@ -1550,10 +1622,12 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, } std::vector<uint8_t> exported; if (!ntlm::verifyV2(ntHash, a, session.ntlmChallenge, exported)) { LoginGuard::instance().failed(a.domain, a.user); std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> BAD PASSWORD" << std::endl; return bindResp(LDAP_INVALID_CREDENTIALS, nullptr); } LoginGuard::instance().succeeded(a.domain, a.user); std::cerr << "[LDAP] bind NTLMSSP '" << a.domain << "\\" << a.user << "' -> SUCCESS (" << princ << ")" << std::endl; session.authenticated = true; Loading Loading @@ -1668,10 +1742,6 @@ bool ActiveDirectory::authenticateSimple(const std::string &bindDN, const std::s if (password.empty()) return false; /* unauthenticated bind: treat as anonymous, refuse here */ dsdb::Dn dn; if (!dsdb::Dn::parse(bindDN, dn)) return false; std::lock_guard<std::mutex> lock(_Lock); std::optional<dsdb::Entry> entry; dsdb::Directory *dir = nullptr; Loading @@ -1689,6 +1759,9 @@ bool ActiveDirectory::authenticateSimple(const std::string &bindDN, const std::s break; } } else { dsdb::Dn dn; if (!dsdb::Dn::parse(bindDN, dn)) return false; Dom *d = domainForDn(dn); if (d) { dir = d->dir.get(); Loading @@ -1699,9 +1772,19 @@ bool ActiveDirectory::authenticateSimple(const std::string &bindDN, const std::s if (!entry || !dir) return false; std::string bindDomain; for (auto &d : _Domains) if (d.dir.get() == dir) bindDomain = d.meta.dnsName; std::string bindSam = entry->str("sAMAccountName"); LoginGuard &guard = LoginGuard::instance(); if (guard.blocked(bindDomain, bindSam)) return false; dsdb::Secrets sec = dir->secretsOf(entry->objectGuid); if (!dsdb::verifyPassword(sec, password)) if (!dsdb::verifyPassword(sec, password)) { guard.failed(bindDomain, bindSam); return false; } guard.succeeded(bindDomain, bindSam); int64_t uac = dsdb::intValue(entry->attrs, "userAccountControl", 0); if (uac & dsdb::UAC_DISABLED) Loading
plugin/ad/ad.h +18 −1 Changes for plugin/ad/ad.h: 18 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -320,7 +320,9 @@ namespace fenrirds { dsdb::Entry &out, std::string &err) override; bool dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, std::string &err) override; uuid::uuid &outGuid, std::string &err, const uuid::uuid *presetGuid = nullptr) override; bool provisionAdminDirectory(std::string &err) override; bool dirModify(const std::string &domain, const uuid::uuid &guid, const dsdb::AttrMap &attrs, const std::string &pw, std::string &err) override; Loading Loading @@ -394,6 +396,21 @@ namespace fenrirds { Dom *domainForDn(const dsdb::Dn &dn); /* the domain by dnsName (case-insensitive), or nullptr; caller holds _Lock */ Dom *domainByName(const std::string &dnsName); /* the same for the dir* calls: also finds admin.local, which stays out * of _Domains so that nothing on the network can reach it */ Dom *directoryByName(const std::string &dnsName); bool ensureAdminDom(std::string &err); std::unique_ptr<Dom> _AdminDom; /* The lock order of the dir* calls: for admin.local its backend comes * first. The admin console holds that backend's guard while it calls * them, the other way round would deadlock. */ class DirLock { public: DirLock(ActiveDirectory &ad, const std::string &domain); private: std::unique_ptr<AuthBackend::Guard> _Guard; std::lock_guard<std::mutex> _Lock; }; /* render a directory entry as an LdapEntry honoring the requested * attribute list (empty / "*" = all user attrs; "+" = operational; * "1.1" = none; secret attributes are never returned) */ Loading
plugin/krb5/krb5.cpp +17 −1 Changes for plugin/krb5/krb5.cpp: 17 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ #include <netplus/crypto/sha.h> #include "krb5.h" #include "loginguard.h" #include "fenrirds.h" #include "backend.h" #include "user.h" Loading Loading @@ -2694,6 +2695,12 @@ void Krb5KDC::udpLoop(netplus::udp *sock) { } catch (...) { continue; } for (size_t i = 0; i < n; ++i) { std::vector<uint8_t> response; char peer[INET6_ADDRSTRLEN] = {}; if (addrs[i].ss_family == AF_INET) inet_ntop(AF_INET, &reinterpret_cast<sockaddr_in *>(&addrs[i])->sin_addr, peer, sizeof(peer)); else if (addrs[i].ss_family == AF_INET6) inet_ntop(AF_INET6, &reinterpret_cast<sockaddr_in6 *>(&addrs[i])->sin6_addr, peer, sizeof(peer)); LoginGuard::Scope loginPeer{std::string(peer)}; try { if (isKpasswd(dgrams[i].data(), dgrams[i].size())) response = handleKpasswd(dgrams[i].data(), dgrams[i].size(), _LocalIp); Loading Loading @@ -2921,6 +2928,7 @@ void Krb5KDC::ResponseEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR / } void Krb5KDC::RequestEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /*args*/) { LoginGuard::Scope loginPeer(curcon.slots.empty() || !curcon.slots[0].csock ? -1 : curcon.slots[0].csock->fd()); /* * Standard Kerberos TCP framing: 4-byte big-endian length prefix * followed by the ASN.1 DER encoded message. Loading Loading @@ -3284,6 +3292,11 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le << "' keylen=" << client_key.size() << std::endl; /* Check pre-authentication */ std::string guardUser = req.cname.name; LoginGuard &loginGuard = LoginGuard::instance(); if (loginGuard.blocked(req.cname.realm, guardUser)) return buildStandardError(static_cast<Krb5ErrorCode>(18) /* KDC_ERR_CLIENT_REVOKED */, "Too many failed logins", req.cname.realm, "krbtgt"); if (req.padata.empty()) { /* Send PREAUTH_REQUIRED with ETYPE-INFO2 */ auto etypeInfo = Asn1Der::encodeEtypeInfo2(selectedEtype, clientSalt); Loading Loading @@ -3382,8 +3395,10 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le time_t now = time(nullptr); std::cerr << "[KDC] preauth decrypt OK (paEtype=" << int(paEtype) << " skew=" << long(now - pa_time) << ")" << std::endl; if (std::abs(static_cast<long>(now - pa_time)) <= _MaxClockSkew) if (std::abs(static_cast<long>(now - pa_time)) <= _MaxClockSkew) { paVerified = true; loginGuard.succeeded(req.cname.realm, guardUser); } } else { std::cerr << "[KDC] preauth decrypt OK but timestamp parse FAILED" << std::endl; } Loading @@ -3402,6 +3417,7 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le * failed → wrong password. Return PREAUTH_FAILED so MIT * kinit reports an error instead of looping. */ loginGuard.failed(req.cname.realm, guardUser); return buildStandardError(KDC_ERR_PREAUTH_FAILED, "Pre-authentication failed", req.cname.realm, "krbtgt"); Loading
plugin/oidc/oidc.cpp +2 −0 Changes for plugin/oidc/oidc.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ #include "domains.h" #include "dsdb/attrs.h" #include "oidc.h" #include "loginguard.h" namespace fenrirds { Loading Loading @@ -157,6 +158,7 @@ public: Http(OidcServer *srv, std::vector<netplus::socket *> socks) : HttpEvent(socks), _Srv(srv) {} void RequestEvent(libhttppp::HttpRequest &curreq, const int, ULONG_PTR) override { LoginGuard::Scope loginPeer(curreq.slots.empty() || !curreq.slots[0].csock ? -1 : curreq.slots[0].csock->fd()); Request rq; Response rs; try { Loading