Implementing Kerberos On Linux in the Go SQL Server Driver

In today’s digital landscape, the demand for secure and reliable communication between databases and applications has never been greater. As an active user and contributor to the Microsoft fork of the go-mssqldb SQL driver one of the noticeable omissions was the ability to use integrated authentication i.e., Active Directory or Kerberos authentication when hosted on Linux.

This article documents an approach for adding cross-platform Kerberos authentication support into the go-mssqldb SQL Server driver and the technologies involved. It delves into what Kerberos is and how it works, as well as the role of SPNEGO. Moreover, it will explore the alterations made to the go-mssqldb driver design to enable SSPI authentication to be added as a pluggable layer within the driver.

How does Kerberos authentication work?

Kerberos is a network protocol for authenticating requests between two or more trusted hosts across an untrusted network. This authentication mechanism provides a foundation for accessing network resources while safeguarding against unauthorized access and potential security threats. Here we will explore the key components of the Kerberos security protocol and understand how it fosters a reliable and efficient authentication process.

There are three main components at play in Kerberos.

  • Principal
  • Application Server
  • Key Distribution Centre (KDC)

Principal: An identifier for some entity that wants to make use of a network resource. Principals play a crucial role in the Kerberos authentication process, as they are used to identify clients and services when requesting authentication tickets and access to network resources.  The principal name typically consists of three parts:

primary[/instance]@REALM

Primary: For users, the primary component is usually their username. For hosts, it is the hostname of the system the principal is associated with. For services, it is the name of the service.

Instance: The instance component is optional and is used to distinguish different instances of services running on the same host. For example, if there are multiple instances of a service, they may have different instance names to identify them individually.

Realm: The realm component identifies the Kerberos realm to which the principal belongs. A realm is a logical administrative domain within the Kerberos authentication infrastructure, often represented by a DNS domain name.

User Principal: alice@EXAMPLE.COM

Primary: alice
Realm: EXAMPLE.COM

Host Principal: host/server.example.com@EXAMPLE.COM

Primary: host/server.example.com
Realm: EXAMPLE.COM

Service Principal: HTTP/webserver.example.com@EXAMPLE.COM

Primary: HTTP/webserver.example.com
Realm: EXAMPLE.COM

Application Server: Any service which provides access to a network resource which uses Kerberos authentication. File Servers, SQL Server, Exchange etc.

Key Distribution Centre (KDC): The backbone of the Kerberos protocol is the Key Distribution Centre (KDC), a trusted third-party authentication service operating within the network. The KDC’s central role is to issue authentication tickets to successfully verified clients, enabling them to prove their identities when accessing application servers and network resources. Only the KDC needs to be trusted for Kerberos authentication to be secure. The KDC comprises two distinct components.

  • Authentication Service: Clients use this service to authenticate themselves and get a Ticket Granting Ticket (TGT)
  • Ticket Granting Service: The TGS issues Service Tickets (or Service Granting Tickets) to clients holding a valid TGT. These Service Tickets grant access to specific network resources hosted on application servers.

The main concept in Kerberos is that clients authenticate themselves with the Authentication Service and are given a Ticket Granting Ticket. This is long lived and can then be used to request service tickets for specific application servers when needed.

The main flow of Kerberos uses three message pairs.

  • Authentication Service Exchange
  • Ticket-Granting Service Exchange
  • Client/Server Exchange

Authentication Service Exchange – The KDC gives the client an authentication session key and a ticket granting ticket (TGT).

The client initiates the authentication process by sending its User ID (principal name) and the name of the desired service (e.g., HTTP, SSH) to the AS. The client also indicates its willingness to use pre-authentication (optional but recommended for improved security) by sending an Authenticator. This is the KRB_AS_REQ message.

This flow describes pre-authentication which is an optional security improvement over the initial Kerberos specification. It requires an initial Authenticator message be sent. The Authenticator is simply the current time encrypted with the Clients Secret Key (Kc), which is derived from their password.

The AS looks up the user in the Kerberos Database. If the user principal is not found or the provided credentials are incorrect, the AS denies authentication, and the process ends.

It then generates two keys:

  • Client Secret Key (Kc) which is the hash of the users’ password.
  • Authentication Session Key (Kc-tgs). This key will be used to secure communication between the client and the Ticket Granting Server (TGS) during the session.

It uses the Client Secret Key (Kc) to decrypt the Authenticator. If the timestamp is within 5 minutes of the current time, the KDC knows that the correct password was used and that a replay attack is unlikely. If the timestamp is outside that window, the AS denies authentication and the process ends.

The AS encrypts the generated Authentication Session Key (Kc-tgs) using the Client Secret Key (Kc) and sends it to the client.

