Loading CMakeLists.txt +20 −2 Changes for CMakeLists.txt: 20 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -6,8 +6,10 @@ project(adconnect VERSION 0.1.0 LANGUAGES CXX) # systempp's <userdb.h>). # # adconnectd the daemon (root): reads /etc/adconnect/adconnect.yaml # (libconfplus), binds to a domain controller over LDAPS or # StartTLS (libnetplus), answers on /run/adconnect.sock and # (libconfplus), binds to a domain controller -- simple bind # over LDAPS or StartTLS (libnetplus), or Kerberos: SASL # GSSAPI with its own Kerberos client (src/krb5*.cpp, # gssapi.cpp, sasl.cpp) -- answers on /run/adconnect.sock and # caches what it was told -- on disk too, so that users and # groups stay known while no domain controller is reachable # adconnect.so the userdb module /etc/userdb.yaml loads into every Loading Loading @@ -50,6 +52,10 @@ add_library(adconnect_core STATIC src/config.cpp src/directory.cpp src/cache.cpp src/krb5crypto.cpp src/krb5.cpp src/gssapi.cpp src/sasl.cpp ) target_link_libraries(adconnect_core PUBLIC adconnect_proto confplus::confplus netplus::netplus Threads::Threads) Loading Loading @@ -78,3 +84,15 @@ if(BUILD_TESTING) add_dependencies(adconnect_test adconnectd adconnect) add_test(NAME adconnect_test COMMAND adconnect_test) endif() # Interoperability with MIT krb5 (KDC and GSS-API acceptor): needs MIT's # gssapi headers and library, so not on FenrirOS -- test/mit_interop.sh # sets up a throw-away KDC and runs it. option(ADCONNECT_MIT_INTEROP "build test/mit_interop against MIT krb5" OFF) if(ADCONNECT_MIT_INTEROP) find_library(GSSAPI_KRB5 gssapi_krb5 REQUIRED) find_path(GSSAPI_INCLUDE gssapi/gssapi.h REQUIRED) add_executable(mit_interop test/mit_interop.cpp) target_include_directories(mit_interop PRIVATE ${GSSAPI_INCLUDE}) target_link_libraries(mit_interop PRIVATE adconnect_core ${GSSAPI_KRB5}) endif() dist/adconnect.yaml +16 −2 Changes for dist/adconnect.yaml: 16 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -14,18 +14,32 @@ adconnect: # domain controllers, tried in turn (default: the domain name itself) servers: [dc1.example.com, dc2.example.com] # ldaps (port 636, default), starttls (389) or none (389, the bind # password in the clear: needs insecure: true) # ldaps (port 636, default), starttls (389) or none (389): with # auth: kerberos Kerberos seals the connection; with a simple bind the # password would go in the clear -- that needs insecure: true tls: ldaps # port: 636 # the CA that issued the DCs' certificates, PEM (default: the system bundle) # ca_file: /etc/adconnect/ca.pem # simple: bind_dn and its password, over LDAPS/StartTLS. kerberos: a # Kerberos ticket for the account, SASL GSSAPI -- the connection is # sealed by Kerberos, no TLS and no DC certificate needed (tls: none is # fine then); the clock must be within 5 minutes of the DCs' (NTP). # servers must be the DCs' DNS names then: tickets are for ldap/<name>. auth: simple # a service account allowed to read users and groups; as user@domain # or a DN. The password from a file of its own is better than here. bind_dn: svc-fenriros@example.com bind_password_file: /etc/adconnect/bind.secret # bind_password: secret # kerberos: the account (default: the user part of bind_dn), its realm # (default: the domain in upper case), a keytab instead of the password, # the KDCs (default: the servers) # kerberos_principal: svc-fenriros # kerberos_realm: EXAMPLE.COM # keytab: /etc/adconnect/svc.keytab # kdcs: [dc1.example.com, dc2.example.com] # ids: rid (id_base + the RID of the account's SID), rfc2307 (uidNumber # and gidNumber; accounts without them are left out) or guid (the Loading src/config.cpp +38 −2 Changes for src/config.cpp: 38 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -170,8 +170,6 @@ Config loadConfig(const std::string &path) { else if (tls == "none") cfg.tls = ldap::Tls::None; else throw std::runtime_error("tls: expected ldaps, starttls or none, got \"" + tls + "\""); readBool(conf, "insecure", cfg.insecure); if (cfg.tls == ldap::Tls::None && !cfg.insecure) throw std::runtime_error("tls: none sends the bind password in the clear -- set insecure: true to allow it"); readString(conf, "ca_file", cfg.caFile); readString(conf, "bind_dn", cfg.bindDn); Loading @@ -179,8 +177,42 @@ Config loadConfig(const std::string &path) { std::string secretFile; readString(conf, "bind_password_file", secretFile); if (!secretFile.empty()) cfg.bindPassword = readSecret(secretFile); std::string auth = "simple"; readString(conf, "auth", auth); auth = lower(auth); if (auth == "simple") cfg.auth = Auth::Simple; else if (auth == "kerberos") cfg.auth = Auth::Kerberos; else throw std::runtime_error("auth: expected simple or kerberos, got \"" + auth + "\""); readString(conf, "kerberos_principal", cfg.krbPrincipal); readString(conf, "kerberos_realm", cfg.krbRealm); readString(conf, "keytab", cfg.keytab); cfg.kdcs = readList(conf, "kdcs"); if (cfg.auth == Auth::Simple) { if (cfg.bindDn.empty() != cfg.bindPassword.empty()) throw std::runtime_error("bind_dn and bind_password (or bind_password_file) go together"); } else { /* svc@EXAMPLE.COM, or the user part of a bind_dn user@domain */ if (cfg.krbPrincipal.empty()) { size_t at = cfg.bindDn.find('@'); if (at == std::string::npos || cfg.bindDn.find('=') != std::string::npos) throw std::runtime_error("auth: kerberos needs kerberos_principal (or bind_dn as user@domain)"); cfg.krbPrincipal = cfg.bindDn.substr(0, at); } size_t at = cfg.krbPrincipal.find('@'); if (at != std::string::npos) { if (cfg.krbRealm.empty()) cfg.krbRealm = cfg.krbPrincipal.substr(at + 1); cfg.krbPrincipal.erase(at); } if (cfg.krbPrincipal.empty()) throw std::runtime_error("kerberos_principal: empty"); if (cfg.krbRealm.empty()) { cfg.krbRealm = cfg.domain; for (char &c : cfg.krbRealm) c = (char)toupper((unsigned char)c); } if (cfg.keytab.empty() && cfg.bindPassword.empty()) throw std::runtime_error("auth: kerberos needs a keytab or bind_password (bind_password_file)"); if (cfg.kdcs.empty()) cfg.kdcs = cfg.servers; } std::string mapping = "rid"; readString(conf, "id_mapping", mapping); Loading @@ -203,6 +235,10 @@ Config loadConfig(const std::string &path) { checkFilter("user_filter", cfg.userFilter); checkFilter("group_filter", cfg.groupFilter); /* Kerberos protects the connection itself; a simple bind needs TLS */ if (cfg.tls == ldap::Tls::None && cfg.auth == Auth::Simple && !cfg.insecure) throw std::runtime_error("tls: none sends the bind password in the clear -- use auth: kerberos, or set insecure: true"); readString(conf, "socket", cfg.socket); readInt(conf, "cache_ttl", cfg.cacheTtl, 0, 86400); readInt(conf, "negative_ttl", cfg.negativeTtl, 0, 86400); Loading src/config.h +11 −0 Changes for src/config.h: 11 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -37,6 +37,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace adconnect { enum class Auth { Simple, /* bind_dn and its password (LDAPS or StartTLS) */ Kerberos, /* SASL GSSAPI with a ticket for the service account */ }; enum class IdMapping { Rid, /* id_base + the RID of the objectSid (classic ids) */ Rfc2307, /* the uidNumber / gidNumber attributes */ Loading @@ -54,6 +59,12 @@ struct Config { std::string bindDn; std::string bindPassword; Auth auth = Auth::Simple; std::string krbPrincipal; /* svc-fenriros (default: bind_dn's user@ part) */ std::string krbRealm; /* default: the domain in upper case */ std::string keytab; /* instead of the password */ std::vector<std::string> kdcs; /* default: the servers */ IdMapping idMapping = IdMapping::Rid; unsigned long idBase = 200000; unsigned long idRange = 2000000; Loading src/directory.cpp +30 −1 Changes for src/directory.cpp: 30 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -32,6 +32,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <set> #include <stdexcept> #include "sasl.h" namespace adconnect { namespace { Loading Loading @@ -74,6 +76,33 @@ bool sidValid(const std::string &b) { /* ---- LdapSource ---- */ LdapSource::LdapSource(const Config &cfg) : _cfg(cfg), _conn(cfg.timeout * 1000) { if (cfg.auth != Auth::Kerberos) return; _krb = std::make_unique<krb5::Client>(cfg.kdcs, cfg.timeout * 1000); krb5::Principal me{ krb5::NtPrincipal, { cfg.krbPrincipal }, cfg.krbRealm }; if (!cfg.keytab.empty()) _krb->setKeytab(me, cfg.keytab); else _krb->setPassword(me, cfg.bindPassword); } /* a simple bind, or GSSAPI with a ticket for ldap/<the server's name> */ void LdapSource::bind(const std::string &host) { if (_krb) { std::string h = host; for (char &c : h) c = (char)tolower((unsigned char)c); krb5::Credentials ticket; try { ticket = _krb->serviceTicket(krb5::Principal{ krb5::SrvInst, { "ldap", h }, _cfg.krbRealm }); } catch (const krb5::Error &e) { _conn.close(); /* a KDC that answered with an error is no transport failure */ throw ldap::Error(e.code() == -1 ? -1 : 49, e.what()); } gssapiBind(_conn, ticket); } else if (!_cfg.bindDn.empty()) { _conn.bind(_cfg.bindDn, _cfg.bindPassword); } } void LdapSource::ensure() { if (_conn.connected()) return; netplus::TlsTrustPolicy trust; Loading @@ -86,7 +115,7 @@ void LdapSource::ensure() { try { trust.expectedHostname = host; _conn.connect(host, _cfg.ldapPort(), _cfg.tls, trust); if (!_cfg.bindDn.empty()) _conn.bind(_cfg.bindDn, _cfg.bindPassword); bind(host); _server = n; return; } catch (const ldap::Error &e) { Loading Loading
CMakeLists.txt +20 −2 Changes for CMakeLists.txt: 20 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -6,8 +6,10 @@ project(adconnect VERSION 0.1.0 LANGUAGES CXX) # systempp's <userdb.h>). # # adconnectd the daemon (root): reads /etc/adconnect/adconnect.yaml # (libconfplus), binds to a domain controller over LDAPS or # StartTLS (libnetplus), answers on /run/adconnect.sock and # (libconfplus), binds to a domain controller -- simple bind # over LDAPS or StartTLS (libnetplus), or Kerberos: SASL # GSSAPI with its own Kerberos client (src/krb5*.cpp, # gssapi.cpp, sasl.cpp) -- answers on /run/adconnect.sock and # caches what it was told -- on disk too, so that users and # groups stay known while no domain controller is reachable # adconnect.so the userdb module /etc/userdb.yaml loads into every Loading Loading @@ -50,6 +52,10 @@ add_library(adconnect_core STATIC src/config.cpp src/directory.cpp src/cache.cpp src/krb5crypto.cpp src/krb5.cpp src/gssapi.cpp src/sasl.cpp ) target_link_libraries(adconnect_core PUBLIC adconnect_proto confplus::confplus netplus::netplus Threads::Threads) Loading Loading @@ -78,3 +84,15 @@ if(BUILD_TESTING) add_dependencies(adconnect_test adconnectd adconnect) add_test(NAME adconnect_test COMMAND adconnect_test) endif() # Interoperability with MIT krb5 (KDC and GSS-API acceptor): needs MIT's # gssapi headers and library, so not on FenrirOS -- test/mit_interop.sh # sets up a throw-away KDC and runs it. option(ADCONNECT_MIT_INTEROP "build test/mit_interop against MIT krb5" OFF) if(ADCONNECT_MIT_INTEROP) find_library(GSSAPI_KRB5 gssapi_krb5 REQUIRED) find_path(GSSAPI_INCLUDE gssapi/gssapi.h REQUIRED) add_executable(mit_interop test/mit_interop.cpp) target_include_directories(mit_interop PRIVATE ${GSSAPI_INCLUDE}) target_link_libraries(mit_interop PRIVATE adconnect_core ${GSSAPI_KRB5}) endif()
dist/adconnect.yaml +16 −2 Changes for dist/adconnect.yaml: 16 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -14,18 +14,32 @@ adconnect: # domain controllers, tried in turn (default: the domain name itself) servers: [dc1.example.com, dc2.example.com] # ldaps (port 636, default), starttls (389) or none (389, the bind # password in the clear: needs insecure: true) # ldaps (port 636, default), starttls (389) or none (389): with # auth: kerberos Kerberos seals the connection; with a simple bind the # password would go in the clear -- that needs insecure: true tls: ldaps # port: 636 # the CA that issued the DCs' certificates, PEM (default: the system bundle) # ca_file: /etc/adconnect/ca.pem # simple: bind_dn and its password, over LDAPS/StartTLS. kerberos: a # Kerberos ticket for the account, SASL GSSAPI -- the connection is # sealed by Kerberos, no TLS and no DC certificate needed (tls: none is # fine then); the clock must be within 5 minutes of the DCs' (NTP). # servers must be the DCs' DNS names then: tickets are for ldap/<name>. auth: simple # a service account allowed to read users and groups; as user@domain # or a DN. The password from a file of its own is better than here. bind_dn: svc-fenriros@example.com bind_password_file: /etc/adconnect/bind.secret # bind_password: secret # kerberos: the account (default: the user part of bind_dn), its realm # (default: the domain in upper case), a keytab instead of the password, # the KDCs (default: the servers) # kerberos_principal: svc-fenriros # kerberos_realm: EXAMPLE.COM # keytab: /etc/adconnect/svc.keytab # kdcs: [dc1.example.com, dc2.example.com] # ids: rid (id_base + the RID of the account's SID), rfc2307 (uidNumber # and gidNumber; accounts without them are left out) or guid (the Loading
src/config.cpp +38 −2 Changes for src/config.cpp: 38 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -170,8 +170,6 @@ Config loadConfig(const std::string &path) { else if (tls == "none") cfg.tls = ldap::Tls::None; else throw std::runtime_error("tls: expected ldaps, starttls or none, got \"" + tls + "\""); readBool(conf, "insecure", cfg.insecure); if (cfg.tls == ldap::Tls::None && !cfg.insecure) throw std::runtime_error("tls: none sends the bind password in the clear -- set insecure: true to allow it"); readString(conf, "ca_file", cfg.caFile); readString(conf, "bind_dn", cfg.bindDn); Loading @@ -179,8 +177,42 @@ Config loadConfig(const std::string &path) { std::string secretFile; readString(conf, "bind_password_file", secretFile); if (!secretFile.empty()) cfg.bindPassword = readSecret(secretFile); std::string auth = "simple"; readString(conf, "auth", auth); auth = lower(auth); if (auth == "simple") cfg.auth = Auth::Simple; else if (auth == "kerberos") cfg.auth = Auth::Kerberos; else throw std::runtime_error("auth: expected simple or kerberos, got \"" + auth + "\""); readString(conf, "kerberos_principal", cfg.krbPrincipal); readString(conf, "kerberos_realm", cfg.krbRealm); readString(conf, "keytab", cfg.keytab); cfg.kdcs = readList(conf, "kdcs"); if (cfg.auth == Auth::Simple) { if (cfg.bindDn.empty() != cfg.bindPassword.empty()) throw std::runtime_error("bind_dn and bind_password (or bind_password_file) go together"); } else { /* svc@EXAMPLE.COM, or the user part of a bind_dn user@domain */ if (cfg.krbPrincipal.empty()) { size_t at = cfg.bindDn.find('@'); if (at == std::string::npos || cfg.bindDn.find('=') != std::string::npos) throw std::runtime_error("auth: kerberos needs kerberos_principal (or bind_dn as user@domain)"); cfg.krbPrincipal = cfg.bindDn.substr(0, at); } size_t at = cfg.krbPrincipal.find('@'); if (at != std::string::npos) { if (cfg.krbRealm.empty()) cfg.krbRealm = cfg.krbPrincipal.substr(at + 1); cfg.krbPrincipal.erase(at); } if (cfg.krbPrincipal.empty()) throw std::runtime_error("kerberos_principal: empty"); if (cfg.krbRealm.empty()) { cfg.krbRealm = cfg.domain; for (char &c : cfg.krbRealm) c = (char)toupper((unsigned char)c); } if (cfg.keytab.empty() && cfg.bindPassword.empty()) throw std::runtime_error("auth: kerberos needs a keytab or bind_password (bind_password_file)"); if (cfg.kdcs.empty()) cfg.kdcs = cfg.servers; } std::string mapping = "rid"; readString(conf, "id_mapping", mapping); Loading @@ -203,6 +235,10 @@ Config loadConfig(const std::string &path) { checkFilter("user_filter", cfg.userFilter); checkFilter("group_filter", cfg.groupFilter); /* Kerberos protects the connection itself; a simple bind needs TLS */ if (cfg.tls == ldap::Tls::None && cfg.auth == Auth::Simple && !cfg.insecure) throw std::runtime_error("tls: none sends the bind password in the clear -- use auth: kerberos, or set insecure: true"); readString(conf, "socket", cfg.socket); readInt(conf, "cache_ttl", cfg.cacheTtl, 0, 86400); readInt(conf, "negative_ttl", cfg.negativeTtl, 0, 86400); Loading
src/config.h +11 −0 Changes for src/config.h: 11 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -37,6 +37,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace adconnect { enum class Auth { Simple, /* bind_dn and its password (LDAPS or StartTLS) */ Kerberos, /* SASL GSSAPI with a ticket for the service account */ }; enum class IdMapping { Rid, /* id_base + the RID of the objectSid (classic ids) */ Rfc2307, /* the uidNumber / gidNumber attributes */ Loading @@ -54,6 +59,12 @@ struct Config { std::string bindDn; std::string bindPassword; Auth auth = Auth::Simple; std::string krbPrincipal; /* svc-fenriros (default: bind_dn's user@ part) */ std::string krbRealm; /* default: the domain in upper case */ std::string keytab; /* instead of the password */ std::vector<std::string> kdcs; /* default: the servers */ IdMapping idMapping = IdMapping::Rid; unsigned long idBase = 200000; unsigned long idRange = 2000000; Loading
src/directory.cpp +30 −1 Changes for src/directory.cpp: 30 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -32,6 +32,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <set> #include <stdexcept> #include "sasl.h" namespace adconnect { namespace { Loading Loading @@ -74,6 +76,33 @@ bool sidValid(const std::string &b) { /* ---- LdapSource ---- */ LdapSource::LdapSource(const Config &cfg) : _cfg(cfg), _conn(cfg.timeout * 1000) { if (cfg.auth != Auth::Kerberos) return; _krb = std::make_unique<krb5::Client>(cfg.kdcs, cfg.timeout * 1000); krb5::Principal me{ krb5::NtPrincipal, { cfg.krbPrincipal }, cfg.krbRealm }; if (!cfg.keytab.empty()) _krb->setKeytab(me, cfg.keytab); else _krb->setPassword(me, cfg.bindPassword); } /* a simple bind, or GSSAPI with a ticket for ldap/<the server's name> */ void LdapSource::bind(const std::string &host) { if (_krb) { std::string h = host; for (char &c : h) c = (char)tolower((unsigned char)c); krb5::Credentials ticket; try { ticket = _krb->serviceTicket(krb5::Principal{ krb5::SrvInst, { "ldap", h }, _cfg.krbRealm }); } catch (const krb5::Error &e) { _conn.close(); /* a KDC that answered with an error is no transport failure */ throw ldap::Error(e.code() == -1 ? -1 : 49, e.what()); } gssapiBind(_conn, ticket); } else if (!_cfg.bindDn.empty()) { _conn.bind(_cfg.bindDn, _cfg.bindPassword); } } void LdapSource::ensure() { if (_conn.connected()) return; netplus::TlsTrustPolicy trust; Loading @@ -86,7 +115,7 @@ void LdapSource::ensure() { try { trust.expectedHostname = host; _conn.connect(host, _cfg.ldapPort(), _cfg.tls, trust); if (!_cfg.bindDn.empty()) _conn.bind(_cfg.bindDn, _cfg.bindPassword); bind(host); _server = n; return; } catch (const ldap::Error &e) { Loading