]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Add LDAP GSSAPI authenticator.
authorGrégory Oestreicher <greg@kamago.net>
Wed, 14 Sep 2016 20:25:51 +0000 (22:25 +0200)
committerGrégory Oestreicher <greg@kamago.net>
Tue, 28 Feb 2017 21:34:29 +0000 (22:34 +0100)
Also add in the LDAP backend docs the new configuration settings
to use it.

docs/markdown/authoritative/backend-ldap.md
modules/ldapbackend/ldapauthenticator.cc
modules/ldapbackend/ldapauthenticator_p.hh
modules/ldapbackend/ldapbackend.cc
modules/ldapbackend/powerldap.cc

index a875e614df6d57497cb8d7d698e6f0a348bc0eb7..522e0ceb82c7536357b4bcc618ac2502cb7faca2 100644 (file)
@@ -58,20 +58,25 @@ There can be multiple LDAP URIs specified for load balancing and high availabili
 In case the used LDAP client library doesn't support LDAP URIs as connection parameter, use plain host names or IP addresses instead (both may optionally be followed by a colon and the port).
 
 ## `ldap-starttls`
-(default "no") : Use TLS encrypted connections to the LDAP server.
-This is only allowed if `ldap-host` is a `ldap://` URI or a host name / IP address.
+(default "no") : Use TLS encrypted connections to the LDAP server. This is only allowed if ldap-host is a <ldap://> URI or a host name / IP address.
 
-## `ldap-basedn`
-(default "") : The PowerDNS LDAP DNS backend searches below this path for objects containing the specified DNS information.
-The retrieval of attributes is limited to this subtree.
-This option must be set to the path according to the layout of the LDAP tree, e.g. `ou=hosts,o=example,c=net` is the DN to my objects containing the DNS information.
+## `ldap-authmethod`
+(default: "simple") : How to authenticate to the LDAP server. Actually only two methods are supported: "simple", which uses the classical DN / password, or "gssapi", which requires a Kerberos keytab.
 
 ## `ldap-binddn`
-(default "") : Path to the object to authenticate against.
-Should only be used if the LDAP server doesn't support anonymous binds.
+(default "") : Path to the object to authenticate against. Should only be used, if the LDAP server doesn't support anonymous binds and with the "simple" authmethod.
 
 ## `ldap-secret`
-(default "") : Password for authentication against the object specified by `ldap-binddn`.
+(default "") : Password for authentication against the object specified by ldap-binddn. Only used when "authmethod" is "simple".
+
+## `ldap-krb5-keytab`
+(default: "") : Full path to the keytab file to use to authenticate. This is only used when "authmethod" is set to "gssapi". The keytab must, ideally, contain only one principal (or to put it otherwise, only the first principal found in the keytab will be used).
+
+## `ldap-krb5-ccache`
+(default: "") : Full path to the Kerberos credential cache file to use. Actually only files are supported, and the "FILE:" prefix must not be set. The PowerDNS process must be able to write to this file and it *must* be the only one able to read it.
+
+## `ldap-basedn`
+(default "") : The PowerDNS LDAP DNS backend searches below this path for objects containing the specified DNS information. The retrieval of attributes is limited to this subtree. This option must be set to the path according to the layout of your LDAP tree, e.g. ou=hosts,o=linuxnetworks,c=de is the DN to my objects containing the DNS information.
 
 ## `ldap-method`
 (default "simple") :
index 7510a5df9b4ba2593907eb557b23a50d14d396bf..2632facf7a6739b29cf42f763c6b1e41c1bbcc9f 100644 (file)
@@ -17,6 +17,7 @@
  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
+#include <krb5.h>
 #include <pdns/logger.hh>
 #include "ldapauthenticator_p.hh"
 #include "ldaputils.hh"
@@ -69,3 +70,191 @@ void LdapSimpleAuthenticator::fillLastError( LDAP* conn, int code )
 {
        lastError = ldapGetError( conn, code );
 }