Additionally, the AS constructs a Ticket Granting Ticket (TGT) containing the client’s identity, the TGS’s identity, a timestamp, and the client’s network address. The TGT is encrypted using the TGS’s Secret Key (Ktgs).

Then encrypts the Authentication Session Key (Kc-tgs) with the Client Secret Key (Kc) and sends that to the client. It also sends the Ticket Granting Ticket (TGT) to the client. This contains the Kc-tgs and is encrypted with the servers TGS Secret Key (Ktgs). These two messages are the KRB_AS_REP message.

Because of the use of different encryption keys, only the client can decrypt the first message and retrieve the Authentication Session Key (Kc-tgs), but only the KDC can decrypt the TGT.

Authentication Service Exchange happens infrequently, generally at the point of login for a user or service, then the TGT is used for multiple subsequent authentications.

Ticket-Granting Service Exchange – The KDC distributes a service session key and a ticket for the service.

When a client wants to make use of some Kerberos enabled Service, it must first gain authorisation to that service.

The Client sends the Server Principal Name (SPN) of the service it wishes to use and the TGT to the Ticket Granting Server (TGS). It also sends another Authenticator message, this time encrypted with the Authentication Session Key (Kc-tgs). This is the KRB_TGS_REQ.

The TGS has access to the Ktgs, with which it can decrypt the TGT. From that it has the Kc-tgs and can then decrypt the Authenticator message and read the clients identity. It compares the client ids from that and the one inside the TGT and the Authenticator timestamp to the current time. If they do not match the authorisation fails and the process ends with an error.

If they match, the TGS generates a new ticket, the Service Ticket. This contains a new encryption key, the Client/Server Session Key (Kc-s). The Service Ticket is encrypted with the requested services private Application Service Secret Key (Ks), which was retrieved from the Kerberos Database. The requested service also has access to this key since it is derived from its password.

The TGS also sends the Client/Server Session Key (Kc-s) encrypted with the Authentication Session Key (Kc-tgs) so the client can decrypt and retrieve it. These two messages are the KRB_TGS_REP.

Client/Server Exchange – The client presents the client-server ticket for admission to a service.

Now the client has everything it needs to authenticate itself with the Service server. The client sends the Service Ticket and a new Authenticator message to the Service Server. The Service Ticket is as it was received from the TGS, while the Authenticator message again consists of the client id and a timestamp. This Authenticator is encrypted using the Client/Server Session Key (Kc-s). These messages are the KRB_AP_REQ.

The Service Server decrypts the Service Ticket using its own Application Service Secret Key (Ks), which it already has access to. Then uses the Client/Server Session Key (Kc-s) contained there to decrypt the Authenticator message and read the timestamp. The Service Server compares the timestamp and the client id from the Authenticator message and Service Tickets. If they do not match, authentication fails.

If they match, then the server responds to the client with the timestamp from the Authenticator message, again encrypted with the Client/Server Session Key (Kc-s). This is the KRB_AP_REQ.

Finally, the client decrypts the response timestamp, if the timestamp matches, within 5 minutes, the one it sent in the Authenticator message then the client knows it can trust the server as only the correct server would have been able to decrypt the messages in the KRB_AP_REP.

At this point service specific communication starts, in our case SQL Server specific messages.

The existing driver SQL Server authentication mechanisms

The driver currently incorporates two distinct SSPI implementations, a pure Go implementation of NTLM SSPI and a “Windows Negotiate” SPNEGO wrapped Kerberos implementation. Kerberos on Linux was not possible as it was implemented using functions from Windows DLLs. It’s worth noting that plain NTLM, being an older security support provider, is now regarded as insecure.

SQL Server logins involve multiple layers, with Kerberos being just one of them. All authentications are done using the “Security Support Provider Interface” (SSPI) which is the Windows implementation of the standardized Generic Security Service API (GSSAPI) used by programs to access available security services. Kerberos, NTLM etc. are examples of protocols that can be used as within SSPI.

One of the SSPI implementations available is Simple and Protected GSSAPI Negotiation Mechanism (SPNEGO). SPNEGO is a protocol for negotiating a protocol to use between client and server. It is also the protocol used by the drivers Windows only SSPI authentication mechanism.

SPNEGO facilitates negotiation between the client and server to determine the most suitable protocol to use for authentication, based on the client’s list of supported protocols (e.g., Kerberos, NTLM, X, Y, or Z) in order of preference. The client also pre-emptively sends data required for its preferred option, such as Kerberos, to speed up the process.

