Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bugfix] Added APOptions []int in client.GSSAPIBindRequest(...) and client.InitSecContext(...), fixes #536 #537

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

p0dalirius
Copy link

@p0dalirius p0dalirius commented Nov 14, 2024

Fixing Kerberos authentication due to missing APOption "MutualRequired"

Overview of the fix

In client.InitSecContext(...)

I have changed the client.InitSecContext(...) prototype from:

func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error)

to:

func (client *Client) InitSecContext(target string, input []byte, APOptions []int) ([]byte, bool, error)

to be able to pass specific flags through the APOptions []int to the call to spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, APOptions)

In client.GSSAPIBind(...)

I have left this function as it was, so it can be used in the generic case of GSSAPI authentication without the need to pass specific AP Options flags in parameters.

In client.GSSAPIBindRequest(...)

I have changed the client.GSSAPIBindRequest(...) prototype from:

func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error

to

func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest, APOptions []int) error

Example of a working code for Kerberos authentication

package main

import (
	"encoding/hex"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/go-ldap/ldap/v3"
	"github.com/jcmturner/gokrb5/v8/iana/flags"
	"github.com/go-ldap/ldap/v3/gssapi"
	"github.com/jcmturner/gokrb5/v8/client"
	"github.com/jcmturner/gokrb5/v8/config"
)

func main() {
	fqdnLDAPHost := "SRV-DC01.lab.local"
	baseDN := "DC=LAB,DC=local"

	realm := "lab.local"
	realm = strings.ToUpper(realm)
	// This is always in uppercase, if not we get the error:
	// error performing GSSAPI bind: [Root cause: KRBMessage_Handling_Error]
	// | KRBMessage_Handling_Error: AS Exchange Error: AS_REP is not valid or client password/keytab incorrect
	// |  | KRBMessage_Handling_Error: CRealm in response does not match what was requested.
	// |  |  | Requested: lab.local;
	// |  |  | Reply: lab.local
	// | 2024/10/08 15:36:16 error querying AD: LDAP Result Code 1 "Operations Error": 000004DC: LdapErr: DSID-0C090A5C,
	// | comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4563

	username := "Administrator"
	// error performing GSSAPI bind: [Root cause: KDC_Error] KDC_Error: AS Exchange Error: kerberos error response from KDC:
	// KRB Error: (6) KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database
	// KDC_ERR_C_PRINCIPAL_UNKNOWN (error code 6) for these means that the domain controller to which the request
	// was made does not host the account and the client should choose a different domain controller.
	// src: https://learn.microsoft.com/en-us/troubleshoot/windows-server/certificates-and-public-key-infrastructure-pki/kdc-err-c-principal-unknown-s4u2self-request
	// ==> This means this username does not exist

	password := "Admin123!"

	servicePrincipalName := fmt.Sprintf("ldap/%s", fqdnLDAPHost)

	krb5Conf := config.New()
	// LibDefaults
	krb5Conf.LibDefaults.AllowWeakCrypto = true
	krb5Conf.LibDefaults.DefaultRealm = realm
	krb5Conf.LibDefaults.DNSLookupRealm = false
	krb5Conf.LibDefaults.DNSLookupKDC = false
	krb5Conf.LibDefaults.TicketLifetime = time.Duration(24) * time.Hour
	krb5Conf.LibDefaults.RenewLifetime = time.Duration(24*7) * time.Hour
	krb5Conf.LibDefaults.Forwardable = true
	krb5Conf.LibDefaults.Proxiable = true
	krb5Conf.LibDefaults.RDNS = false
	krb5Conf.LibDefaults.UDPPreferenceLimit = 1 // Force use of tcp
	krb5Conf.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTGSEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTktEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.PreferredPreauthTypes = []int{18, 17, 23}

	// Realms
	krb5Conf.Realms = append(krb5Conf.Realms, config.Realm{
		Realm:         realm,
		AdminServer:   []string{fqdnLDAPHost},
		DefaultDomain: realm,
		KDC:           []string{fmt.Sprintf("%s:88", fqdnLDAPHost)},
		KPasswdServer: []string{fmt.Sprintf("%s:464", fqdnLDAPHost)},
		MasterKDC:     []string{fqdnLDAPHost},
	})

	// Domain Realm
	krb5Conf.DomainRealm[strings.ToLower(realm)] = realm
	krb5Conf.DomainRealm[fmt.Sprintf(".%s", strings.ToLower(realm))] = realm

	printKrb5Conf(krb5Conf)

	// Connect to LDAP server
	bindString := fmt.Sprintf("ldaps://%s:636", fqdnLDAPHost)
	ldapConnection, err := ldap.DialURL(
		bindString,
		ldap.DialWithTLSConfig(
			&tls.Config{
				InsecureSkipVerify: true,
			},
		),
	)
	if err != nil {
		log.Printf("[error] ldap.DialURL(\"%s\"): %s\n", bindString, err)
		return
	} else {
		log.Printf("[debug] ldap.DialURL(\"%s\"): success\n", bindString)
	}
	ldapConnection.Debug = true

	// Initialize kerberos client
	// Inspired from: https://github.com/go-ldap/ldap/blob/06d50d1ad03bcd323e48f2fe174d95ceb31b8b90/v3/gssapi/client.go#L51
	kerberosClient := gssapi.Client{
		Client: client.NewWithPassword(
			username,
			realm,
			password,
			krb5Conf,
			// Active Directory does not commonly support FAST negotiationso you will need to disable this on the client.
			// If this is the case you will see this error: KDC did not respond appropriately to FAST negotiation
			// https://github.com/jcmturner/gokrb5/blob/master/USAGE.md#active-directory-kdc-and-fast-negotiation
			client.DisablePAFXFAST(true),
		),
	}
	defer kerberosClient.Close()

	// Initiating ldap GSSAPIBind
	err = ldapConnection.GSSAPIBindRequest(
		&kerberosClient,
		&ldap.GSSAPIBindRequest{
			ServicePrincipalName: servicePrincipalName,
			AuthZID:              "",
		},
		[]int{flags.APOptionMutualRequired},
        )
	if err != nil {
		log.Printf("[error] ldapConnection.GSSAPIBind(): %s\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.GSSAPIBind(): success\n")
	}

	// Successfully bound
	searchRequest := ldap.NewSearchRequest(
		baseDN,
		ldap.ScopeWholeSubtree,
		ldap.NeverDerefAliases,
		0,
		0,
		false,
		"(objectClass=user)",
		[]string{"distinguishedName"},
		nil,
	)
	ldapResults, err := ldapConnection.SearchWithPaging(searchRequest, 1000)
	if err != nil {
		log.Fatalf("[error] ldapConnection.Search(): %v\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.Search(): success\n")
	}

	for _, entry := range ldapResults.Entries {
		fmt.Printf(" - %s", entry.DN)
	}

	log.Printf("[debug] All done!\n")
}
}