+
+/*****************************
+ * 
+ * LdapGssapiAuthenticator
+ * 
+ ****************************/
+
+static int ldapGssapiAuthenticatorSaslInteractCallback( LDAP *conn, unsigned flags, void *defaults, void *in )
+{
+       return LDAP_SUCCESS;
+}
+
+LdapGssapiAuthenticator::LdapGssapiAuthenticator( const std::string& kt, const std::string &ccache, int tmout )
+       : keytabFile( kt ), cCacheFile( ccache ), timeout( tmout )
+{
+}
+
+bool LdapGssapiAuthenticator::authenticate( LDAP *conn )
+{
+       int code = attemptAuth( conn );
+
+       if ( code == -1 ) {
+               return false;
+       }
+       else if ( code == -2 ) {
+               // Here it may be possible to retry after obtainting a fresh ticket
+               L<<Logger::Debug << "LDAP GSSAPI" << "No TGT found, trying to acquire a new one" << std::endl;
+               code = updateTgt();
+
+               if ( attemptAuth( conn ) != 0 ) {
+                       L<<Logger::Error << "LDAP GSSAPI" << "Failed to acquire a TGT" << std::endl;
+                       return false;
+               }
+       }
+
+       return true;
+}
+
+std::string LdapGssapiAuthenticator::getError() const
+{
+       return lastError;
+}
+
+int LdapGssapiAuthenticator::attemptAuth( LDAP *conn )
+{
+       // Create SASL defaults
+       SaslDefaults defaults;
+       char *ldapOption = 0;
+
+       ldap_get_option( conn, LDAP_OPT_X_SASL_MECH, ldapOption );
+       if ( !ldapOption )
+               defaults.mech = std::string( "GSSAPI" );
+       else
+               defaults.mech = std::string( ldapOption );
+       ldap_memfree( ldapOption );
+
+       ldap_get_option( conn, LDAP_OPT_X_SASL_REALM, ldapOption );
+       if ( ldapOption )
+               defaults.realm = std::string( ldapOption );
+       ldap_memfree( ldapOption );
+
+       ldap_get_option( conn, LDAP_OPT_X_SASL_AUTHCID, ldapOption );
+       if ( ldapOption )
+               defaults.authcid = std::string( ldapOption );
+       ldap_memfree( ldapOption );
+
+       ldap_get_option( conn, LDAP_OPT_X_SASL_AUTHZID, ldapOption );
+       if ( ldapOption )
+               defaults.authzid = std::string( ldapOption );
+       ldap_memfree( ldapOption );
+
+       // And now try to bind
+       int rc = ldap_sasl_interactive_bind_s( conn, "", defaults.mech.c_str(),
+                                           NULL, NULL, LDAP_SASL_QUIET,
+                                           ldapGssapiAuthenticatorSaslInteractCallback, &defaults );
+       L<<Logger::Debug << "LDAP GSSAPI" << "ldap_sasl_interactive_bind_s returned " << rc << std::endl;
+
+       if ( rc == LDAP_LOCAL_ERROR ) {
+               // This may mean that the ticket has expired, so let the caller know
+               lastError = ldapGetError( conn, rc );
+               return -2;
+       }
+       else if ( rc != LDAP_SUCCESS ) {
+               lastError = ldapGetError( conn, rc );
+               return -1;
+       }
+
+       return rc;
+}
+
+int LdapGssapiAuthenticator::updateTgt()
+{
+       krb5_error_code code;
+       krb5_context context;
+       krb5_creds credentials;
+       krb5_keytab keytab;
+       krb5_principal principal;
+       krb5_ccache ccache;
+       krb5_get_init_creds_opt *options;
+
+       if ( ( code = krb5_init_context( &context ) ) != 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "Failed to init krb5 context" << std::endl;
+               return code;
+       }
+
+       if ( !keytabFile.empty() ) {
+               std::string keytabStr( "FILE:" + keytabFile );
+               code = krb5_kt_resolve( context, keytabStr.c_str(), &keytab );
+       }
+       else {
+               code = krb5_kt_default( context, &keytab );
+       }
+       
+       if ( code != 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "krb5 error: " << std::string( krb5_get_error_message( context, code ) ) << std::endl;
+               return code;
+       }
+
+       // Extract the principal name from the keytab
+       krb5_kt_cursor cursor;
+       if ( ( code = krb5_kt_start_seq_get( context, keytab, &cursor ) ) != 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "krb5 error: " << std::string( krb5_get_error_message( context, code ) ) << std::endl;
+               krb5_kt_close( context, keytab );
+               return code;
+       }
+
+       krb5_keytab_entry entry;
+       if ( ( code = krb5_kt_next_entry( context, keytab, &entry, &cursor ) ) == 0 ) {
+               code = krb5_copy_principal( context, entry.principal, &principal );
+               krb5_kt_free_entry( context, &entry );
+       }
+
+       krb5_kt_end_seq_get( context, keytab, &cursor );
+       if ( code != 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "krb5 error: " << std::string( krb5_get_error_message( context, code ) ) << std::endl;
+               krb5_kt_close( context, keytab );
+               krb5_free_principal( context, principal );
+               return code;
+       }
+
+       // Locate the credentials cache file
+       if ( !cCacheFile.empty() ) {
+               std::string cCacheStr( "FILE:" + cCacheFile );
+               code = krb5_cc_resolve( context, cCacheStr.c_str(), &ccache );
+       }
+       else {
+               code = krb5_cc_default( context, &ccache );
+       }
+
+       if ( code != 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "krb5 error: " << std::string( krb5_get_error_message( context, code ) ) << std::endl;
+               krb5_kt_close( context, keytab );
+               krb5_free_principal( context, principal );
+               return code;
+       }
+
+       // Initialize the credentials cache file
+       if ( ( code = krb5_cc_initialize( context, ccache, principal ) ) != 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "krb5 error: " << std::string( krb5_get_error_message( context, code ) ) << std::endl;
+               krb5_kt_close( context, keytab );
+               krb5_free_principal( context, principal );
+               return code;
+       }
+
+       if ( ( code = krb5_get_init_creds_opt_alloc( context, &options ) ) != 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "krb5 error: " << std::string( krb5_get_error_message( context, code ) ) << std::endl;
+               krb5_kt_close( context, keytab );
+               krb5_free_principal( context, principal );
+               return code;
+       }
+       krb5_get_init_creds_opt_set_default_flags( context, "pdns", NULL, options );
+
+       // And finally get the TGT!
+       code = krb5_get_init_creds_keytab( context, &credentials, principal, keytab, 0, NULL, options );
+       krb5_get_init_creds_opt_free( context, options );
+       krb5_kt_close( context, keytab );
+       krb5_free_principal( context, principal );
+
+       if ( code == 0 ) {
+               L<<Logger::Error << "LDAP GSSAPI" << "krb5 error: " << std::string( krb5_get_error_message( context, code ) ) << std::endl;
+               code = krb5_cc_store_cred( context, ccache, &credentials );
+               krb5_free_cred_contents( context, &credentials );
+               krb5_cc_close( context, ccache );
+       }
+
+       krb5_free_context( context );
+       return code;
+}
index 0168cb2050dbe3691f3011399688c269f195efa4..6b120b69a265285b74ea96694918a3324eecbf68 100644 (file)
 #ifndef LDAPAUTHENTICATOR_P_HH
 #define LDAPAUTHENTICATOR_P_HH
 
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_DEFAULT_FLAGS
+#define krb5_get_init_creds_opt_set_default_flags( a, b, c, d ) /* This does not exist with MIT Kerberos */
+#endif
+
 class LdapSimpleAuthenticator : public LdapAuthenticator
 {
        std::string binddn;
@@ -36,4 +40,27 @@ public:
        virtual std::string getError() const;
 };
 
+class LdapGssapiAuthenticator : public LdapAuthenticator
+{
+       std::string keytabFile;
+       std::string cCacheFile;
+       int timeout;
+       std::string lastError;
+       
+       struct SaslDefaults {
+               std::string mech;
+               std::string realm;
+               std::string authcid;
+               std::string authzid;
+       };
+
+       int attemptAuth( LDAP *conn );
+       int updateTgt();
+
+public:
+       LdapGssapiAuthenticator( const std::string &keytab, const std::string &credsCache, int timeout );
+       virtual bool authenticate( LDAP *conn );
+       virtual std::string getError() const;
+};
+
 #endif // LDAPAUTHENTICATOR_P_HH
index a14c81bd52fd939e3b9cb27a9e9b1d4ce78f4fe3..88ffa9db503bf0d89af427a42ac8cc19feb5e2c2 100644 (file)
@@ -81,8 +81,14 @@ LdapBackend::LdapBackend( const string &suffix )
 
                m_pldap = new PowerLDAP( hoststr.c_str(), LDAP_PORT, mustDo( "starttls" ) );
                m_pldap->setOption( LDAP_OPT_DEREF, LDAP_DEREF_ALWAYS );
-               m_pldap->bind( getArg( "binddn" ), getArg( "secret" ), LDAP_AUTH_SIMPLE, getArgAsNum( "timeout" ) );
-               m_authenticator = new LdapSimpleAuthenticator( getArg( "binddn" ), getArg( "secret" ), getArgAsNum( "timeout" ) );
+
+               string bindmethod = getArg( "bindmethod" );
+               if ( bindmethod == "gssapi" ) {
+                       m_authenticator = new LdapGssapiAuthenticator( getArg( "krb5-keytab" ), getArg( "krb5-ccache" ), getArgAsNum( "timeout" ) );
+               }
+               else {
+                       m_authenticator = new LdapSimpleAuthenticator( getArg( "binddn" ), getArg( "secret" ), getArgAsNum( "timeout" ) );
+               }
                m_pldap->bind( m_authenticator );
 
                L << Logger::Notice << m_myname << " Ldap connection succeeded" << endl;