Essentially, the client sends a list of protocols it supports (Kerberos, NTLM, X, Y or Z), in order of preference and pre-emptively sends the data required for its preferred option, in this case Kerberos. The server then looks at the list and picks the first supported protocol from the list. If that is the first one, it already has the initial message for it. However, if it doesn’t support the clients preferred option, it can instead send back “No, sorry, please use Y instead.” And the client will have to redo the request with Y, which will either succeed or fail again.

This use of SPNEGO for SQL Server authentication wraps the final KRB_AP_REQ process we have just described. The Kerberos data flow is still there, we just negotiate the use of Kerberos first.

Current  go-mssqldb authentication structure

The existing SSPI communications are abstracted with the non-exported auth interface used within tds.go

type auth interface {
	InitialBytes() ([]byte, error)
	NextBytes([]byte) ([]byte, error)
	Free()
}

It deals with passing opaque byte slices back and forth to SQL Server so the client and server can implement the security mechanism without the connection handling code having to know the details of the protocol at all.

Given a SSPI implementation, InitialBytes is called first and the drivers connection handling code sends the result to the server in the login packet. Then NextBytes is called for each SSPI message the server returns. If NextBytes returns a populated byte slice this is sent to the server, if the slice is empty, that is the end of the comms. The messages are opaque to the driver, in this way it is insulated from the security code.

Changes to the go-mssqldb authentication structure

To incorporate the Kerberos protocol into the driver without relying on Windows DLLs, we opted for a new SSPI implementation using the outstanding, pure Go implementation of the Krb5 protocols in: https://github.com/jcmturner/gokrb5

Furthermore, the proposal was to make the chosen SSPI protocol to use be configurable on the connection string using new authenticator parameter.

authenticator=winsspi;
authenticator=ntml;
authenticator=krb5;

If the value were not specified, then the default for the platform would be used, winsspi on Windows, NTLM elsewhere.

Additionally, the aim was to make the implementation behind the authenticator key be runtime pluggable. SSPI implementations should register themselves with the driver at start up and then the nominated protocol would be called when opening the connection.

This pluggable architecture would allow other SSPI implementations to be used by clients without further need to change the driver.

We started by renaming the auth interface to IntegratedAuthenticator and moving it to its own package integratedauth. It remains structurally the same.

// IntegratedAuthenticator is the interface for SSPI Login Authentication providers
type IntegratedAuthenticator interface {
    InitialBytes() ([]byte, error)
    NextBytes([]byte) ([]byte, error)
    Free()
}

Along with a new interface Provider which is responsible for reading the parsed connection string and returning an appropriately configured IntegratedAuthenticator.

// Provider returns an SSPI compatible authentication provider
type Provider interface {
// GetIntegratedAuthenticator is responsible for returning an instance of the required IntegratedAuthenticator interface
    GetIntegratedAuthenticator(config msdsn.Config) (IntegratedAuthenticator, error)
}

This allows implementations to change behaviour based on the remainder of the connection string, including using new parameter values.

With this in place, each implementation registers itself with the driver by calling

integratedauth.SetIntegratedAuthenticationProvider(“providername”, ProviderImplementation)

e.g.

func init() {
    err := integratedauth.SetIntegratedAuthenticationProvider("newprovidername", AuthProvider)
    if err != nil {
      panic(err)
    }
}

Which simply adds the implementation to a map of providers keyed by their name. That name will then be available for use by the authenticator connection string parameter.

The existing winsspi and NTLM providers are both registered by the driver itself using platform build constraints: winsspi on Windows, NTLM on Linux.

To enable Kerberos authentication on Linux you must import “github.com/go-mssqldb/integratedauth/krb5” for side effects. Which will register the new “krb5” authenticator value.

package main
import (
    ...
    "github.com/microsoft/go-mssqldb"
    _ "github.com/microsoft/go-mssqldb/integratedauth/krb5"
)

func main() {
    ...
}

The new krb5 provider we have written supports authentication via three methods, Keytabs, Credential Cache and Raw credentials.

Keytabs are files containing pairs of Kerberos principals and encrypted keys. Specify the username, keytab file, the krb5.conf file, and realm.

authenticator=krb5;server=DatabaseServerName;database=DBName;user id=MyUserName;krb5-realm=domain.com;krb5-configfile=/etc/krb5.conf;krb5-keytabfile=~/MyUserName.keytab

A Credential Cache is a file holding Kerberos credentials including tickets, session keys and other identifying information in semi-permanent storage. Specify the krb5.conf file path and credential cache file path.

authenticator=krb5;server=DatabaseServerName;database=DBName;krb5-configfile=/etc/krb5.conf;krb5-credcachefile=~/MyUserNameCachedCreds

