From 09d34231ecc50574674f8ef315180b11b6e7e08c Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Wed, 20 May 2026 16:09:47 +0530 Subject: [PATCH 1/9] feat(pam): NTLM authentication for MSSQL proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gateway MSSQL proxy now supports NTLM (Windows Authentication) in addition to SQL auth. When AuthMethod is "ntlm", the proxy performs a 3-message NTLM handshake with the server (negotiate, challenge, authenticate) using go-ntlmssp, instead of sending SQL credentials in LOGIN7. The client-to-gateway leg is unchanged — clients always use SQL auth with dummy credentials, and the proxy injects the real NTLM auth on the server leg. --- go.mod | 1 + go.sum | 2 + packages/pam/handlers/mssql/proxy.go | 116 +++++++++++++++++++++++++-- packages/pam/handlers/mssql/tds.go | 48 +++++++++-- packages/pam/pam-proxy.go | 2 + 5 files changed, 157 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 2ccba870..07d8dde1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Infisical/infisical-merge go 1.25.9 require ( + github.com/Azure/go-ntlmssp v0.1.1 github.com/BobuSumisu/aho-corasick v1.0.3 github.com/Masterminds/sprig/v3 v3.3.0 github.com/awnumar/memguard v0.23.0 diff --git a/go.sum b/go.sum index 8ca629d6..2b2740c3 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= +github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/packages/pam/handlers/mssql/proxy.go b/packages/pam/handlers/mssql/proxy.go index 5073ba99..0488b34d 100644 --- a/packages/pam/handlers/mssql/proxy.go +++ b/packages/pam/handlers/mssql/proxy.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/Azure/go-ntlmssp" "github.com/Infisical/infisical-merge/packages/pam/session" "github.com/rs/zerolog/log" ) @@ -18,6 +19,8 @@ type MssqlProxyConfig struct { InjectUsername string InjectPassword string InjectDatabase string + InjectDomain string + AuthMethod string // "sql-login" or "ntlm" EnableTLS bool TLSConfig *tls.Config SessionID string @@ -231,7 +234,19 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e log.Info().Str("sessionID", p.config.SessionID).Msg("TLS established with server") } - // 4. Send LOGIN7 with injected credentials + if p.config.AuthMethod == "ntlm" { + return p.authenticateNTLM(serverConn) + } + return p.authenticateSQL(serverConn) +} + +func (p *MssqlProxy) authenticateSQL(serverConn net.Conn) (_ net.Conn, _ []*TDSPacket, retErr error) { + defer func() { + if retErr != nil { + serverConn.Close() + } + }() + loginMsg := &Login7Message{ Username: p.config.InjectUsername, Password: p.config.InjectPassword, @@ -247,7 +262,6 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e Payload: loginMsg.Encode(), } if err := loginPkt.Write(serverConn); err != nil { - serverConn.Close() return nil, nil, fmt.Errorf("send login to server: %w", err) } @@ -257,11 +271,8 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e Int("loginPktLen", len(loginPkt.Payload)+TDSHeaderSize). Msg("Sent LOGIN7 to server") - // 5. Read login response - forward to client - log.Info().Str("sessionID", p.config.SessionID).Msg("Waiting for login response...") response, err := ReadAllPackets(serverConn) if err != nil { - serverConn.Close() return nil, nil, fmt.Errorf("read login response: %w", err) } log.Info(). @@ -271,11 +282,9 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e respPayload := CombinePayloads(response) if ContainsToken(respPayload, TokenError) { - serverConn.Close() return nil, nil, fmt.Errorf("server authentication failed") } if !ContainsToken(respPayload, TokenLoginAck) { - serverConn.Close() return nil, nil, fmt.Errorf("no login ack from server") } @@ -283,6 +292,99 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e return serverConn, response, nil } +func (p *MssqlProxy) authenticateNTLM(serverConn net.Conn) (_ net.Conn, _ []*TDSPacket, retErr error) { + defer func() { + if retErr != nil { + serverConn.Close() + } + }() + + negotiate, err := ntlmssp.NewNegotiateMessage(p.config.InjectDomain, "infisical-proxy") + if err != nil { + return nil, nil, fmt.Errorf("create NTLM negotiate message: %w", err) + } + + loginMsg := &Login7Message{ + Database: p.config.InjectDatabase, + AppName: "Infisical PAM Proxy", + Hostname: "infisical-proxy", + SSPIData: negotiate, + } + + loginPkt := &TDSPacket{ + Type: PacketTypeLogin7, + Status: StatusEOM, + PacketID: 1, + Payload: loginMsg.Encode(), + } + if err := loginPkt.Write(serverConn); err != nil { + return nil, nil, fmt.Errorf("send NTLM login to server: %w", err) + } + + log.Info(). + Str("sessionID", p.config.SessionID). + Str("domain", p.config.InjectDomain). + Str("user", p.config.InjectUsername). + Msg("Sent LOGIN7 with NTLM negotiate to server") + + challengeResponse, err := ReadAllPackets(serverConn) + if err != nil { + return nil, nil, fmt.Errorf("read NTLM challenge: %w", err) + } + + challengePayload := CombinePayloads(challengeResponse) + + if ContainsToken(challengePayload, TokenError) { + return nil, nil, fmt.Errorf("server rejected NTLM negotiate") + } + + challengeToken, err := ExtractSSPIToken(challengePayload) + if err != nil { + return nil, nil, fmt.Errorf("extract NTLM challenge: %w", err) + } + + log.Info(). + Str("sessionID", p.config.SessionID). + Int("challengeLen", len(challengeToken)). + Msg("Received NTLM challenge from server") + + // domainNeeded=true: include domain in the NTLM authenticate response + authenticate, err := ntlmssp.ProcessChallenge(challengeToken, p.config.InjectUsername, p.config.InjectPassword, true) + if err != nil { + return nil, nil, fmt.Errorf("process NTLM challenge: %w", err) + } + + sspiPkt := &TDSPacket{ + Type: PacketTypeSSPI, + Status: StatusEOM, + PacketID: 1, + Payload: authenticate, + } + if err := sspiPkt.Write(serverConn); err != nil { + return nil, nil, fmt.Errorf("send NTLM authenticate: %w", err) + } + + log.Info(). + Str("sessionID", p.config.SessionID). + Msg("Sent NTLM authenticate to server") + + response, err := ReadAllPackets(serverConn) + if err != nil { + return nil, nil, fmt.Errorf("read NTLM login response: %w", err) + } + + respPayload := CombinePayloads(response) + if ContainsToken(respPayload, TokenError) { + return nil, nil, fmt.Errorf("NTLM authentication failed") + } + if !ContainsToken(respPayload, TokenLoginAck) { + return nil, nil, fmt.Errorf("no login ack after NTLM authentication") + } + + log.Info().Str("sessionID", p.config.SessionID).Msg("MSSQL NTLM authentication successful") + return serverConn, response, nil +} + func (p *MssqlProxy) proxyToServer(client, server net.Conn, errCh chan error) { defer func() { if r := recover(); r != nil { diff --git a/packages/pam/handlers/mssql/tds.go b/packages/pam/handlers/mssql/tds.go index fe40845e..8af822a7 100644 --- a/packages/pam/handlers/mssql/tds.go +++ b/packages/pam/handlers/mssql/tds.go @@ -53,6 +53,17 @@ const ( MaxPackets = 100 ) +var ntlmsspSignature = []byte("NTLMSSP\x00") + +// ExtractSSPIToken finds the NTLM token in a TDS server response by scanning for the NTLMSSP signature. +func ExtractSSPIToken(payload []byte) ([]byte, error) { + idx := bytes.Index(payload, ntlmsspSignature) + if idx < 0 { + return nil, fmt.Errorf("no NTLMSSP token found in server response") + } + return payload[idx:], nil +} + // TDSPacket represents a TDS packet type TDSPacket struct { Type uint8 @@ -294,6 +305,7 @@ type Login7Message struct { Password string AppName string Database string + SSPIData []byte } // ParseLogin7 parses a LOGIN7 message (extracts only what we need) @@ -324,7 +336,8 @@ const ( fSetLang = 0x80 // OptionFlags2 - fODBC = 0x02 + fODBC = 0x02 + fIntSecurity = 0x80 // Integrated Security (SSPI/NTLM) ) // Encode serializes the LOGIN7 message @@ -341,9 +354,19 @@ func (m *Login7Message) Encode() []byte { m.Header.OptionFlags1 = fUseDB | fSetLang m.Header.OptionFlags2 = fODBC + useSSPI := len(m.SSPIData) > 0 + hostname := encodeUTF16(m.Hostname) - username := encodeUTF16(m.Username) - password := manglePassword(m.Password) + var username, password []byte + if useSSPI { + // NTLM: username and password are empty in LOGIN7; auth is via SSPI blob + username = nil + password = nil + m.Header.OptionFlags2 |= fIntSecurity + } else { + username = encodeUTF16(m.Username) + password = manglePassword(m.Password) + } appname := encodeUTF16(m.AppName) database := encodeUTF16(m.Database) cltIntName := encodeUTF16("ODBC") // Client interface name @@ -385,12 +408,24 @@ func (m *Login7Message) Encode() []byte { offset += uint16(len(database)) m.Header.SSPIOffset = offset - m.Header.SSPILength = 0 + if useSSPI { + sspiLen := len(m.SSPIData) + if sspiLen <= 65535 { + m.Header.SSPILength = uint16(sspiLen) + m.Header.SSPILongLength = 0 + } else { + m.Header.SSPILength = 0 + m.Header.SSPILongLength = uint32(sspiLen) + } + offset += uint16(sspiLen) + } else { + m.Header.SSPILength = 0 + m.Header.SSPILongLength = 0 + } m.Header.AtchDBFileOffset = offset m.Header.AtchDBFileLength = 0 m.Header.ChangePasswordOff = offset m.Header.ChangePasswordLen = 0 - m.Header.SSPILongLength = 0 m.Header.Length = uint32(offset) @@ -404,6 +439,9 @@ func (m *Login7Message) Encode() []byte { buf.Write(appname) buf.Write(cltIntName) buf.Write(database) + if useSSPI { + buf.Write(m.SSPIData) + } return buf.Bytes() } diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index edf273af..f57077a0 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -313,6 +313,8 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo InjectUsername: credentials.Username, InjectPassword: credentials.Password, InjectDatabase: credentials.Database, + InjectDomain: credentials.Domain, + AuthMethod: credentials.AuthMethod, EnableTLS: credentials.SSLEnabled, TLSConfig: tlsConfig, SessionID: pamConfig.SessionId, From 67407a30dddedf36359306dfa78bccc308121a4f Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Wed, 20 May 2026 16:24:47 +0530 Subject: [PATCH 2/9] fix: add go-ntlmssp to e2e module --- e2e/go.mod | 1 + e2e/go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/e2e/go.mod b/e2e/go.mod index 44d14afd..328ed660 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -34,6 +34,7 @@ require ( dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/BobuSumisu/aho-corasick v1.0.3 // indirect github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect github.com/Masterminds/goutils v1.1.1 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 04236c5f..fef3f46e 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -53,6 +53,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= +github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= From fd093e2673b317925dfb6ef2b18d1b3ba310a74a Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Wed, 20 May 2026 17:04:58 +0530 Subject: [PATCH 3/9] fix(pam): avoid false-positive error detection on NTLM challenge + fix SSPI length sentinel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract SSPI token before checking for error tokens — the NTLM challenge contains random binary that can match the 0xAA error byte. Also fix the SSPI length boundary: 0xFFFF is a sentinel per TDS spec, so use strict less-than. --- packages/pam/handlers/mssql/proxy.go | 7 +++---- packages/pam/handlers/mssql/tds.go | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/pam/handlers/mssql/proxy.go b/packages/pam/handlers/mssql/proxy.go index 0488b34d..379017a7 100644 --- a/packages/pam/handlers/mssql/proxy.go +++ b/packages/pam/handlers/mssql/proxy.go @@ -334,12 +334,11 @@ func (p *MssqlProxy) authenticateNTLM(serverConn net.Conn) (_ net.Conn, _ []*TDS challengePayload := CombinePayloads(challengeResponse) - if ContainsToken(challengePayload, TokenError) { - return nil, nil, fmt.Errorf("server rejected NTLM negotiate") - } - challengeToken, err := ExtractSSPIToken(challengePayload) if err != nil { + if ContainsToken(challengePayload, TokenError) { + return nil, nil, fmt.Errorf("server rejected NTLM negotiate") + } return nil, nil, fmt.Errorf("extract NTLM challenge: %w", err) } diff --git a/packages/pam/handlers/mssql/tds.go b/packages/pam/handlers/mssql/tds.go index 8af822a7..5e6b4dbe 100644 --- a/packages/pam/handlers/mssql/tds.go +++ b/packages/pam/handlers/mssql/tds.go @@ -410,7 +410,7 @@ func (m *Login7Message) Encode() []byte { m.Header.SSPIOffset = offset if useSSPI { sspiLen := len(m.SSPIData) - if sspiLen <= 65535 { + if sspiLen < 65535 { m.Header.SSPILength = uint16(sspiLen) m.Header.SSPILongLength = 0 } else { From f9ebc545885dac6f246d0c19185cc1874c020504 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Wed, 20 May 2026 17:46:20 +0530 Subject: [PATCH 4/9] fix(pam): pass DOMAIN\username to NTLM ProcessChallenge go-ntlmssp uses splitNameForAuth to extract domain from the username. Without the DOMAIN\ prefix, the domain is empty in the NTLMv2 hash and authenticate message, which may fail on domain-member SQL Servers authenticating against a remote DC. --- packages/pam/handlers/mssql/proxy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pam/handlers/mssql/proxy.go b/packages/pam/handlers/mssql/proxy.go index 379017a7..343d1502 100644 --- a/packages/pam/handlers/mssql/proxy.go +++ b/packages/pam/handlers/mssql/proxy.go @@ -347,8 +347,8 @@ func (p *MssqlProxy) authenticateNTLM(serverConn net.Conn) (_ net.Conn, _ []*TDS Int("challengeLen", len(challengeToken)). Msg("Received NTLM challenge from server") - // domainNeeded=true: include domain in the NTLM authenticate response - authenticate, err := ntlmssp.ProcessChallenge(challengeToken, p.config.InjectUsername, p.config.InjectPassword, true) + ntlmUsername := p.config.InjectDomain + "\\" + p.config.InjectUsername + authenticate, err := ntlmssp.ProcessChallenge(challengeToken, ntlmUsername, p.config.InjectPassword, true) if err != nil { return nil, nil, fmt.Errorf("process NTLM challenge: %w", err) } From 9fa72ff2ae9b8f924d726aebb0e9ac8052a73fde Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Wed, 20 May 2026 17:46:28 +0530 Subject: [PATCH 5/9] style: gofmt struct field alignment --- packages/pam/handlers/mssql/proxy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pam/handlers/mssql/proxy.go b/packages/pam/handlers/mssql/proxy.go index 343d1502..514657ee 100644 --- a/packages/pam/handlers/mssql/proxy.go +++ b/packages/pam/handlers/mssql/proxy.go @@ -19,8 +19,8 @@ type MssqlProxyConfig struct { InjectUsername string InjectPassword string InjectDatabase string - InjectDomain string - AuthMethod string // "sql-login" or "ntlm" + InjectDomain string + AuthMethod string // "sql-login" or "ntlm" EnableTLS bool TLSConfig *tls.Config SessionID string From 44bc36f04f3b8bf4f751f230b35852afa3eb4e36 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 28 May 2026 04:59:43 +0530 Subject: [PATCH 6/9] feat(pam): Kerberos authentication for MSSQL proxy Gateway MSSQL proxy now supports Kerberos via gokrb5/v8. Obtains service ticket from KDC, wraps in SPNEGO, sends in LOGIN7. Handles mutual auth round-trip. Actionable error messages for KDC/SPN/clock issues. --- e2e/go.mod | 1 + e2e/go.sum | 13 +++ go.mod | 7 ++ go.sum | 21 ++++ packages/api/model.go | 3 + packages/pam/handlers/mssql/proxy.go | 160 ++++++++++++++++++++++++++- packages/pam/pam-proxy.go | 3 + packages/pam/session/credentials.go | 6 + 8 files changed, 213 insertions(+), 1 deletion(-) diff --git a/e2e/go.mod b/e2e/go.mod index 328ed660..4f3b2854 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -160,6 +160,7 @@ require ( github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index fef3f46e..7c8486a5 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -492,6 +492,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= @@ -523,6 +525,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= @@ -560,6 +564,13 @@ github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE= github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= @@ -1093,6 +1104,7 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= @@ -1184,6 +1196,7 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= diff --git a/go.mod b/go.mod index 07d8dde1..91b54bac 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/infisical/go-sdk v0.7.0 github.com/infisical/infisical-kmip v0.3.17 github.com/jackc/pgx/v5 v5.9.0 + github.com/jcmturner/gokrb5/v8 v8.4.4 github.com/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/mango-cobra v1.2.0 @@ -116,10 +117,16 @@ require ( github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gosimple/slug v1.15.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/imdario/mergo v0.3.6 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.8 // indirect diff --git a/go.sum b/go.sum index 2b2740c3..0f3413f5 100644 --- a/go.sum +++ b/go.sum @@ -330,6 +330,10 @@ github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= @@ -349,6 +353,9 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -379,6 +386,18 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE= github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -679,6 +698,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= @@ -764,6 +784,7 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= diff --git a/packages/api/model.go b/packages/api/model.go index c1b5fea2..31e6df78 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -938,6 +938,9 @@ type PAMSessionCredentials struct { ServiceAccountName string `json:"serviceAccountName,omitempty"` Namespace string `json:"namespace,omitempty"` Domain string `json:"domain,omitempty"` + Realm string `json:"realm,omitempty"` + KDCAddress string `json:"kdcAddress,omitempty"` + SPN string `json:"spn,omitempty"` } type MFASessionStatus string diff --git a/packages/pam/handlers/mssql/proxy.go b/packages/pam/handlers/mssql/proxy.go index 514657ee..71cbb119 100644 --- a/packages/pam/handlers/mssql/proxy.go +++ b/packages/pam/handlers/mssql/proxy.go @@ -6,11 +6,15 @@ import ( "fmt" "io" "net" + "strings" "sync" "time" "github.com/Azure/go-ntlmssp" "github.com/Infisical/infisical-merge/packages/pam/session" + "github.com/jcmturner/gokrb5/v8/client" + "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/spnego" "github.com/rs/zerolog/log" ) @@ -20,7 +24,10 @@ type MssqlProxyConfig struct { InjectPassword string InjectDatabase string InjectDomain string - AuthMethod string // "sql-login" or "ntlm" + InjectRealm string + InjectKDCAddr string + InjectSPN string + AuthMethod string // "sql-login", "ntlm", or "kerberos" EnableTLS bool TLSConfig *tls.Config SessionID string @@ -237,6 +244,9 @@ func (p *MssqlProxy) connectAndAuthenticateToServer() (net.Conn, []*TDSPacket, e if p.config.AuthMethod == "ntlm" { return p.authenticateNTLM(serverConn) } + if p.config.AuthMethod == "kerberos" { + return p.authenticateKerberos(serverConn) + } return p.authenticateSQL(serverConn) } @@ -384,6 +394,154 @@ func (p *MssqlProxy) authenticateNTLM(serverConn net.Conn) (_ net.Conn, _ []*TDS return serverConn, response, nil } +func (p *MssqlProxy) authenticateKerberos(serverConn net.Conn) (_ net.Conn, _ []*TDSPacket, retErr error) { + defer func() { + if retErr != nil { + serverConn.Close() + } + }() + + cfg, err := buildKrb5Config(p.config.InjectRealm, p.config.InjectKDCAddr) + if err != nil { + return nil, nil, wrapKerberosError(err, p.config.InjectKDCAddr) + } + + krbClient := client.NewWithPassword( + p.config.InjectUsername, p.config.InjectRealm, p.config.InjectPassword, + cfg, client.DisablePAFXFAST(true), + ) + defer krbClient.Destroy() + + if err := krbClient.Login(); err != nil { + return nil, nil, wrapKerberosError(err, p.config.InjectKDCAddr) + } + + ticket, encryptionKey, err := krbClient.GetServiceTicket(p.config.InjectSPN) + if err != nil { + return nil, nil, wrapKerberosError(err, p.config.InjectKDCAddr) + } + + initToken, err := spnego.NewNegTokenInitKRB5(krbClient, ticket, encryptionKey) + if err != nil { + return nil, nil, fmt.Errorf("create SPNEGO token: %w", err) + } + + tokenBytes, err := initToken.Marshal() + if err != nil { + return nil, nil, fmt.Errorf("marshal SPNEGO token: %w", err) + } + + loginMsg := &Login7Message{ + Database: p.config.InjectDatabase, + AppName: "Infisical PAM Proxy", + Hostname: "infisical-proxy", + SSPIData: tokenBytes, + } + + loginPkt := &TDSPacket{ + Type: PacketTypeLogin7, + Status: StatusEOM, + PacketID: 1, + Payload: loginMsg.Encode(), + } + if err := loginPkt.Write(serverConn); err != nil { + return nil, nil, fmt.Errorf("send Kerberos login to server: %w", err) + } + + log.Info(). + Str("sessionID", p.config.SessionID). + Str("realm", p.config.InjectRealm). + Str("spn", p.config.InjectSPN). + Str("user", p.config.InjectUsername). + Msg("Sent LOGIN7 with Kerberos SPNEGO to server") + + response, err := ReadAllPackets(serverConn) + if err != nil { + return nil, nil, fmt.Errorf("read Kerberos login response: %w", err) + } + + respPayload := CombinePayloads(response) + + // Kerberos mutual auth: server sends SSPI accept-incomplete before LoginAck. + // Acknowledge with an empty SSPI packet to complete the handshake. + if !ContainsToken(respPayload, TokenLoginAck) && !ContainsToken(respPayload, TokenError) { + + sspiAck := &TDSPacket{ + Type: PacketTypeSSPI, + Status: StatusEOM, + PacketID: 1, + } + if err := sspiAck.Write(serverConn); err != nil { + return nil, nil, fmt.Errorf("send Kerberos SSPI acknowledgement: %w", err) + } + + response, err = ReadAllPackets(serverConn) + if err != nil { + return nil, nil, fmt.Errorf("read Kerberos final response: %w", err) + } + respPayload = CombinePayloads(response) + } + + if ContainsToken(respPayload, TokenError) { + return nil, nil, fmt.Errorf("Kerberos authentication failed") + } + if !ContainsToken(respPayload, TokenLoginAck) { + return nil, nil, fmt.Errorf("no login ack after Kerberos authentication") + } + + log.Info().Str("sessionID", p.config.SessionID).Msg("MSSQL Kerberos authentication successful") + return serverConn, response, nil +} + +func buildKrb5Config(realm, kdcAddress string) (*config.Config, error) { + for _, s := range []string{realm, kdcAddress} { + if strings.ContainsAny(s, "\n\r[]{}=") { + return nil, fmt.Errorf("invalid characters in Kerberos configuration") + } + } + + dnsLookup := "true" + realmsSection := "" + + if kdcAddress != "" { + dnsLookup = "false" + if !strings.Contains(kdcAddress, ":") { + kdcAddress = kdcAddress + ":88" + } + realmsSection = fmt.Sprintf(` +[realms] + %s = { + kdc = %s + }`, realm, kdcAddress) + } + + cfgString := fmt.Sprintf(` +[libdefaults] + default_realm = %s + dns_lookup_kdc = %s + dns_lookup_realm = false + udp_preference_limit = 1 +%s`, realm, dnsLookup, realmsSection) + + return config.NewFromString(cfgString) +} + +func wrapKerberosError(err error, kdcAddress string) error { + msg := err.Error() + switch { + case strings.Contains(msg, "sending to KDC") || strings.Contains(msg, "connection refused"): + return fmt.Errorf("Kerberos: cannot reach KDC at %s (verify address and ensure port 88 is open): %w", kdcAddress, err) + case strings.Contains(msg, "Clock skew") || strings.Contains(msg, "KRB_AP_ERR_SKEW"): + return fmt.Errorf("Kerberos: clock skew too large (gateway and KDC must be within 5 minutes): %w", err) + case strings.Contains(msg, "Preauthentication") || strings.Contains(msg, "KDC_ERR_PREAUTH_FAILED"): + return fmt.Errorf("Kerberos: invalid credentials (check username, password, and realm): %w", err) + case strings.Contains(msg, "not found in Kerberos database") || strings.Contains(msg, "PRINCIPAL_UNKNOWN"): + return fmt.Errorf("Kerberos: principal not found (check realm and SPN): %w", err) + default: + return fmt.Errorf("Kerberos authentication failed: %w", err) + } +} + func (p *MssqlProxy) proxyToServer(client, server net.Conn, errCh chan error) { defer func() { if r := recover(); r != nil { diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index f57077a0..2f2cf552 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -314,6 +314,9 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo InjectPassword: credentials.Password, InjectDatabase: credentials.Database, InjectDomain: credentials.Domain, + InjectRealm: credentials.Realm, + InjectKDCAddr: credentials.KDCAddress, + InjectSPN: credentials.SPN, AuthMethod: credentials.AuthMethod, EnableTLS: credentials.SSLEnabled, TLSConfig: tlsConfig, diff --git a/packages/pam/session/credentials.go b/packages/pam/session/credentials.go index fcc9e3f1..d0e8e84d 100644 --- a/packages/pam/session/credentials.go +++ b/packages/pam/session/credentials.go @@ -35,6 +35,9 @@ type PAMCredentials struct { ServiceAccountName string Namespace string Domain string + Realm string + KDCAddress string + SPN string PolicyRules *api.PAMPolicyRules } @@ -188,6 +191,9 @@ func (cm *CredentialsManager) GetPAMSessionCredentials(sessionId string, expiryT ServiceAccountName: response.Credentials.ServiceAccountName, Namespace: response.Credentials.Namespace, Domain: response.Credentials.Domain, + Realm: response.Credentials.Realm, + KDCAddress: response.Credentials.KDCAddress, + SPN: response.Credentials.SPN, PolicyRules: response.PolicyRules, } From 492c9b71dc377a70efe930db1f1df66ddc3b3e06 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 28 May 2026 09:21:53 +0530 Subject: [PATCH 7/9] fix(pam): remove empty SSPI ack + improve KDC error detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed empty SSPI acknowledgement packet during Kerberos mutual auth — server sends LoginAck without needing it. Added dial timeout and i/o timeout patterns to wrapKerberosError for unreachable KDC. --- packages/pam/handlers/mssql/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pam/handlers/mssql/proxy.go b/packages/pam/handlers/mssql/proxy.go index 2d765a05..d896bed6 100644 --- a/packages/pam/handlers/mssql/proxy.go +++ b/packages/pam/handlers/mssql/proxy.go @@ -519,7 +519,7 @@ func buildKrb5Config(realm, kdcAddress string) (*config.Config, error) { func wrapKerberosError(err error, kdcAddress string) error { msg := err.Error() switch { - case strings.Contains(msg, "sending to KDC") || strings.Contains(msg, "connection refused"): + case strings.Contains(msg, "sending to KDC") || strings.Contains(msg, "connection refused") || strings.Contains(msg, "dial timeout") || strings.Contains(msg, "i/o timeout"): return fmt.Errorf("Kerberos: cannot reach KDC at %s (verify address and ensure port 88 is open): %w", kdcAddress, err) case strings.Contains(msg, "Clock skew") || strings.Contains(msg, "KRB_AP_ERR_SKEW"): return fmt.Errorf("Kerberos: clock skew too large (gateway and KDC must be within 5 minutes): %w", err) From 5788d34a06048d424f4e44d9bdafcf131fee429d Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 28 May 2026 09:43:36 +0530 Subject: [PATCH 8/9] fix: add gokrb5 sub-deps to e2e module --- e2e/go.mod | 6 ++++++ e2e/go.sum | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/e2e/go.mod b/e2e/go.mod index 4f3b2854..7cf39d18 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -149,6 +149,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -160,7 +161,12 @@ require ( github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 7c8486a5..fb3f3689 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -492,7 +492,9 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -526,6 +528,7 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -564,12 +567,17 @@ github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE= github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= From 0637d5b8bb044facbfe2f3818c80b01b565a17a2 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Mon, 1 Jun 2026 04:52:53 +0530 Subject: [PATCH 9/9] improvement(pam): use config.New() instead of INI string interpolation for krb5 config --- packages/pam/handlers/mssql/proxy.go | 44 +++++++++------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/packages/pam/handlers/mssql/proxy.go b/packages/pam/handlers/mssql/proxy.go index d896bed6..4d1c93d2 100644 --- a/packages/pam/handlers/mssql/proxy.go +++ b/packages/pam/handlers/mssql/proxy.go @@ -401,10 +401,7 @@ func (p *MssqlProxy) authenticateKerberos(serverConn net.Conn) (_ net.Conn, _ [] } }() - cfg, err := buildKrb5Config(p.config.InjectRealm, p.config.InjectKDCAddr) - if err != nil { - return nil, nil, wrapKerberosError(err, p.config.InjectKDCAddr) - } + cfg := buildKrb5Config(p.config.InjectRealm, p.config.InjectKDCAddr) krbClient := client.NewWithPassword( p.config.InjectUsername, p.config.InjectRealm, p.config.InjectPassword, @@ -483,37 +480,24 @@ func (p *MssqlProxy) authenticateKerberos(serverConn net.Conn) (_ net.Conn, _ [] return serverConn, response, nil } -func buildKrb5Config(realm, kdcAddress string) (*config.Config, error) { - for _, s := range []string{realm, kdcAddress} { - if strings.ContainsAny(s, "\n\r[]{}=") { - return nil, fmt.Errorf("invalid characters in Kerberos configuration") - } - } - - dnsLookup := "true" - realmsSection := "" +func buildKrb5Config(realm, kdcAddress string) *config.Config { + cfg := config.New() + cfg.LibDefaults.DefaultRealm = realm + cfg.LibDefaults.DNSLookupKDC = kdcAddress == "" + cfg.LibDefaults.DNSLookupRealm = false + cfg.LibDefaults.UDPPreferenceLimit = 1 if kdcAddress != "" { - dnsLookup = "false" if !strings.Contains(kdcAddress, ":") { kdcAddress = kdcAddress + ":88" } - realmsSection = fmt.Sprintf(` -[realms] - %s = { - kdc = %s - }`, realm, kdcAddress) - } - - cfgString := fmt.Sprintf(` -[libdefaults] - default_realm = %s - dns_lookup_kdc = %s - dns_lookup_realm = false - udp_preference_limit = 1 -%s`, realm, dnsLookup, realmsSection) - - return config.NewFromString(cfgString) + cfg.Realms = append(cfg.Realms, config.Realm{ + Realm: realm, + KDC: []string{kdcAddress}, + }) + } + + return cfg } func wrapKerberosError(err error, kdcAddress string) error {