@@ -540,8 +546,11 @@ public:
                declare( suffix, "starttls", "Use TLS to encrypt connection (unused for LDAP URIs)", "no" );
                declare( suffix, "basedn", "Search root in ldap tree (must be set)","" );
                declare( suffix, "basedn-axfr-override", "Override base dn for AXFR subtree search", "no" );
+               declare( suffix, "bindmethod", "Bind method to use (simple or gssapi)", "simple" );
                declare( suffix, "binddn", "User dn for non anonymous binds","" );
                declare( suffix, "secret", "User password for non anonymous binds", "" );
+               declare( suffix, "krb5-keytab", "The keytab to use for GSSAPI authentication", "" );
+               declare( suffix, "krb5-ccache", "The credentials cache used for GSSAPI authentication", "" );
                declare( suffix, "timeout", "Seconds before connecting to server fails", "5" );
                declare( suffix, "method", "How to search entries (simple, strict or tree)", "simple" );
                declare( suffix, "filter-axfr", "LDAP filter for limiting AXFR results", "(:target:)" );
index f6c48c39f61a21f7f4d8af88ea78f0d9a293bf92..646c642a52b098dc428dd53af6aecdb8bcb65b01 100644 (file)
@@ -155,14 +155,13 @@ void PowerLDAP::simpleBind( const string& ldapbinddn, const string& ldapsecret )
 
 int PowerLDAP::search( const string& base, int scope, const string& filter, const char** attr )
 {
-        int msgid, rc;
+       int msgid, rc;
 
-        if( ( rc = ldap_search_ext( d_ld, base.c_str(), scope, filter.c_str(), const_cast<char**> (attr), 0, NULL, NULL, NULL, LDAP_NO_LIMIT, &msgid ) ) != LDAP_SUCCESS )
-        {
-               throw LDAPException( "Starting LDAP search: " + getError( rc ) );
-        }
+       if ( ( rc = ldap_search_ext( d_ld, base.c_str(), scope, filter.c_str(), const_cast<char**> (attr), 0, NULL, NULL, NULL, LDAP_NO_LIMIT, &msgid ) ) ) {
+               throw LDAPException( "Starting LDAP search: " + getError( rc ) );
+       }
 
-        return msgid;
+       return msgid;
 }
 
 
@@ -193,19 +192,32 @@ bool PowerLDAP::getSearchEntry( int msgid, sentry_t& entry, bool dn, int timeout
         vector<string> values;
         LDAPMessage* result;
         LDAPMessage* object;
-
-
-        if( ( i = waitResult( msgid, timeout, &result ) ) == LDAP_RES_SEARCH_RESULT )
-        {
-               ldap_msgfree( result );
-               return false;
-        }
-
-        if( i != LDAP_RES_SEARCH_ENTRY )
-        {
-               ldap_msgfree( result );
-               throw LDAPException( "Search returned an unexpected result" );
-        }
+       bool hasResult = false;
+
+       while ( !hasResult ) {
+               i = waitResult( msgid, timeout, &result );
+               // Here we deliberately ignore LDAP_RES_SEARCH_REFERENCE as we don't follow them.
+               // Instead we get the next result.
+               // If the function returned an error (i <= 0) we'll deal with after this loop too.
+               if ( i == LDAP_RES_SEARCH_ENTRY || i == LDAP_RES_SEARCH_RESULT || i <= 0 )
+                       hasResult = true;
+       }
+
+       if ( i == -1 ) {
+               // Error while retrieving the message
+               throw LDAPException( "Error when retrieving LDAP result: " + getError() );
+       }
+
+       if ( i == 0 ) {
+               // Timeout expired before the message could be retrieved
+               throw LDAPTimeout();
+       }
+
+       if ( i == LDAP_RES_SEARCH_RESULT ) {
+               // We're done with this request
+               ldap_msgfree( result );
+               return false;
+       }
 
         if( ( object = ldap_first_entry( d_ld, result ) ) == NULL )
         {