Raw credentials specify krb5.confg, Username, Password and Realm. Useful in circumstances where credentials are provided by the user.

authenticator=krb5;server=DatabaseServerName;database=DBName;user id=MyUserName;password=foo;krb5-realm=domain.com;krb5-configfile=/etc/krb5.conf;

The krb5 provider reads its parameters from the connections and validates them, returning a specific exported error for each failure type.

The krb5 provider extracts and verifies its parameters from the connections, generating distinct exported error messages for each type of failure encountered. Based on the provided parameters, it determines the appropriate authentication mechanism to utilize and produces a krb5Login instance. This contains essential fields from the connection string required for communication with the KDC.

const (
    none loginMethod = iota
    usernameAndPassword
    keyTabFile
    cachedCredentialsFile
)

type krb5Login struct {
    Krb5ConfigFile     string
    KeytabFile         string
    CredCacheFile      string
    Realm              string
    UserName           string
    Password           string
    ServerSPN          string
    DNSLookupKDC       bool
    UDPPreferenceLimit int
    loginMethod        loginMethod
}

and from that returns an instance of krbAuth

// krbAuth implements the integratedauth.IntegratedAuthenticator interface. 
// It is responsible for Kerberos Service Provider Negotiation.
type krbAuth struct {
    krb5Config   *krb5Login
    spnegoClient *spnego.SPNEGO
    krb5Client   *client.Client
}

InitialBytes creates an instance of jcmturner/gokrb5/client.Client inside getKrb5Client using the appropriate credentials.

func (k *krbAuth) InitialBytes() ([]byte, error) {
    krbClient, err := getKrb5Client(k.krb5Config)
    if err != nil {
        return nil, err
    }
    
    err = krbClient.Login()
    if err != nil {
        return nil, err
    }

    k.krb5Client = krbClient
    k.spnegoClient = spnego.SPNEGOClient(k.krb5Client, canonicalize(k.krb5Config.ServerSPN))

    tkn, err := k.spnegoClient.InitSecContext()

    if err != nil {
        return nil, err
    }
    return tkn.Marshal()
}

Next, we execute client.Login() to perform the Authentication Exchange (KRB_AS_REQ) with the Key Distribution Center (KDC), which authenticates our credentials.

Subsequently, we initialize SPNEGO for Kerberos authentication. We specify the Service Principal Name (SPN) of the destination SQL Server and invoke InitSecContext to generate a gssapi.ContextToken.

This process involves the following steps:

  • Performing the KRB_TGS_REQ to obtain a Service Ticket for the destination SQL Server.
  • Creating the first SPNEGO message, consisting of the desired protocol names and the KRB_AP_REQ, in memory. As we exclusively prefer Kerberos and do not wish to fallback to other protocols, the only protocol named is KRB5.
  • The resulting gssapi.ContextToken, containing the SPNEGO message, is returned to our InitialBytes function. We convert it to a byte slice and return it to the driver, ready to be sent to the server.

SQLServer is expecting an SSPI message, which it has received as SPNEGO is a valid SSPI. It extracts the requested protocols, verifies support for Kerberos, and proceeds with the standard Kerberos workflow. It validates our embedded KRB_AP_REQ and responds with another SSPI message which the driver detects and calls NextBytes with. This unmarshals the payload into a SPNEGOToken and Verifys it. If SQL Server accepts the Kerberos protocol and validates the KRB_AP_REQ successfully, this step will succeed.

func (k *krbAuth) NextBytes(bytes []byte) ([]byte, error) {
    var resp spnego.SPNEGOToken
    if err := resp.Unmarshal(bytes); err != nil {
        return nil, err
    }

    ok, status := resp.Verify()
    if ok { // we're ok, done
        return nil, nil
    }

    switch status.Code {
        case gssapi.StatusContinueNeeded:
            return nil, nil
        default:
            return nil, fmt.Errorf("bad status: %+v", status)
    }
}

The verification can sometimes result in a gssapi.StatusContinueNeeded status. This also signifies success and can be treated as the end of the process.

Once NextBytes returns an empty slice the connection process will continue with other messages from SQLServer acknowledging the login and our secure connection to the SQL Server will be complete.

Conclusion

We have successfully demonstrated the ability to enable cross-platform Kerberos authentication support within the go-mssqldb SQL driver using native go libraries and a pluggable architecture. This approach has subsequently been adopted in collaboration with the go SQL driver community into the Microsoft go SQL driver.

Further Reading

https://syfuhs.net/a-bit-about-kerberos – a plain English explanation of Kerberos, including how pre-authentication and password changing works, by a Microsoft Crypto developer on the Authentication team.