Skip to content

Commit eca87d5

Browse files
pulled-latest-mit-kerb-plus-main
2 parents 40c5d36 + 6aec75a commit eca87d5

4 files changed

Lines changed: 89 additions & 125 deletions

File tree

internal/params/flags.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const (
6767
IgnoreProxyFlag = "ignore-proxy"
6868
IgnoreProxyFlagUsage = "Ignore proxy configuration"
6969
ProxyTypeFlag = "proxy-auth-type"
70-
ProxyTypeFlagUsage = "Proxy authentication type, (basic, ntlm, kerberos, or kerberos-native)"
70+
ProxyTypeFlagUsage = "Proxy authentication type (supported types: basic, ntlm, kerberos or kerberos-native)"
7171
TimeoutFlag = "timeout"
7272
TimeoutFlagUsage = "Timeout for network activity, (default 5 seconds)"
7373
NtlmProxyDomainFlag = "proxy-ntlm-domain"
@@ -80,8 +80,8 @@ const (
8080
SastRecommendedExclusionsFlags = "sast-recommended-exclusions"
8181
NtlmProxyDomainFlagUsage = "Window domain when using NTLM proxy"
8282
KerberosProxySPNFlagUsage = "Service Principal Name (SPN) for Kerberos proxy authentication"
83-
KerberosKrb5ConfFlagUsage = "Path to krb5.conf file for Kerberos (default: /etc/krb5.conf)"
84-
KerberosCcacheFlagUsage = "Path to Kerberos credential cache (optional, uses KRB5CCNAME env or default)"
83+
KerberosKrb5ConfFlagUsage = "Path to Kerberos configuration file(default: /etc/krb5.conf on linux and C:\\Windows\\krb5.ini on windows)"
84+
KerberosCcacheFlagUsage = "Path to Kerberos credential cache (optional, default uses KRB5CCNAME env or OS default)"
8585
BaseURIFlagUsage = "The base system URI"
8686
BaseAuthURIFlag = "base-auth-uri"
8787
BaseAuthURIFlagUsage = "The base system IAM URI"

internal/wrappers/client.go

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"net/http/httptrace"
1313
"net/url"
1414
"os"
15-
"os/exec"
1615
"runtime"
1716
"strings"
1817
"sync"
@@ -48,6 +47,7 @@ const (
4847
contentTypeHeader = "Content-Type"
4948
formURLContentType = "application/x-www-form-urlencoded"
5049
jsonContentType = "application/json"
50+
defaultDialerDuration = 30 * time.Second
5151
)
5252

5353
var (
@@ -191,8 +191,8 @@ func basicProxyClient(timeout uint, proxyStr string) *http.Client {
191191

192192
func ntmlProxyClient(timeout uint, proxyStr string) *http.Client {
193193
dialer := &net.Dialer{
194-
Timeout: 30 * time.Second,
195-
KeepAlive: 30 * time.Second,
194+
Timeout: defaultDialerDuration,
195+
KeepAlive: defaultDialerDuration,
196196
}
197197
u, _ := url.Parse(proxyStr)
198198
domainStr := viper.GetString(commonParams.ProxyDomainKey)
@@ -211,8 +211,8 @@ func ntmlProxyClient(timeout uint, proxyStr string) *http.Client {
211211

212212
func kerberosProxyClient(timeout uint, proxyStr string) *http.Client {
213213
dialer := &net.Dialer{
214-
Timeout: 30 * time.Second,
215-
KeepAlive: 30 * time.Second,
214+
Timeout: defaultDialerDuration,
215+
KeepAlive: defaultDialerDuration,
216216
}
217217
u, _ := url.Parse(proxyStr)
218218

@@ -221,65 +221,45 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client {
221221

222222
// Validate required SPN parameter
223223
if proxySPN == "" {
224-
logger.PrintIfVerbose("ERROR: Kerberos SPN is required for Kerberos proxy authentication.")
225-
fmt.Printf("Error: Kerberos SPN is required for --proxy-auth-type kerberos\n")
226-
fmt.Printf("Please provide: --proxy-kerberos-spn 'HTTP/proxy.example.com'\n")
227-
fmt.Printf("Or set environment variable: CX_PROXY_KERBEROS_SPN=HTTP/proxy.example.com\n")
228-
os.Exit(1)
224+
logger.PrintIfVerbose("Error: Kerberos SPN is required for Kerberos proxy authentication.")
225+
logger.Print("Error: Kerberos SPN is required for Kerberos proxy authentication.")
226+
logger.PrintIfVerbose("Please provide SPN using: --proxy-kerberos-spn 'HTTP/proxy.example.com' or set CX_PROXY_KERBEROS_SPN environment variable")
227+
228+
os.Exit(0)
229229
}
230230

231231
// Use gokrb5 for all platforms (standard Kerberos)
232232
return kerberosGokrb5ProxyClient(timeout, proxyStr, u, dialer, proxySPN)
233233
}
234234

235-
// isWindowsKerberosAvailable checks if Windows has Kerberos available (simple check)
236-
func isWindowsKerberosAvailable() bool {
237-
if runtime.GOOS != "windows" {
238-
return false
239-
}
240-
241-
// Simple check: can we run klist?
242-
cmd := exec.Command("klist")
243-
err := cmd.Run()
244-
return err == nil
245-
}
246-
247235
// kerberosNativeProxyClient creates an HTTP client using Windows native Kerberos (SSPI)
248236
func kerberosNativeProxyClient(timeout uint, proxyStr string) *http.Client {
249-
// IMMEDIATE platform validation - fail fast with clear message
250237
if runtime.GOOS != "windows" {
251-
logger.PrintIfVerbose("ERROR: --proxy-auth-type kerberos-native is only supported on Windows")
252-
fmt.Printf("Error: --proxy-auth-type kerberos-native is only supported on Windows.\n")
253-
fmt.Printf("Current platform: %s\n", runtime.GOOS)
254-
fmt.Printf("Use --proxy-auth-type kerberos for cross-platform MIT Kerberos support.\n")
255-
os.Exit(1)
238+
logger.PrintIfVerbose("Error: --proxy-auth-type kerberos-native is only supported on Windows")
239+
logger.Print("Error: --proxy-auth-type kerberos-native is only supported on Windows")
240+
241+
os.Exit(0)
256242
}
257243

258244
dialer := &net.Dialer{
259-
Timeout: 30 * time.Second,
260-
KeepAlive: 30 * time.Second,
245+
Timeout: defaultDialerDuration,
246+
KeepAlive: defaultDialerDuration,
261247
}
262248
u, _ := url.Parse(proxyStr)
263249

264250
// Get Kerberos configuration
265251
proxySPN := viper.GetString(commonParams.ProxyKerberosSPNKey)
266252
if proxySPN == "" {
267-
logger.PrintIfVerbose("ERROR: Kerberos SPN is required for Windows native authentication")
268-
fmt.Println("Error: Kerberos SPN is required. Use --proxy-kerberos-spn flag")
269-
fmt.Println("Example: --proxy-kerberos-spn 'HTTP/proxy.example.com'")
270-
os.Exit(1)
253+
logger.PrintIfVerbose("ERROR: Kerberos SPN is required for windows native kerberos authentication")
254+
logger.Print("Error: Kerberos SPN is required for windows native kerberos authentication")
255+
os.Exit(0)
271256
}
272257

273258
// Validate SSPI setup
274259
if err := kerberos.ValidateSSPISetup(proxySPN); err != nil {
275-
logger.PrintIfVerbose("ERROR: Windows SSPI validation failed: " + err.Error())
276-
fmt.Printf("Error: Windows native Kerberos setup failed: %v\n", err)
277-
fmt.Printf("\nTroubleshooting:\n")
278-
fmt.Printf("1. Ensure you are logged into a Windows domain\n")
279-
fmt.Printf("2. Check if Kerberos tickets exist: run 'klist'\n")
280-
fmt.Printf("3. Verify the SPN is correct: --proxy-kerberos-spn 'HTTP/proxy.example.com'\n")
281-
fmt.Printf("4. Alternative: Use --proxy-auth-type kerberos for MIT Kerberos\n")
282-
os.Exit(1)
260+
logger.PrintIfVerbose("Error: Failed to generate a token for the specified SPN." + err.Error())
261+
logger.Print("Error: Failed to generate a token for the specified SPN.")
262+
os.Exit(0)
283263
}
284264

285265
logger.PrintIfVerbose("Creating HTTP client using Windows native Kerberos (SSPI)")
@@ -303,18 +283,19 @@ func kerberosGokrb5ProxyClient(timeout uint, proxyStr string, u *url.URL, dialer
303283
if krb5ConfPath == "" {
304284
krb5ConfPath = kerberos.GetDefaultKrb5ConfPath()
305285
}
286+
306287
ccachePath := viper.GetString(commonParams.ProxyKerberosCcacheKey)
307288

308289
// Early validation: Check gokrb5 Kerberos setup before creating client
309290
if err := kerberos.ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN); err != nil {
310-
logger.PrintIfVerbose("Error: gokrb5 Kerberos proxy authentication setup failed: " + err.Error())
311-
fmt.Println(fmt.Sprintf("Error: gokrb5 Kerberos proxy authentication setup failed: %v", err.Error()))
291+
logger.PrintIfVerbose("Error: Kerberos proxy authentication setup failed: " + err.Error())
292+
logger.Printf("Error: %v", err.Error())
312293
os.Exit(0)
313294
}
314295

315-
logger.PrintIfVerbose("Creating HTTP client using gokrb5 Kerberos Proxy using: " + proxyStr)
316-
logger.PrintIfVerbose("gokrb5 Kerberos SPN: " + proxySPN)
317-
logger.PrintIfVerbose("gokrb5 Kerberos krb5.conf: " + krb5ConfPath)
296+
logger.PrintIfVerbose("Creating HTTP client using Kerberos Proxy using: " + proxyStr)
297+
logger.PrintIfVerbose("Kerberos SPN: " + proxySPN)
298+
logger.PrintIfVerbose("Kerberos krb5 configuration file: " + krb5ConfPath)
318299

319300
kerberosConfig := kerberos.KerberosConfig{
320301
ProxySPN: proxySPN,
@@ -797,6 +778,11 @@ func request(client *http.Client, req *http.Request, responseBody bool) (*http.R
797778
Domains = AppendIfNotExists(Domains, req.URL.Host)
798779
if err != nil {
799780
logger.PrintIfVerbose(err.Error())
781+
// Check if this is a non-retryable error (e.g., wrong Kerberos SPN)
782+
if kerberos.IsNonRetryable(err) {
783+
logger.PrintIfVerbose("Non-retryable error detected, skipping retries")
784+
return nil, err
785+
}
800786
}
801787
if resp != nil && err == nil {
802788
if hasRedirectStatusCode(resp) {

internal/wrappers/kerberos/proxy-kerberos.go

Lines changed: 34 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@ import (
2020
"github.com/pkg/errors"
2121
)
2222

23+
const (
24+
osWindows = "windows"
25+
httpsScheme = "https"
26+
)
27+
28+
// NonRetryableError represents an error that should not trigger HTTP request retries
29+
type NonRetryableError struct {
30+
Message string
31+
}
32+
33+
func (e *NonRetryableError) Error() string {
34+
return e.Message
35+
}
36+
37+
// IsNonRetryable returns true if the error should not trigger retries
38+
func IsNonRetryable(err error) bool {
39+
_, ok := err.(*NonRetryableError)
40+
return ok
41+
}
42+
2343
// Gokrb5DialContext is the DialContext function that should be wrapped with a
2444
// Kerberos Authentication using gokrb5.
2545
type Gokrb5DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
@@ -41,7 +61,7 @@ func NewKerberosProxyDialContext(dialer *net.Dialer, proxyURL *url.URL,
4161
}
4262
return func(ctx context.Context, network, addr string) (net.Conn, error) {
4363
dialProxy := func() (net.Conn, error) {
44-
if proxyURL.Scheme == "https" {
64+
if proxyURL.Scheme == httpsScheme {
4565
return tls.DialWithDialer(dialer, "tcp", proxyURL.Host, tlsConfig)
4666
}
4767
return dialer.DialContext(ctx, network, proxyURL.Host)
@@ -51,12 +71,6 @@ func NewKerberosProxyDialContext(dialer *net.Dialer, proxyURL *url.URL,
5171
}
5272

5373
func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func() (net.Conn, error)) (net.Conn, error) {
54-
// Validate required SPN parameter early
55-
if kerberosConfig.ProxySPN == "" {
56-
log.Printf("Kerberos SPN is required but not provided")
57-
return nil, errors.New("Kerberos SPN is required. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var")
58-
}
59-
6074
conn, err := baseDial()
6175
if err != nil {
6276
log.Printf("Could not call dial context with proxy: %s", err)
@@ -65,36 +79,16 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func(
6579

6680
// Use default krb5.conf path if not specified
6781
krb5ConfPath := kerberosConfig.Krb5ConfPath
68-
if krb5ConfPath == "" {
69-
krb5ConfPath = GetDefaultKrb5ConfPath()
70-
}
71-
72-
// Check if krb5.conf exists before trying to load it
73-
if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) {
74-
log.Printf("Kerberos configuration file not found at %s", krb5ConfPath)
75-
return conn, errors.New("Kerberos configuration file not found. Please ensure krb5.conf is properly configured")
76-
}
7782

7883
// Load krb5.conf
7984
krb5cfg, err := config.Load(krb5ConfPath)
8085
if err != nil {
81-
log.Printf("Failed to load krb5.conf from %s: %s", krb5ConfPath, err)
82-
return conn, errors.New("failed to load Kerberos configuration. Please check the krb5.conf file")
86+
log.Printf("Failed to load krb5 configuration file from %s: %s", krb5ConfPath, err)
87+
return conn, errors.New("failed to load Kerberos configuration. Please check the krb5 configuration file")
8388
}
8489

8590
// Load credential cache
8691
ccPath := kerberosConfig.CcachePath
87-
if ccPath == "" {
88-
ccPath = getDefaultCCachePath()
89-
}
90-
91-
// Check if credential cache exists before trying to load it
92-
if ccPath != "" {
93-
if _, err := os.Stat(ccPath); os.IsNotExist(err) {
94-
log.Printf("Kerberos credential cache not found at %s", ccPath)
95-
return conn, errors.New("Kerberos credential cache not found. Please run 'kinit' to obtain Kerberos tickets first")
96-
}
97-
}
9892

9993
cc, err := credentials.LoadCCache(ccPath)
10094
if err != nil {
@@ -122,7 +116,7 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func(
122116
// Build SPNEGO token for the proxy SPN
123117
if err := spnego.SetSPNEGOHeader(krbClient, connect, kerberosConfig.ProxySPN); err != nil {
124118
log.Printf("Failed to generate SPNEGO token for SPN '%s': %s", kerberosConfig.ProxySPN, err)
125-
return conn, errors.New("failed to generate SPNEGO token. Please check if the SPN is correct")
119+
return conn, &NonRetryableError{Message: "failed to generate SPNEGO token. Please check if the SPN is correct"}
126120
}
127121

128122
// spnego.SetSPNEGOHeader sets Authorization: Negotiate <token>
@@ -160,7 +154,7 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func(
160154
log.Printf("Proxy-Authenticate: %s", v)
161155
}
162156
_ = resp.Body.Close()
163-
return conn, errors.New("proxy authentication failed. Check SPN and proxy keytab/KDC configuration")
157+
return conn, &NonRetryableError{Message: "proxy authentication failed. Check SPN and proxy keytab/KDC configuration"}
164158
}
165159
_ = resp.Body.Close()
166160
return conn, errors.New(http.StatusText(resp.StatusCode))
@@ -176,25 +170,19 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func(
176170
// This function performs all the same checks as dialAndNegotiate but without actually
177171
// making network connections, allowing early detection of configuration problems
178172
func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error {
179-
// Validate SPN
180-
if proxySPN == "" {
181-
return errors.New("Kerberos SPN is required. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var")
182-
}
183-
184-
// Use default krb5.conf path if not specified
185173
if krb5ConfPath == "" {
186-
krb5ConfPath = GetDefaultKrb5ConfPath()
174+
krb5ConfPath = GetDefaultKrb5ConfPath() // Use default krb5.conf path if not specified
187175
}
188176

189177
// Check if krb5.conf exists
190178
if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) {
191-
return errors.New("Kerberos configuration file not found. Please ensure krb5.conf is properly configured")
179+
return errors.New("Kerberos proxy authentication setup failed because no valid Kerberos config file was found. Please ensure that a properly configured krb5.conf/krb5.ini file is available at the specified location.")
192180
}
193181

194182
// Load krb5.conf to validate it's readable
195-
_, err := config.Load(krb5ConfPath)
183+
krb5cfg, err := config.Load(krb5ConfPath)
196184
if err != nil {
197-
return errors.New("failed to load Kerberos configuration. Please check the krb5.conf file")
185+
return errors.New("Kerberos proxy authentication setup failed because no valid Kerberos config file was found. Please ensure that a properly configured krb5.conf/krb5.ini file is available at the specified location.")
198186
}
199187

200188
// Get default credential cache path if not specified
@@ -205,25 +193,19 @@ func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error {
205193
// Check if credential cache exists
206194
if ccachePath != "" {
207195
if _, err := os.Stat(ccachePath); os.IsNotExist(err) {
208-
return errors.New("Kerberos credential cache not found. Please run 'kinit' to obtain Kerberos tickets first")
196+
return errors.New("Kerberos proxy authentication setup failed because no Kerberos credential cache was found. Make sure to run 'kinit' to populate the cache before running this command.")
209197
}
210198
}
211199

212200
// Try to load credential cache to validate it's usable
213201
cc, err := credentials.LoadCCache(ccachePath)
214202
if err != nil {
215-
return errors.New("failed to load Kerberos credential cache. Please run 'kinit' to obtain valid Kerberos tickets")
216-
}
217-
218-
// Try to create Kerberos client to validate tickets are valid
219-
krb5cfg, err := config.Load(krb5ConfPath)
220-
if err != nil {
221-
return errors.New("failed to reload Kerberos configuration")
203+
return errors.New("Kerberos proxy authentication setup failed because no Kerberos credential cache was found. Make sure to run 'kinit' to populate the cache before running this command.")
222204
}
223205

224206
_, err = client.NewFromCCache(cc, krb5cfg)
225207
if err != nil {
226-
return errors.New("failed to create Kerberos client. Please check your Kerberos tickets with 'klist'")
208+
return errors.New("Failed to create Kerberos client. Please check your Kerberos tickets with 'klist'")
227209
}
228210

229211
return nil
@@ -232,7 +214,7 @@ func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error {
232214
// GetDefaultKrb5ConfPath returns the default krb5.conf path for the current platform
233215
func GetDefaultKrb5ConfPath() string {
234216
switch runtime.GOOS {
235-
case "windows":
217+
case osWindows:
236218
// Windows typically uses krb5.ini
237219
if windir := os.Getenv("WINDIR"); windir != "" {
238220
return filepath.Join(windir, "krb5.ini")
@@ -262,9 +244,7 @@ func getDefaultCCachePath() string {
262244
}
263245

264246
switch runtime.GOOS {
265-
case "windows":
266-
// On Windows, use the default credential cache managed by the system
267-
// The gokrb5 library should handle this automatically with empty string
247+
case osWindows:
268248
return ""
269249
default:
270250
// Linux, macOS, and other Unix-like systems

0 commit comments

Comments
 (0)