Commit f0ebc8ec authored by jan.koester's avatar jan.koester
Browse files

test

parent a3f9b86a
Loading
Loading
Loading
Loading
Loading
+22 −0
Changes for README.md: 22 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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`).
+116 −33
Changes for plugin/ad/ad.cpp: 116 added lines, 33 removed lines.
Original line number Diff line number Diff line
@@ -27,6 +27,9 @@

#include <ctime>
#include <iostream>

#include "loginguard.h"

#include <cstring>
#include <map>
#include <algorithm>
@@ -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))
@@ -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;
@@ -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; }
@@ -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; }
@@ -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");
@@ -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;
@@ -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; }
@@ -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 {
@@ -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; }
@@ -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;
    }
@@ -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; }
@@ -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);
@@ -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);
@@ -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");
@@ -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
@@ -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;
@@ -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;
@@ -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
@@ -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;
@@ -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;
@@ -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();
@@ -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)
+18 −1
Changes for plugin/ad/ad.h: 18 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -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;
@@ -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) */
+17 −1
Changes for plugin/krb5/krb5.cpp: 17 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@
#include <netplus/crypto/sha.h>

#include "krb5.h"
#include "loginguard.h"
#include "fenrirds.h"
#include "backend.h"
#include "user.h"
@@ -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);
@@ -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.
@@ -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);
@@ -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;
                        }
@@ -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");
+2 −0
Changes for plugin/oidc/oidc.cpp: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@
#include "domains.h"
#include "dsdb/attrs.h"
#include "oidc.h"
#include "loginguard.h"

namespace fenrirds {

@@ -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