Summary

These APOptions []int can now be set when calling client.GSSAPIBindRequest(...), which will then pass it to the underlying client.InitSecContext(...), which will then be processed in the call to spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, APOptions)

Best Regards,

@p0dalirius p0dalirius force-pushed the fix-ldap-gssapi-kerberos-auth-on-windows-active-directory branch from c76a77b to 3a20a5c Compare November 14, 2024 15:14
@cpuschma
Copy link
Member

Hi @p0dalirius,

thank you very much for your PR and the work you put into debugging this issue (#536)! My strengths are not at all in Kerberos and my knowledge of the protocol is practically zero, so thank you again!

But: Modifying the signature of an existing function will definitely interfere with existing code. Even the addition of changes that are only noticed by linters has caused trouble in the past (#463).

If there is really no other way to implement Kerberos authentication, I would be willing to do so and include it in the next release, but only if no one objects. @go-ldap/committers thoughts?

@p0dalirius
Copy link
Author

p0dalirius commented Nov 18, 2024

@cpuschma, before merging, It would be nice if someone can try to connect to a UNIX ldap server with GSSAPI to confirm that it still works with this fix

I will try to setup one and test it

@stefanmcshane
Copy link
Contributor

Hi @p0dalirius,

thank you very much for your PR and the work you put into debugging this issue (#536)! My strengths are not at all in Kerberos and my knowledge of the protocol is practically zero, so thank you again!

But: Modifying the signature of an existing function will definitely interfere with existing code. Even the addition of changes that are only noticed by linters has caused trouble in the past (#463).

If there is really no other way to implement Kerberos authentication, I would be willing to do so and include it in the next release, but only if no one objects. @go-ldap/committers thoughts?

Using functional options instead of a slice of ints should not break existing implementations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants