From 341ffdc3a66e935834288756d56d63c4a2811c8e Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:09:14 +0530 Subject: [PATCH 01/18] kerberos-implemented --- go.mod | 22 +- go.sum | 13 + internal/commands/root.go | 6 + internal/params/binds.go | 3 + internal/params/envs.go | 3 + internal/params/flags.go | 8 +- internal/params/keys.go | 3 + internal/wrappers/client.go | 41 +++- internal/wrappers/kerberos/proxy-kerberos.go | 242 +++++++++++++++++++ 9 files changed, 333 insertions(+), 8 deletions(-) create mode 100644 internal/wrappers/kerberos/proxy-kerberos.go diff --git a/go.mod b/go.mod index 84f7f9955..8a35859a9 100644 --- a/go.mod +++ b/go.mod @@ -35,12 +35,16 @@ require ( ) require ( - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - modernc.org/libc v1.66.3 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.38.2 // indirect + github.com/hashicorp/go-uuid v1.0.1 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect + gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect + gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect + gopkg.in/jcmturner/gokrb5.v7 v7.5.0 // indirect + gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect +) + +require ( dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20240914100643-eb91380d8434 // indirect @@ -209,6 +213,7 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nwaples/rardecode v1.1.3 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -223,6 +228,7 @@ require ( github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/profile v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/rubenv/sql-migrate v1.8.0 // indirect @@ -304,6 +310,10 @@ require ( k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/kubectl v0.33.3 // indirect k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.2 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/kustomize/api v0.19.0 // indirect diff --git a/go.sum b/go.sum index cd28af725..afe13006a 100644 --- a/go.sum +++ b/go.sum @@ -578,6 +578,7 @@ github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 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 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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= @@ -611,6 +612,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +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/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= @@ -1537,6 +1540,16 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw= +gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= +gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI= +gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= +gopkg.in/jcmturner/gokrb5.v7 v7.5.0 h1:a9tsXlIDD9SKxotJMK3niV7rPZAJeX2aD/0yg3qlIrg= +gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= +gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU= +gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/commands/root.go b/internal/commands/root.go index dfeb2de76..e6f107222 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -92,6 +92,9 @@ func NewAstCLI( rootCmd.PersistentFlags().Bool(params.IgnoreProxyFlag, false, params.IgnoreProxyFlagUsage) rootCmd.PersistentFlags().String(params.ProxyTypeFlag, "", params.ProxyTypeFlagUsage) rootCmd.PersistentFlags().String(params.NtlmProxyDomainFlag, "", params.NtlmProxyDomainFlagUsage) + rootCmd.PersistentFlags().String(params.KerberosProxySPNFlag, "", params.KerberosProxySPNFlagUsage) + rootCmd.PersistentFlags().String(params.KerberosKrb5ConfFlag, "", params.KerberosKrb5ConfFlagUsage) + rootCmd.PersistentFlags().String(params.KerberosCcacheFlag, "", params.KerberosCcacheFlagUsage) rootCmd.PersistentFlags().String(params.TimeoutFlag, "", params.TimeoutFlagUsage) rootCmd.PersistentFlags().String(params.BaseURIFlag, params.BaseURI, params.BaseURIFlagUsage) rootCmd.PersistentFlags().String(params.BaseAuthURIFlag, params.BaseIAMURI, params.BaseAuthURIFlagUsage) @@ -135,6 +138,9 @@ func NewAstCLI( _ = viper.BindPFlag(params.ProxyKey, rootCmd.PersistentFlags().Lookup(params.ProxyFlag)) _ = viper.BindPFlag(params.ProxyTypeKey, rootCmd.PersistentFlags().Lookup(params.ProxyTypeFlag)) _ = viper.BindPFlag(params.ProxyDomainKey, rootCmd.PersistentFlags().Lookup(params.NtlmProxyDomainFlag)) + _ = viper.BindPFlag(params.ProxyKerberosSPNKey, rootCmd.PersistentFlags().Lookup(params.KerberosProxySPNFlag)) + _ = viper.BindPFlag(params.ProxyKerberosKrb5ConfKey, rootCmd.PersistentFlags().Lookup(params.KerberosKrb5ConfFlag)) + _ = viper.BindPFlag(params.ProxyKerberosCcacheKey, rootCmd.PersistentFlags().Lookup(params.KerberosCcacheFlag)) _ = viper.BindPFlag(params.ClientTimeoutKey, rootCmd.PersistentFlags().Lookup(params.TimeoutFlag)) _ = viper.BindPFlag(params.BaseAuthURIKey, rootCmd.PersistentFlags().Lookup(params.BaseAuthURIFlag)) _ = viper.BindPFlag(params.AstAPIKey, rootCmd.PersistentFlags().Lookup(params.AstAPIKeyFlag)) diff --git a/internal/params/binds.go b/internal/params/binds.go index e8d4140af..414aa656b 100644 --- a/internal/params/binds.go +++ b/internal/params/binds.go @@ -8,6 +8,9 @@ var EnvVarsBinds = []struct { {BaseURIKey, BaseURIEnv, ""}, {ProxyTypeKey, ProxyTypeEnv, "basic"}, {ProxyDomainKey, ProxyDomainEnv, ""}, + {ProxyKerberosSPNKey, ProxyKerberosSPNEnv, ""}, + {ProxyKerberosKrb5ConfKey, ProxyKerberosKrb5ConfEnv, ""}, + {ProxyKerberosCcacheKey, ProxyKerberosCcacheEnv, ""}, {BaseAuthURIKey, BaseAuthURIEnv, ""}, {AstAPIKey, AstAPIKeyEnv, ""}, {IgnoreProxyKey, IgnoreProxyEnv, ""}, diff --git a/internal/params/envs.go b/internal/params/envs.go index ef3bf06ef..19dc0f3c7 100644 --- a/internal/params/envs.go +++ b/internal/params/envs.go @@ -11,6 +11,9 @@ const ( CxProxyEnv = "CX_HTTP_PROXY" ProxyTypeEnv = "CX_PROXY_AUTH_TYPE" ProxyDomainEnv = "CX_PROXY_NTLM_DOMAIN" + ProxyKerberosSPNEnv = "CX_PROXY_KERBEROS_SPN" + ProxyKerberosKrb5ConfEnv = "CX_PROXY_KERBEROS_KRB5_CONF" + ProxyKerberosCcacheEnv = "CX_PROXY_KERBEROS_CCACHE" BaseAuthURIEnv = "CX_BASE_AUTH_URI" AstAPIKeyEnv = "CX_APIKEY" AccessKeyIDEnv = "CX_CLIENT_ID" diff --git a/internal/params/flags.go b/internal/params/flags.go index 7541e6d5f..0b58f09a0 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -64,15 +64,21 @@ const ( IgnoreProxyFlag = "ignore-proxy" IgnoreProxyFlagUsage = "Ignore proxy configuration" ProxyTypeFlag = "proxy-auth-type" - ProxyTypeFlagUsage = "Proxy authentication type, (basic or ntlm)" + ProxyTypeFlagUsage = "Proxy authentication type, (basic, ntlm, or kerberos)" TimeoutFlag = "timeout" TimeoutFlagUsage = "Timeout for network activity, (default 5 seconds)" NtlmProxyDomainFlag = "proxy-ntlm-domain" + KerberosProxySPNFlag = "proxy-kerberos-spn" + KerberosKrb5ConfFlag = "proxy-kerberos-krb5-conf" + KerberosCcacheFlag = "proxy-kerberos-ccache" SastFastScanFlag = "sast-fast-scan" SastLightQueriesFlag = "sast-light-queries" BranchPrimaryFlag = "branch-primary" SastRecommendedExclusionsFlags = "sast-recommended-exclusions" NtlmProxyDomainFlagUsage = "Window domain when using NTLM proxy" + KerberosProxySPNFlagUsage = "Service Principal Name (SPN) for Kerberos proxy authentication" + KerberosKrb5ConfFlagUsage = "Path to krb5.conf file for Kerberos (default: /etc/krb5.conf)" + KerberosCcacheFlagUsage = "Path to Kerberos credential cache (optional, uses KRB5CCNAME env or default)" BaseURIFlagUsage = "The base system URI" BaseAuthURIFlag = "base-auth-uri" BaseAuthURIFlagUsage = "The base system IAM URI" diff --git a/internal/params/keys.go b/internal/params/keys.go index 90bb6a09b..839b13e53 100644 --- a/internal/params/keys.go +++ b/internal/params/keys.go @@ -10,6 +10,9 @@ var ( ProxyKey = strings.ToLower(ProxyEnv) ProxyTypeKey = strings.ToLower(ProxyTypeEnv) ProxyDomainKey = strings.ToLower(ProxyDomainEnv) + ProxyKerberosSPNKey = strings.ToLower(ProxyKerberosSPNEnv) + ProxyKerberosKrb5ConfKey = strings.ToLower(ProxyKerberosKrb5ConfEnv) + ProxyKerberosCcacheKey = strings.ToLower(ProxyKerberosCcacheEnv) BaseAuthURIKey = strings.ToLower(BaseAuthURIEnv) ClientTimeoutKey = strings.ToLower(ClientTimeoutEnv) AstAPIKey = strings.ToLower(AstAPIKeyEnv) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index ffd38322d..0964ba29d 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/viper" commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/wrappers/kerberos" "github.com/checkmarx/ast-cli/internal/wrappers/ntlm" ) @@ -30,6 +31,7 @@ const ( expiryGraceSeconds = 10 NoTimeout = 0 ntlmProxyToken = "ntlm" + kerberosProxyToken = "kerberos" checkmarxURLError = "Could not reach provided Checkmarx server" invalidCredentialsError = "Provided credentials are invalid" APIKeyDecodeErrorFormat = "Token decoding error: %s" @@ -139,6 +141,8 @@ func GetClient(timeout uint) *http.Client { client = basicProxyClient(timeout, "") } else if proxyTypeStr == ntlmProxyToken { client = ntmlProxyClient(timeout, proxyStr) + } else if proxyTypeStr == kerberosProxyToken { + client = kerberosProxyClient(timeout, proxyStr) } else { client = basicProxyClient(timeout, proxyStr) } @@ -200,6 +204,41 @@ func ntmlProxyClient(timeout uint, proxyStr string) *http.Client { } } +func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + u, _ := url.Parse(proxyStr) + + // Get Kerberos configuration from viper + proxySPN := viper.GetString(commonParams.ProxyKerberosSPNKey) + krb5ConfPath := viper.GetString(commonParams.ProxyKerberosKrb5ConfKey) + if krb5ConfPath == "" { + krb5ConfPath = kerberos.GetDefaultKrb5ConfPath() + } + ccachePath := viper.GetString(commonParams.ProxyKerberosCcacheKey) + + logger.PrintIfVerbose("Creating HTTP client using Kerberos Proxy using: " + proxyStr) + logger.PrintIfVerbose("Kerberos SPN: " + proxySPN) + logger.PrintIfVerbose("Kerberos krb5.conf: " + krb5ConfPath) + + kerberosConfig := kerberos.KerberosConfig{ + ProxySPN: proxySPN, + Krb5ConfPath: krb5ConfPath, + CcachePath: ccachePath, + } + + kerberosDialContext := kerberos.NewKerberosProxyDialContext(dialer, u, kerberosConfig, nil) + return &http.Client{ + Transport: &http.Transport{ + Proxy: nil, + DialContext: kerberosDialContext, + }, + Timeout: time.Duration(timeout) * time.Second, + } +} + func getURLAndAccessToken(path string) (urlFromPath, accessToken string, err error) { accessToken, err = GetAccessToken() if err != nil { @@ -540,7 +579,7 @@ func getNewToken(credentialsPayload, authServerURI string) (string, error) { if req.Body != nil { body, err = io.ReadAll(req.Body) if err != nil { - fmt.Errorf("failed to read request body: %w", err) + return "", fmt.Errorf("failed to read request body: %w", err) } if req.Body != nil { req.Body.Close() diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go new file mode 100644 index 000000000..a33a93662 --- /dev/null +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -0,0 +1,242 @@ +package kerberos + +import ( + "bufio" + "context" + "crypto/tls" + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + + "gopkg.in/jcmturner/gokrb5.v7/client" + "gopkg.in/jcmturner/gokrb5.v7/config" + "gopkg.in/jcmturner/gokrb5.v7/credentials" + "gopkg.in/jcmturner/gokrb5.v7/spnego" +) + +// DialContext is the DialContext function that should be wrapped with a +// Kerberos Authentication. +type DialContext func(ctx context.Context, network, addr string) (net.Conn, error) + +// KerberosConfig holds the configuration for Kerberos authentication +type KerberosConfig struct { + ProxySPN string // SPN configured in proxy keytab/KDC (must match) + Krb5ConfPath string // path to krb5.conf file + CcachePath string // path to credential cache (optional, will use KRB5CCNAME or default) +} + +// NewKerberosProxyDialContext creates a new DialContext that uses Kerberos authentication +// for proxy connections. Unlike NTLM, it describes the proxy location with a full URL, +// whose scheme can be HTTP or HTTPS. +func NewKerberosProxyDialContext(dialer *net.Dialer, proxyURL *url.URL, + kerberosConfig KerberosConfig, tlsConfig *tls.Config) DialContext { + if dialer == nil { + dialer = &net.Dialer{} + } + return func(ctx context.Context, network, addr string) (net.Conn, error) { + dialProxy := func() (net.Conn, error) { + if proxyURL.Scheme == "https" { + return tls.DialWithDialer(dialer, "tcp", proxyURL.Host, tlsConfig) + } + return dialer.DialContext(ctx, network, proxyURL.Host) + } + return dialAndNegotiate(addr, kerberosConfig, dialProxy) + } +} + +func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func() (net.Conn, error)) (net.Conn, error) { + conn, err := baseDial() + if err != nil { + log.Printf("Could not call dial context with proxy: %s", err) + return conn, err + } + + // Use default krb5.conf path if not specified + krb5ConfPath := kerberosConfig.Krb5ConfPath + if krb5ConfPath == "" { + krb5ConfPath = GetDefaultKrb5ConfPath() + } + + // Load krb5.conf + krb5cfg, err := config.Load(krb5ConfPath) + if err != nil { + log.Printf("Failed to load krb5.conf from %s: %s", krb5ConfPath, err) + return conn, err + } + + // Load credential cache + ccPath := kerberosConfig.CcachePath + if ccPath == "" { + ccPath = getDefaultCCachePath() + } + + cc, err := credentials.LoadCCache(ccPath) + if err != nil { + log.Printf("Failed to load ccache %s: %s", ccPath, err) + return conn, err + } + + // Create Kerberos client from cache + krbClient, err := client.NewClientFromCCache(cc, krb5cfg) + if err != nil { + log.Printf("Failed to create Kerberos client: %s", err) + return conn, err + } + + // Step 1: Try plain request first (like curl does) + header := make(http.Header) + header.Set("Proxy-Connection", "Keep-Alive") + connect := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: header, + } + + err = connect.Write(conn) + if err != nil { + log.Printf("Could not write initial request to proxy: %s", err) + return conn, err + } + + // Read response + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connect) + if err != nil { + log.Printf("Could not read response from proxy: %s", err) + return conn, err + } + + _, err = io.ReadAll(resp.Body) + if err != nil { + log.Printf("Could not read response body from proxy: %s", err) + return conn, err + } + _ = resp.Body.Close() + + // If proxy did NOT require auth, return connection + if resp.StatusCode != http.StatusProxyAuthRequired { + log.Printf("Proxy did not require authentication, connection established") + return conn, nil + } + + // Step 2: Proxy asked for auth. Check it includes Negotiate + hasNegotiate := false + for _, v := range resp.Header.Values("Proxy-Authenticate") { + if strings.HasPrefix(strings.TrimSpace(v), "Negotiate") { + hasNegotiate = true + break + } + } + if !hasNegotiate { + log.Printf("Proxy requires auth but did not advertise Negotiate, got: '%s'", + resp.Header.Get("Proxy-Authenticate")) + return conn, fmt.Errorf("no Negotiate authentication method available") + } + + // Step 3: Build SPNEGO token for the proxy SPN and attach as Proxy-Authorization + header2 := make(http.Header) //nolint:gosec + header2.Set("Proxy-Connection", "Keep-Alive") + req2 := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: header2, + } + + if err := spnego.SetSPNEGOHeader(krbClient, req2, kerberosConfig.ProxySPN); err != nil { + log.Printf("Failed to set SPNEGO header: %s", err) + return conn, err + } + + // spnego.SetSPNEGOHeader sets Authorization: Negotiate + authVal := req2.Header.Get("Authorization") + if authVal == "" { + log.Printf("SPNEGO did not generate an Authorization header") + return conn, fmt.Errorf("failed to generate SPNEGO token") + } + + // Move Authorization -> Proxy-Authorization (proxy level) + req2.Header.Set("Proxy-Authorization", authVal) + req2.Header.Del("Authorization") // don't forward to origin + + // Step 4: Retry with Proxy-Authorization + if err = req2.Write(conn); err != nil { + log.Printf("Could not write authorization to proxy: %s", err) + return conn, err + } + + resp2, err := http.ReadResponse(br, req2) + if err != nil { + log.Printf("Could not read response from proxy: %s", err) + return conn, err + } + + if resp2.StatusCode != http.StatusOK { + log.Printf("Expected %d as return status, got: %d", http.StatusOK, resp2.StatusCode) + if resp2.StatusCode == http.StatusProxyAuthRequired { + log.Printf("Proxy still returned 407 after sending Negotiate token. Check SPN and proxy keytab/KDC.") + // Print Proxy-Authenticate for diagnostics + for _, v := range resp2.Header.Values("Proxy-Authenticate") { + log.Printf("Proxy-Authenticate: %s", v) + } + } + _ = resp2.Body.Close() + return conn, fmt.Errorf("proxy authentication failed: %s", resp2.Status) + } + + // Successfully authorized with Kerberos + _ = resp2.Body.Close() + log.Printf("Successfully authenticated with proxy using Kerberos") + return conn, nil +} + +// GetDefaultKrb5ConfPath returns the default krb5.conf path for the current platform +func GetDefaultKrb5ConfPath() string { + switch runtime.GOOS { + case "windows": + // Windows typically uses krb5.ini + if windir := os.Getenv("WINDIR"); windir != "" { + return filepath.Join(windir, "krb5.ini") + } + // Fallback locations + locations := []string{ + "C:\\ProgramData\\MIT\\Kerberos5\\krb5.ini", + "C:\\Windows\\krb5.ini", + } + for _, loc := range locations { + if _, err := os.Stat(loc); err == nil { + return loc + } + } + return "C:\\Windows\\krb5.ini" // Default fallback + default: + // Linux, macOS, and other Unix-like systems + return "/etc/krb5.conf" + } +} + +// getDefaultCCachePath returns the default credential cache path for the current platform +func getDefaultCCachePath() string { + // Check KRB5CCNAME environment variable first + if ccname := os.Getenv("KRB5CCNAME"); ccname != "" { + return ccname + } + + switch runtime.GOOS { + case "windows": + // On Windows, use the default credential cache managed by the system + // The gokrb5 library should handle this automatically with empty string + return "" + default: + // Linux, macOS, and other Unix-like systems + return fmt.Sprintf("/tmp/krb5cc_%d", os.Getuid()) + } +} From bc799d55b1dc182e41e53ec95eb78e94f5a52762 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Wed, 3 Sep 2025 22:40:40 +0530 Subject: [PATCH 02/18] breaking-infinity --- go.mod | 2 +- internal/wrappers/client.go | 10 ++ internal/wrappers/kerberos/proxy-kerberos.go | 125 +++++++------------ 3 files changed, 55 insertions(+), 82 deletions(-) diff --git a/go.mod b/go.mod index 8a35859a9..85a2dc070 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( golang.org/x/text v0.27.0 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 + gopkg.in/jcmturner/gokrb5.v7 v7.5.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools v2.2.0+incompatible ) @@ -40,7 +41,6 @@ require ( gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect - gopkg.in/jcmturner/gokrb5.v7 v7.5.0 // indirect gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect ) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 0964ba29d..18f849902 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -213,6 +213,16 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // Get Kerberos configuration from viper proxySPN := viper.GetString(commonParams.ProxyKerberosSPNKey) + + // Validate required SPN parameter + if proxySPN == "" { + logger.PrintIfVerbose("ERROR: Kerberos SPN is required for Kerberos proxy authentication.") + logger.PrintIfVerbose("Please provide SPN using: --proxy-kerberos-spn 'HTTP/proxy.example.com' or set CX_PROXY_KERBEROS_SPN environment variable") + logger.PrintIfVerbose("Falling back to basic proxy authentication") + // Return a basic client that will fail gracefully + return basicProxyClient(timeout, proxyStr) + } + krb5ConfPath := viper.GetString(commonParams.ProxyKerberosKrb5ConfKey) if krb5ConfPath == "" { krb5ConfPath = kerberos.GetDefaultKrb5ConfPath() diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go index a33a93662..138651612 100644 --- a/internal/wrappers/kerberos/proxy-kerberos.go +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -5,7 +5,6 @@ import ( "context" "crypto/tls" "fmt" - "io" "log" "net" "net/http" @@ -13,7 +12,6 @@ import ( "os" "path/filepath" "runtime" - "strings" "gopkg.in/jcmturner/gokrb5.v7/client" "gopkg.in/jcmturner/gokrb5.v7/config" @@ -52,6 +50,11 @@ func NewKerberosProxyDialContext(dialer *net.Dialer, proxyURL *url.URL, } func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func() (net.Conn, error)) (net.Conn, error) { + // Validate required SPN parameter early + if kerberosConfig.ProxySPN == "" { + return nil, fmt.Errorf("Kerberos SPN is required but not provided. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var") + } + conn, err := baseDial() if err != nil { log.Printf("Could not call dial context with proxy: %s", err) @@ -64,11 +67,15 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( krb5ConfPath = GetDefaultKrb5ConfPath() } + // Check if krb5.conf exists before trying to load it + if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) { + return conn, fmt.Errorf("Kerberos configuration file not found at %s. Please ensure krb5.conf is properly configured", krb5ConfPath) + } + // Load krb5.conf krb5cfg, err := config.Load(krb5ConfPath) if err != nil { - log.Printf("Failed to load krb5.conf from %s: %s", krb5ConfPath, err) - return conn, err + return conn, fmt.Errorf("failed to load krb5.conf from %s: %w. Please check the Kerberos configuration file", krb5ConfPath, err) } // Load credential cache @@ -77,21 +84,26 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( ccPath = getDefaultCCachePath() } + // Check if credential cache exists before trying to load it + if ccPath != "" { + if _, err := os.Stat(ccPath); os.IsNotExist(err) { + return conn, fmt.Errorf("Kerberos credential cache not found at %s. Please run 'kinit' to obtain Kerberos tickets first", ccPath) + } + } + cc, err := credentials.LoadCCache(ccPath) if err != nil { - log.Printf("Failed to load ccache %s: %s", ccPath, err) - return conn, err + return conn, fmt.Errorf("failed to load Kerberos credential cache from %s: %w. Please run 'kinit' to obtain valid Kerberos tickets", ccPath, err) } // Create Kerberos client from cache krbClient, err := client.NewClientFromCCache(cc, krb5cfg) if err != nil { - log.Printf("Failed to create Kerberos client: %s", err) - return conn, err + return conn, fmt.Errorf("failed to create Kerberos client: %w. Please check your Kerberos tickets with 'klist'", err) } - // Step 1: Try plain request first (like curl does) - header := make(http.Header) + // Kerberos Step 1: Send CONNECT with SPNEGO token directly (like NTLM does) + header := make(http.Header) //nolint:gosec header.Set("Proxy-Connection", "Keep-Alive") connect := &http.Request{ Method: "CONNECT", @@ -100,100 +112,51 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( Header: header, } - err = connect.Write(conn) - if err != nil { - log.Printf("Could not write initial request to proxy: %s", err) - return conn, err - } - - // Read response - br := bufio.NewReader(conn) - resp, err := http.ReadResponse(br, connect) - if err != nil { - log.Printf("Could not read response from proxy: %s", err) - return conn, err - } - - _, err = io.ReadAll(resp.Body) - if err != nil { - log.Printf("Could not read response body from proxy: %s", err) - return conn, err - } - _ = resp.Body.Close() - - // If proxy did NOT require auth, return connection - if resp.StatusCode != http.StatusProxyAuthRequired { - log.Printf("Proxy did not require authentication, connection established") - return conn, nil - } - - // Step 2: Proxy asked for auth. Check it includes Negotiate - hasNegotiate := false - for _, v := range resp.Header.Values("Proxy-Authenticate") { - if strings.HasPrefix(strings.TrimSpace(v), "Negotiate") { - hasNegotiate = true - break - } - } - if !hasNegotiate { - log.Printf("Proxy requires auth but did not advertise Negotiate, got: '%s'", - resp.Header.Get("Proxy-Authenticate")) - return conn, fmt.Errorf("no Negotiate authentication method available") - } - - // Step 3: Build SPNEGO token for the proxy SPN and attach as Proxy-Authorization - header2 := make(http.Header) //nolint:gosec - header2.Set("Proxy-Connection", "Keep-Alive") - req2 := &http.Request{ - Method: "CONNECT", - URL: &url.URL{Opaque: addr}, - Host: addr, - Header: header2, - } - - if err := spnego.SetSPNEGOHeader(krbClient, req2, kerberosConfig.ProxySPN); err != nil { - log.Printf("Failed to set SPNEGO header: %s", err) - return conn, err + // Build SPNEGO token for the proxy SPN + if err := spnego.SetSPNEGOHeader(krbClient, connect, kerberosConfig.ProxySPN); err != nil { + return conn, fmt.Errorf("failed to generate SPNEGO token for SPN '%s': %w. Please check if the SPN is correct", kerberosConfig.ProxySPN, err) } // spnego.SetSPNEGOHeader sets Authorization: Negotiate - authVal := req2.Header.Get("Authorization") + authVal := connect.Header.Get("Authorization") if authVal == "" { - log.Printf("SPNEGO did not generate an Authorization header") - return conn, fmt.Errorf("failed to generate SPNEGO token") + return conn, fmt.Errorf("SPNEGO failed to generate Authorization header for SPN '%s'. Please check Kerberos configuration", kerberosConfig.ProxySPN) } // Move Authorization -> Proxy-Authorization (proxy level) - req2.Header.Set("Proxy-Authorization", authVal) - req2.Header.Del("Authorization") // don't forward to origin + connect.Header.Set("Proxy-Authorization", authVal) + connect.Header.Del("Authorization") // don't forward to origin - // Step 4: Retry with Proxy-Authorization - if err = req2.Write(conn); err != nil { - log.Printf("Could not write authorization to proxy: %s", err) + log.Printf("Sending CONNECT with Kerberos SPNEGO token") + err = connect.Write(conn) + if err != nil { + log.Printf("Could not write Kerberos auth request to proxy: %s", err) return conn, err } - resp2, err := http.ReadResponse(br, req2) + // Kerberos Step 2: Read response + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connect) if err != nil { log.Printf("Could not read response from proxy: %s", err) return conn, err } - if resp2.StatusCode != http.StatusOK { - log.Printf("Expected %d as return status, got: %d", http.StatusOK, resp2.StatusCode) - if resp2.StatusCode == http.StatusProxyAuthRequired { - log.Printf("Proxy still returned 407 after sending Negotiate token. Check SPN and proxy keytab/KDC.") + if resp.StatusCode != http.StatusOK { + log.Printf("Expected %d as return status, got: %d", http.StatusOK, resp.StatusCode) + if resp.StatusCode == http.StatusProxyAuthRequired { + log.Printf("Proxy returned 407 after sending Negotiate token. Check SPN and proxy keytab/KDC.") // Print Proxy-Authenticate for diagnostics - for _, v := range resp2.Header.Values("Proxy-Authenticate") { + for _, v := range resp.Header.Values("Proxy-Authenticate") { log.Printf("Proxy-Authenticate: %s", v) } } - _ = resp2.Body.Close() - return conn, fmt.Errorf("proxy authentication failed: %s", resp2.Status) + _ = resp.Body.Close() + return conn, fmt.Errorf("proxy authentication failed: %s", resp.Status) } // Successfully authorized with Kerberos - _ = resp2.Body.Close() + _ = resp.Body.Close() log.Printf("Successfully authenticated with proxy using Kerberos") return conn, nil } From e4ba1eb69f8c2dc856e4a7528398bc8b8760a90b Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:21:36 +0530 Subject: [PATCH 03/18] breaking-infinity1 --- internal/wrappers/kerberos/proxy-kerberos.go | 29 ++++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go index 138651612..2bc293141 100644 --- a/internal/wrappers/kerberos/proxy-kerberos.go +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -13,6 +13,7 @@ import ( "path/filepath" "runtime" + "github.com/pkg/errors" "gopkg.in/jcmturner/gokrb5.v7/client" "gopkg.in/jcmturner/gokrb5.v7/config" "gopkg.in/jcmturner/gokrb5.v7/credentials" @@ -52,7 +53,8 @@ func NewKerberosProxyDialContext(dialer *net.Dialer, proxyURL *url.URL, func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func() (net.Conn, error)) (net.Conn, error) { // Validate required SPN parameter early if kerberosConfig.ProxySPN == "" { - return nil, fmt.Errorf("Kerberos SPN is required but not provided. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var") + log.Printf("Kerberos SPN is required but not provided") + return nil, errors.New("Kerberos SPN is required. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var") } conn, err := baseDial() @@ -69,13 +71,15 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( // Check if krb5.conf exists before trying to load it if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) { - return conn, fmt.Errorf("Kerberos configuration file not found at %s. Please ensure krb5.conf is properly configured", krb5ConfPath) + log.Printf("Kerberos configuration file not found at %s", krb5ConfPath) + return conn, errors.New("Kerberos configuration file not found. Please ensure krb5.conf is properly configured") } // Load krb5.conf krb5cfg, err := config.Load(krb5ConfPath) if err != nil { - return conn, fmt.Errorf("failed to load krb5.conf from %s: %w. Please check the Kerberos configuration file", krb5ConfPath, err) + log.Printf("Failed to load krb5.conf from %s: %s", krb5ConfPath, err) + return conn, errors.New("failed to load Kerberos configuration. Please check the krb5.conf file") } // Load credential cache @@ -87,19 +91,22 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( // Check if credential cache exists before trying to load it if ccPath != "" { if _, err := os.Stat(ccPath); os.IsNotExist(err) { - return conn, fmt.Errorf("Kerberos credential cache not found at %s. Please run 'kinit' to obtain Kerberos tickets first", ccPath) + log.Printf("Kerberos credential cache not found at %s", ccPath) + return conn, errors.New("Kerberos credential cache not found. Please run 'kinit' to obtain Kerberos tickets first") } } cc, err := credentials.LoadCCache(ccPath) if err != nil { - return conn, fmt.Errorf("failed to load Kerberos credential cache from %s: %w. Please run 'kinit' to obtain valid Kerberos tickets", ccPath, err) + log.Printf("Failed to load Kerberos credential cache from %s: %s", ccPath, err) + return conn, errors.New("failed to load Kerberos credential cache. Please run 'kinit' to obtain valid Kerberos tickets") } // Create Kerberos client from cache krbClient, err := client.NewClientFromCCache(cc, krb5cfg) if err != nil { - return conn, fmt.Errorf("failed to create Kerberos client: %w. Please check your Kerberos tickets with 'klist'", err) + log.Printf("Failed to create Kerberos client: %s", err) + return conn, errors.New("failed to create Kerberos client. Please check your Kerberos tickets with 'klist'") } // Kerberos Step 1: Send CONNECT with SPNEGO token directly (like NTLM does) @@ -114,13 +121,15 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( // Build SPNEGO token for the proxy SPN if err := spnego.SetSPNEGOHeader(krbClient, connect, kerberosConfig.ProxySPN); err != nil { - return conn, fmt.Errorf("failed to generate SPNEGO token for SPN '%s': %w. Please check if the SPN is correct", kerberosConfig.ProxySPN, err) + log.Printf("Failed to generate SPNEGO token for SPN '%s': %s", kerberosConfig.ProxySPN, err) + return conn, errors.New("failed to generate SPNEGO token. Please check if the SPN is correct") } // spnego.SetSPNEGOHeader sets Authorization: Negotiate authVal := connect.Header.Get("Authorization") if authVal == "" { - return conn, fmt.Errorf("SPNEGO failed to generate Authorization header for SPN '%s'. Please check Kerberos configuration", kerberosConfig.ProxySPN) + log.Printf("SPNEGO did not generate an Authorization header") + return conn, errors.New("failed to generate SPNEGO token. Please check Kerberos configuration") } // Move Authorization -> Proxy-Authorization (proxy level) @@ -150,9 +159,11 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( for _, v := range resp.Header.Values("Proxy-Authenticate") { log.Printf("Proxy-Authenticate: %s", v) } + _ = resp.Body.Close() + return conn, errors.New("proxy authentication failed. Check SPN and proxy keytab/KDC configuration") } _ = resp.Body.Close() - return conn, fmt.Errorf("proxy authentication failed: %s", resp.Status) + return conn, errors.New(http.StatusText(resp.StatusCode)) } // Successfully authorized with Kerberos From 773d4ce5509c8ce1516d3c69d9e4da18f4fad1a0 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:21:34 +0530 Subject: [PATCH 04/18] interupting-kerberos-incase-of-failure --- internal/wrappers/client.go | 12 +++++ internal/wrappers/kerberos/proxy-kerberos.go | 57 ++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 18f849902..4e84f467d 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptrace" "net/url" + "os" "strings" "sync" "time" @@ -229,6 +230,17 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { } ccachePath := viper.GetString(commonParams.ProxyKerberosCcacheKey) + // Early validation: Check Kerberos setup before creating client + // This ensures errors are caught immediately during client creation, not during HTTP requests + if err := kerberos.ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN); err != nil { + logger.PrintIfVerbose("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) + logger.PrintIfVerbose("Falling back to basic proxy authentication") + // This allows the CLI to continue working even with Kerberos misconfiguration + logger.Print("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) + os.Exit(1) + // return basicProxyClient(timeout, proxyStr) + } + logger.PrintIfVerbose("Creating HTTP client using Kerberos Proxy using: " + proxyStr) logger.PrintIfVerbose("Kerberos SPN: " + proxySPN) logger.PrintIfVerbose("Kerberos krb5.conf: " + krb5ConfPath) diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go index 2bc293141..fd89f344e 100644 --- a/internal/wrappers/kerberos/proxy-kerberos.go +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -172,6 +172,63 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( return conn, nil } +// ValidateKerberosSetup validates Kerberos configuration early to fail fast +// This function performs all the same checks as dialAndNegotiate but without actually +// making network connections, allowing early detection of configuration problems +func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error { + // Validate SPN + if proxySPN == "" { + return errors.New("Kerberos SPN is required. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var") + } + + // Use default krb5.conf path if not specified + if krb5ConfPath == "" { + krb5ConfPath = GetDefaultKrb5ConfPath() + } + + // Check if krb5.conf exists + if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) { + return errors.New("Kerberos configuration file not found. Please ensure krb5.conf is properly configured") + } + + // Load krb5.conf to validate it's readable + _, err := config.Load(krb5ConfPath) + if err != nil { + return errors.New("failed to load Kerberos configuration. Please check the krb5.conf file") + } + + // Get default credential cache path if not specified + if ccachePath == "" { + ccachePath = getDefaultCCachePath() + } + + // Check if credential cache exists + if ccachePath != "" { + if _, err := os.Stat(ccachePath); os.IsNotExist(err) { + return errors.New("Kerberos credential cache not found. Please run 'kinit' to obtain Kerberos tickets first") + } + } + + // Try to load credential cache to validate it's usable + cc, err := credentials.LoadCCache(ccachePath) + if err != nil { + return errors.New("failed to load Kerberos credential cache. Please run 'kinit' to obtain valid Kerberos tickets") + } + + // Try to create Kerberos client to validate tickets are valid + krb5cfg, err := config.Load(krb5ConfPath) + if err != nil { + return errors.New("failed to reload Kerberos configuration") + } + + _, err = client.NewClientFromCCache(cc, krb5cfg) + if err != nil { + return errors.New("failed to create Kerberos client. Please check your Kerberos tickets with 'klist'") + } + + return nil +} + // GetDefaultKrb5ConfPath returns the default krb5.conf path for the current platform func GetDefaultKrb5ConfPath() string { switch runtime.GOOS { From 96a5eb9d7caecd63284666d4447462c6e4b080d2 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:59:34 +0530 Subject: [PATCH 05/18] interupting-kerberos-incase-of-failure1 --- internal/wrappers/client.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 4e84f467d..4807176b2 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -11,7 +11,6 @@ import ( "net/http" "net/http/httptrace" "net/url" - "os" "strings" "sync" "time" @@ -236,9 +235,9 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { logger.PrintIfVerbose("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) logger.PrintIfVerbose("Falling back to basic proxy authentication") // This allows the CLI to continue working even with Kerberos misconfiguration - logger.Print("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) - os.Exit(1) - // return basicProxyClient(timeout, proxyStr) + logger.Printf("ERROR: Kerberos proxy authentication setup failed %v", err.Error()) + // Instead of os.Exit(1), return nil to avoid printing "exit status 1" + return nil } logger.PrintIfVerbose("Creating HTTP client using Kerberos Proxy using: " + proxyStr) From b3cfb1233e5a2b8a38a72475402fc73c2a3cccda Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:10:07 +0530 Subject: [PATCH 06/18] interrupting-silently --- internal/wrappers/client.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 4807176b2..b3665887f 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptrace" "net/url" + "os" "strings" "sync" "time" @@ -233,11 +234,11 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // This ensures errors are caught immediately during client creation, not during HTTP requests if err := kerberos.ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN); err != nil { logger.PrintIfVerbose("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) - logger.PrintIfVerbose("Falling back to basic proxy authentication") + // logger.PrintIfVerbose("Falling back to basic proxy authentication") // This allows the CLI to continue working even with Kerberos misconfiguration logger.Printf("ERROR: Kerberos proxy authentication setup failed %v", err.Error()) - // Instead of os.Exit(1), return nil to avoid printing "exit status 1" - return nil + os.Exit(0) + // return basicProxyClient(timeout, proxyStr) } logger.PrintIfVerbose("Creating HTTP client using Kerberos Proxy using: " + proxyStr) From a67145cc12b36f9e26a915f120aa83b687082875 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:12:35 +0530 Subject: [PATCH 07/18] log-line-corrected --- internal/wrappers/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index b3665887f..51116828c 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -236,7 +236,7 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { logger.PrintIfVerbose("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) // logger.PrintIfVerbose("Falling back to basic proxy authentication") // This allows the CLI to continue working even with Kerberos misconfiguration - logger.Printf("ERROR: Kerberos proxy authentication setup failed %v", err.Error()) + fmt.Println(fmt.Sprintf("ERROR: Kerberos proxy authentication setup failed %v", err.Error())) os.Exit(0) // return basicProxyClient(timeout, proxyStr) } From abc684f44928ad8c48263665fdcb4669310ece91 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:14:41 +0530 Subject: [PATCH 08/18] log-line-corrected1 --- internal/wrappers/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 51116828c..6a3b0d4d0 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -236,7 +236,7 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { logger.PrintIfVerbose("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) // logger.PrintIfVerbose("Falling back to basic proxy authentication") // This allows the CLI to continue working even with Kerberos misconfiguration - fmt.Println(fmt.Sprintf("ERROR: Kerberos proxy authentication setup failed %v", err.Error())) + fmt.Println(fmt.Sprintf("ERROR: Kerberos proxy authentication setup failed: %v", err.Error())) os.Exit(0) // return basicProxyClient(timeout, proxyStr) } From 7f3cdd0fdf2ee4bcc2cdb9a16a2dca382842e95f Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:47:31 +0530 Subject: [PATCH 09/18] formatted-error-message --- internal/wrappers/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 6a3b0d4d0..f9208ed5a 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -233,10 +233,10 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // Early validation: Check Kerberos setup before creating client // This ensures errors are caught immediately during client creation, not during HTTP requests if err := kerberos.ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN); err != nil { - logger.PrintIfVerbose("ERROR: Kerberos proxy authentication setup failed: " + err.Error()) + logger.PrintIfVerbose("Error: Kerberos proxy authentication setup failed: " + err.Error()) // logger.PrintIfVerbose("Falling back to basic proxy authentication") // This allows the CLI to continue working even with Kerberos misconfiguration - fmt.Println(fmt.Sprintf("ERROR: Kerberos proxy authentication setup failed: %v", err.Error())) + fmt.Println(fmt.Sprintf("Error: Kerberos proxy authentication setup failed: %v", err.Error())) os.Exit(0) // return basicProxyClient(timeout, proxyStr) } From b9c1c71f03286b0440001fab88fd22c0e79ff652 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:50:07 +0530 Subject: [PATCH 10/18] formatted-error-message1 --- internal/wrappers/client.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index f9208ed5a..c8c5f3885 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -234,11 +234,8 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // This ensures errors are caught immediately during client creation, not during HTTP requests if err := kerberos.ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN); err != nil { logger.PrintIfVerbose("Error: Kerberos proxy authentication setup failed: " + err.Error()) - // logger.PrintIfVerbose("Falling back to basic proxy authentication") - // This allows the CLI to continue working even with Kerberos misconfiguration fmt.Println(fmt.Sprintf("Error: Kerberos proxy authentication setup failed: %v", err.Error())) os.Exit(0) - // return basicProxyClient(timeout, proxyStr) } logger.PrintIfVerbose("Creating HTTP client using Kerberos Proxy using: " + proxyStr) From 1a58a277703f4aa95dde305bf036e8f25d3f631c Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:59:13 +0530 Subject: [PATCH 11/18] using-latest-gokrb5 --- PROXY_AUTHENTICATION_GUIDE.md | 389 +++++++++++++++++++ go.mod | 12 +- go.sum | 33 +- internal/wrappers/kerberos/proxy-kerberos.go | 12 +- 4 files changed, 423 insertions(+), 23 deletions(-) create mode 100644 PROXY_AUTHENTICATION_GUIDE.md diff --git a/PROXY_AUTHENTICATION_GUIDE.md b/PROXY_AUTHENTICATION_GUIDE.md new file mode 100644 index 000000000..b9bdcbad1 --- /dev/null +++ b/PROXY_AUTHENTICATION_GUIDE.md @@ -0,0 +1,389 @@ +# Proxy Authentication Guide + +This guide explains how to configure proxy authentication using NTLM and Kerberos protocols with the AST CLI tool. + +## Overview + +The AST CLI supports three types of proxy authentication: +- **Basic**: Standard username/password authentication +- **NTLM**: Windows NT LAN Manager authentication +- **Kerberos**: MIT Kerberos authentication protocol + +## Configuration Methods + +You can configure proxy authentication using either **command-line flags** or **environment variables**. + +--- + +## 🔧 Basic Proxy Authentication + +### Command Line Flags +```bash +cx scan create --proxy http://username:password@proxy.company.com:8080 +``` + +### Environment Variables +```bash +export HTTP_PROXY=http://username:password@proxy.company.com:8080 +# or +export CX_HTTP_PROXY=http://username:password@proxy.company.com:8080 +``` + +--- + +## 🔐 NTLM Proxy Authentication + +### When to Use NTLM +- Corporate environments using Windows-based proxy servers +- Active Directory domain authentication required +- Windows NTLM challenge-response authentication + +### Command Line Flags +```bash +cx scan create \ + --proxy http://proxy.company.com:8080 \ + --proxy-auth-type ntlm \ + --proxy-ntlm-domain COMPANY_DOMAIN +``` + +### Environment Variables +```bash +export CX_HTTP_PROXY=http://username:password@proxy.company.com:8080 +export CX_PROXY_AUTH_TYPE=ntlm +export CX_PROXY_NTLM_DOMAIN=COMPANY_DOMAIN +``` + +### NTLM Configuration Details + +| Flag | Environment Variable | Description | Required | +|------|---------------------|-------------|----------| +| `--proxy` | `CX_HTTP_PROXY` | Proxy URL with credentials | ✅ Yes | +| `--proxy-auth-type ntlm` | `CX_PROXY_AUTH_TYPE=ntlm` | Enable NTLM authentication | ✅ Yes | +| `--proxy-ntlm-domain` | `CX_PROXY_NTLM_DOMAIN` | Windows domain name | ✅ Yes | + +### NTLM Example +```bash +# Full NTLM configuration +cx scan create \ + --proxy http://john.doe:mypassword@proxy.company.com:8080 \ + --proxy-auth-type ntlm \ + --proxy-ntlm-domain COMPANY \ + --source-dir /path/to/source +``` + +--- + +## 🎫 Kerberos Proxy Authentication + +### When to Use Kerberos +- Enterprise environments with MIT Kerberos infrastructure +- Single Sign-On (SSO) requirements +- Enhanced security with ticket-based authentication +- Cross-platform Kerberos deployments + +### Prerequisites +1. **Kerberos tickets**: Obtain valid Kerberos tickets using `kinit` +2. **SPN configuration**: Know the Service Principal Name for your proxy +3. **krb5.conf**: Proper Kerberos configuration file + +### Command Line Flags +```bash +cx scan create \ + --proxy http://proxy.company.com:8080 \ + --proxy-auth-type kerberos \ + --proxy-kerberos-spn HTTP/proxy.company.com +``` + +### Environment Variables +```bash +export CX_HTTP_PROXY=http://proxy.company.com:8080 +export CX_PROXY_AUTH_TYPE=kerberos +export CX_PROXY_KERBEROS_SPN=HTTP/proxy.company.com +``` + +### Kerberos Configuration Details + +| Flag | Environment Variable | Description | Required | +|------|---------------------|-------------|----------| +| `--proxy` | `CX_HTTP_PROXY` | Proxy URL (no credentials needed) | ✅ Yes | +| `--proxy-auth-type kerberos` | `CX_PROXY_AUTH_TYPE=kerberos` | Enable Kerberos authentication | ✅ Yes | +| `--proxy-kerberos-spn` | `CX_PROXY_KERBEROS_SPN` | Service Principal Name for proxy | ✅ Yes | +| `--proxy-kerberos-krb5-conf` | `CX_PROXY_KERBEROS_KRB5_CONF` | Path to krb5.conf file | ❌ Optional | +| `--proxy-kerberos-ccache` | `CX_PROXY_KERBEROS_CCACHE` | Path to credential cache | ❌ Optional | + +### Kerberos Setup Steps + +#### 1. Obtain Kerberos Tickets +```bash +# Get Kerberos tickets for your user +kinit username@REALM.COM + +# Verify tickets are available +klist +``` + +#### 2. Configure SPN +```bash +# Example SPN format for HTTP proxy +--proxy-kerberos-spn HTTP/proxy.company.com + +# Example SPN for specific port +--proxy-kerberos-spn HTTP/proxy.company.com:8080 +``` + +#### 3. Run with Kerberos Authentication +```bash +cx scan create \ + --proxy http://proxy.company.com:8080 \ + --proxy-auth-type kerberos \ + --proxy-kerberos-spn HTTP/proxy.company.com \ + --source-dir /path/to/source +``` + +### Advanced Kerberos Configuration + +#### Custom krb5.conf Location +```bash +cx scan create \ + --proxy http://proxy.company.com:8080 \ + --proxy-auth-type kerberos \ + --proxy-kerberos-spn HTTP/proxy.company.com \ + --proxy-kerberos-krb5-conf /etc/custom/krb5.conf +``` + +#### Custom Credential Cache +```bash +cx scan create \ + --proxy http://proxy.company.com:8080 \ + --proxy-auth-type kerberos \ + --proxy-kerberos-spn HTTP/proxy.company.com \ + --proxy-kerberos-ccache /tmp/custom_krb5cc +``` + +--- + +## 🌍 Default Locations + +### Kerberos Configuration Files + +**Linux/macOS:** +- krb5.conf: `/etc/krb5.conf` +- Credential cache: `/tmp/krb5cc_$(id -u)` + +**Windows:** +- krb5.conf: `C:\Windows\krb5.ini` +- Credential cache: Managed by Windows credential manager + +### Environment Variables for Kerberos +```bash +# Override default credential cache location +export KRB5CCNAME=/path/to/custom/ccache + +# Standard Kerberos environment variable +export KRB5_CONFIG=/path/to/custom/krb5.conf +``` + +--- + +## 📋 Complete Examples + +### NTLM Corporate Environment +```bash +#!/bin/bash +# NTLM proxy authentication example + +export CX_HTTP_PROXY=http://jdoe:password123@proxy.corp.com:8080 +export CX_PROXY_AUTH_TYPE=ntlm +export CX_PROXY_NTLM_DOMAIN=CORP + +cx scan create \ + --project-name "MyProject" \ + --source-dir /workspace/myapp \ + --branch main +``` + +### Kerberos Enterprise Environment +```bash +#!/bin/bash +# Kerberos proxy authentication example + +# 1. Get Kerberos tickets +kinit jdoe@CORP.COM + +# 2. Configure proxy with Kerberos +export CX_HTTP_PROXY=http://proxy.corp.com:8080 +export CX_PROXY_AUTH_TYPE=kerberos +export CX_PROXY_KERBEROS_SPN=HTTP/proxy.corp.com + +# 3. Run scan +cx scan create \ + --project-name "MyProject" \ + --source-dir /workspace/myapp \ + --branch main +``` + +### Mixed Configuration (Environment + Flags) +```bash +# Set proxy in environment +export CX_HTTP_PROXY=http://proxy.company.com:8080 + +# Use Kerberos with command-line flags +cx scan create \ + --proxy-auth-type kerberos \ + --proxy-kerberos-spn HTTP/proxy.company.com \ + --project-name "MyProject" \ + --source-dir . +``` + +--- + +## 🚨 Troubleshooting + +### Common NTLM Issues + +**Problem**: Authentication fails with 407 Proxy Authentication Required +``` +Solution: Verify domain name and credentials +- Check --proxy-ntlm-domain matches your Windows domain +- Ensure username/password in proxy URL are correct +- Test domain format: try both "DOMAIN" and "domain.com" +``` + +**Problem**: Connection timeout +``` +Solution: Check proxy URL format +- Ensure proxy URL includes protocol: http:// or https:// +- Verify proxy server address and port are correct +``` + +### Common Kerberos Issues + +**Problem**: "Kerberos SPN is required" error +``` +Solution: Always provide the SPN +--proxy-kerberos-spn HTTP/proxy.company.com + +Check with your system administrator for the correct SPN format. +``` + +**Problem**: "Kerberos credential cache not found" +``` +Solution: Obtain Kerberos tickets first +kinit username@REALM.COM + +Verify tickets exist: +klist +``` + +**Problem**: "Failed to generate SPNEGO token" +``` +Solution: Check SPN format and proxy configuration +- Verify SPN matches proxy server configuration +- Ensure proxy server supports Kerberos authentication +- Check krb5.conf file is properly configured +``` + +**Problem**: "Kerberos configuration file not found" +``` +Solution: Specify krb5.conf location +--proxy-kerberos-krb5-conf /path/to/krb5.conf + +Or ensure krb5.conf exists in default location (/etc/krb5.conf) +``` + +### Testing Authentication + +#### Test NTLM +```bash +# Enable verbose logging to see authentication details +cx scan create --verbose \ + --proxy http://user:pass@proxy.com:8080 \ + --proxy-auth-type ntlm \ + --proxy-ntlm-domain DOMAIN \ + --project-name test +``` + +#### Test Kerberos +```bash +# Enable verbose logging for Kerberos +cx scan create --verbose \ + --proxy http://proxy.com:8080 \ + --proxy-auth-type kerberos \ + --proxy-kerberos-spn HTTP/proxy.com \ + --project-name test +``` + +--- + +## 🔒 Security Best Practices + +### For NTLM +1. **Use HTTPS proxies** when possible to encrypt credentials +2. **Avoid hardcoding passwords** in scripts - use environment variables +3. **Rotate passwords regularly** according to company policy +4. **Limit proxy access** to necessary users only + +### For Kerberos +1. **Secure credential cache** - ensure proper file permissions (600) +2. **Regular ticket renewal** - use kinit periodically for long-running processes +3. **SPN verification** - confirm SPN with proxy administrator +4. **Network security** - ensure Kerberos traffic is protected + +### General +1. **Use environment variables** instead of command-line flags for sensitive data +2. **Enable verbose logging** only for troubleshooting +3. **Test authentication** in non-production environments first +4. **Monitor proxy logs** for authentication attempts + +--- + +## 📞 Support + +If you encounter issues: + +1. **Check logs** with `--verbose` flag +2. **Verify proxy server** supports the chosen authentication method +3. **Contact system administrator** for SPN/domain configuration +4. **Test proxy connectivity** outside of AST CLI first + +### System Administrator Checklist + +For NTLM: +- [ ] Proxy supports NTLM authentication +- [ ] User account has proxy access permissions +- [ ] Windows domain is correctly configured + +For Kerberos: +- [ ] Proxy server has SPN registered in KDC +- [ ] Proxy supports SPNEGO/Kerberos authentication +- [ ] Client machine can reach KDC +- [ ] krb5.conf is properly configured + +--- + +## 📚 Reference + +### All Available Flags +``` +--proxy Proxy server URL +--proxy-auth-type Authentication type (basic|ntlm|kerberos) +--proxy-ntlm-domain Windows domain for NTLM +--proxy-kerberos-spn Service Principal Name for Kerberos +--proxy-kerberos-krb5-conf Path to krb5.conf file +--proxy-kerberos-ccache Path to Kerberos credential cache +--ignore-proxy Ignore all proxy settings +``` + +### All Environment Variables +``` +HTTP_PROXY Standard proxy environment variable +CX_HTTP_PROXY Checkmarx proxy URL +CX_PROXY_AUTH_TYPE Authentication type +CX_PROXY_NTLM_DOMAIN NTLM domain name +CX_PROXY_KERBEROS_SPN Kerberos Service Principal Name +CX_PROXY_KERBEROS_KRB5_CONF Kerberos configuration file path +CX_PROXY_KERBEROS_CCACHE Kerberos credential cache path +KRB5CCNAME Standard Kerberos cache environment variable +``` + +For more information, refer to the AST CLI documentation or contact your system administrator. diff --git a/go.mod b/go.mod index 85a2dc070..c528bd709 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/gomarkdown/markdown v0.0.0-20241102151059-6bc1ffdc6e8c github.com/google/uuid v1.6.0 github.com/gookit/color v1.5.4 + github.com/jcmturner/gokrb5/v8 v8.4.4 github.com/jsumners/go-getport v1.0.0 github.com/mssola/user_agent v0.6.0 github.com/pkg/errors v0.9.1 @@ -30,18 +31,17 @@ require ( golang.org/x/text v0.27.0 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 - gopkg.in/jcmturner/gokrb5.v7 v7.5.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools v2.2.0+incompatible ) require ( - github.com/hashicorp/go-uuid v1.0.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // 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 - gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect - gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect - gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect - gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect ) require ( diff --git a/go.sum b/go.sum index afe13006a..a2388d4bf 100644 --- a/go.sum +++ b/go.sum @@ -543,6 +543,10 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 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.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= @@ -578,8 +582,10 @@ github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 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 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= 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/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= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= @@ -612,8 +618,18 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +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/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= @@ -1093,6 +1109,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1179,6 +1196,8 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1287,12 +1306,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1540,16 +1561,6 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw= -gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= -gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM= -gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= -gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI= -gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= -gopkg.in/jcmturner/gokrb5.v7 v7.5.0 h1:a9tsXlIDD9SKxotJMK3niV7rPZAJeX2aD/0yg3qlIrg= -gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= -gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU= -gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go index fd89f344e..1e79cf387 100644 --- a/internal/wrappers/kerberos/proxy-kerberos.go +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -13,11 +13,11 @@ import ( "path/filepath" "runtime" + "github.com/jcmturner/gokrb5/v8/client" //nolint + "github.com/jcmturner/gokrb5/v8/config" //nolint + "github.com/jcmturner/gokrb5/v8/credentials" //nolint + "github.com/jcmturner/gokrb5/v8/spnego" //nolint "github.com/pkg/errors" - "gopkg.in/jcmturner/gokrb5.v7/client" - "gopkg.in/jcmturner/gokrb5.v7/config" - "gopkg.in/jcmturner/gokrb5.v7/credentials" - "gopkg.in/jcmturner/gokrb5.v7/spnego" ) // DialContext is the DialContext function that should be wrapped with a @@ -103,7 +103,7 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( } // Create Kerberos client from cache - krbClient, err := client.NewClientFromCCache(cc, krb5cfg) + krbClient, err := client.NewFromCCache(cc, krb5cfg) if err != nil { log.Printf("Failed to create Kerberos client: %s", err) return conn, errors.New("failed to create Kerberos client. Please check your Kerberos tickets with 'klist'") @@ -221,7 +221,7 @@ func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error { return errors.New("failed to reload Kerberos configuration") } - _, err = client.NewClientFromCCache(cc, krb5cfg) + _, err = client.NewFromCCache(cc, krb5cfg) if err != nil { return errors.New("failed to create Kerberos client. Please check your Kerberos tickets with 'klist'") } From ce539d11f06356d0a18737dce2407c05495c28ef Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:21:41 +0530 Subject: [PATCH 12/18] added-proxy-auth-guide --- Kerberos_Configuration_Guide.md | 97 +++++++++++++++++++++++++++++++++ PROXY_AUTHENTICATION_GUIDE.md | 10 +--- 2 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 Kerberos_Configuration_Guide.md diff --git a/Kerberos_Configuration_Guide.md b/Kerberos_Configuration_Guide.md new file mode 100644 index 000000000..18e17bf15 --- /dev/null +++ b/Kerberos_Configuration_Guide.md @@ -0,0 +1,97 @@ +# Kerberos Authentication - Configuration Guide + +## Configuration Parameters + +### Required Parameters + +| Parameter | Flag | Environment Variable | Required | Description | +|-----------|------|---------------------|----------|-------------| +| **Proxy URL** | `--proxy` | `CX_HTTP_PROXY` | ✅ Yes | Proxy server URL | +| **Auth Type** | `--proxy-auth-type kerberos` | `CX_PROXY_AUTH_TYPE=kerberos` | ✅ Yes | Enable Kerberos | +| **Service Principal** | `--proxy-kerberos-spn` | `CX_PROXY_KERBEROS_SPN` | ✅ Yes | Proxy SPN | + +### Optional Parameters + +| Parameter | Flag | Environment Variable | Default | Description | +|-----------|------|---------------------|---------|-------------| +| **krb5.conf Path** | `--proxy-kerberos-krb5-conf` | `CX_PROXY_KERBEROS_KRB5_CONF` | `/etc/krb5.conf` (Linux) `C:\Windows\krb5.ini` (Windows) | Kerberos config file | +| **Credential Cache** | `--proxy-kerberos-ccache` | `CX_PROXY_KERBEROS_CCACHE` | `/tmp/krb5cc_$(id -u)` (Linux) Windows Credential Manager (Windows) | Ticket cache location | + +### Standard Kerberos Variables + +| Variable | Purpose | When to Use | +|----------|---------|-------------| +| `KRB5CCNAME` | Custom credential cache location | Alternative cache path | +| `KRB5_CONFIG` | Custom krb5.conf file location | Alternative config file | + +## Configuration Precedence + +| Priority | Source | Example | +|----------|--------|---------| +| **1 (Highest)** | Command-Line Flags | `--proxy-kerberos-spn HTTP/proxy.company.com` | +| **2** | Environment Variables | `export CX_PROXY_KERBEROS_SPN=HTTP/proxy.company.com` | +| **3** | Configuration File | `cx_proxy_kerberos_spn: HTTP/proxy.company.com` | +| **4 (Lowest)** | Default Values | Platform defaults | + +## Examples + +### Example 1: Project List with Flags + +```bash +# Get Kerberos tickets first +kinit user@COMPANY.COM + +# Run command with flags +cx project list \ + --proxy "http://proxy.company.com:8080" \ + --proxy-auth-type kerberos \ + --proxy-kerberos-spn "HTTP/proxy.company.com" +``` + +### Example 2: Scan with Environment Variables + +```bash +# Get Kerberos tickets +kinit user@COMPANY.COM + +# Set environment variables +export CX_HTTP_PROXY="http://proxy.company.com:8080" +export CX_PROXY_AUTH_TYPE=kerberos +export CX_PROXY_KERBEROS_SPN="HTTP/proxy.company.com" + +# Run scan (environment variables applied automatically) +cx scan create \ + --project-name "projectName" \ + -s "codePath" \ + --branch "branchName" +``` + +### Example 3: Mixed Configuration + +```bash +# Get tickets +kinit user@COMPANY.COM + +# Set base configuration via environment +export CX_HTTP_PROXY="http://proxy.company.com:8080" +export CX_PROXY_AUTH_TYPE=kerberos + +# Override SPN with flag (higher precedence) +cx project list --proxy-kerberos-spn "HTTP/specific-proxy.company.com" +``` + +## Setup Steps + +1. **Get Kerberos tickets**: `kinit user@COMPANY.COM` +2. **Verify tickets**: `klist` +3. **Configure proxy settings** (flags or environment variables) +4. **Run AST CLI commands** + +## Quick Reference + +| Action | Command | +|--------|---------| +| **Get tickets** | `kinit user@COMPANY.COM` | +| **Check tickets** | `klist` | +| **Renew tickets** | `kinit -R` | +| **Test connectivity** | `curl --proxy "http://proxy.company.com:8080" http://httpbin.org/ip` | diff --git a/PROXY_AUTHENTICATION_GUIDE.md b/PROXY_AUTHENTICATION_GUIDE.md index b9bdcbad1..b07526381 100644 --- a/PROXY_AUTHENTICATION_GUIDE.md +++ b/PROXY_AUTHENTICATION_GUIDE.md @@ -75,12 +75,6 @@ cx scan create \ ## 🎫 Kerberos Proxy Authentication -### When to Use Kerberos -- Enterprise environments with MIT Kerberos infrastructure -- Single Sign-On (SSO) requirements -- Enhanced security with ticket-based authentication -- Cross-platform Kerberos deployments - ### Prerequisites 1. **Kerberos tickets**: Obtain valid Kerberos tickets using `kinit` 2. **SPN configuration**: Know the Service Principal Name for your proxy @@ -306,7 +300,7 @@ cx scan create --verbose \ #### Test Kerberos ```bash # Enable verbose logging for Kerberos -cx scan create --verbose \ +cx project list create --verbose \ --proxy http://proxy.com:8080 \ --proxy-auth-type kerberos \ --proxy-kerberos-spn HTTP/proxy.com \ @@ -386,4 +380,4 @@ CX_PROXY_KERBEROS_CCACHE Kerberos credential cache path KRB5CCNAME Standard Kerberos cache environment variable ``` -For more information, refer to the AST CLI documentation or contact your system administrator. + From 32b7fb82905dd85b8d0908acb3664cac55d8dd7f Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:34:57 +0530 Subject: [PATCH 13/18] handled-no-retry-for-wrong-spn --- internal/wrappers/client.go | 5 +++++ internal/wrappers/kerberos/proxy-kerberos.go | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index c8c5f3885..8f41ccd26 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -723,6 +723,11 @@ func request(client *http.Client, req *http.Request, responseBody bool) (*http.R Domains = AppendIfNotExists(Domains, req.URL.Host) if err != nil { logger.PrintIfVerbose(err.Error()) + // Check if this is a non-retryable error (e.g., wrong Kerberos SPN) + if kerberos.IsNonRetryable(err) { + logger.PrintIfVerbose("Non-retryable error detected, skipping retries") + return nil, err + } } if resp != nil && err == nil { if hasRedirectStatusCode(resp) { diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go index 1e79cf387..674349279 100644 --- a/internal/wrappers/kerberos/proxy-kerberos.go +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -20,6 +20,21 @@ import ( "github.com/pkg/errors" ) +// NonRetryableError represents an error that should not trigger HTTP request retries +type NonRetryableError struct { + Message string +} + +func (e *NonRetryableError) Error() string { + return e.Message +} + +// IsNonRetryable returns true if the error should not trigger retries +func IsNonRetryable(err error) bool { + _, ok := err.(*NonRetryableError) + return ok +} + // DialContext is the DialContext function that should be wrapped with a // Kerberos Authentication. type DialContext func(ctx context.Context, network, addr string) (net.Conn, error) @@ -122,7 +137,7 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( // Build SPNEGO token for the proxy SPN if err := spnego.SetSPNEGOHeader(krbClient, connect, kerberosConfig.ProxySPN); err != nil { log.Printf("Failed to generate SPNEGO token for SPN '%s': %s", kerberosConfig.ProxySPN, err) - return conn, errors.New("failed to generate SPNEGO token. Please check if the SPN is correct") + return conn, &NonRetryableError{Message: "failed to generate SPNEGO token. Please check if the SPN is correct"} } // spnego.SetSPNEGOHeader sets Authorization: Negotiate @@ -160,7 +175,7 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( log.Printf("Proxy-Authenticate: %s", v) } _ = resp.Body.Close() - return conn, errors.New("proxy authentication failed. Check SPN and proxy keytab/KDC configuration") + return conn, &NonRetryableError{Message: "proxy authentication failed. Check SPN and proxy keytab/KDC configuration"} } _ = resp.Body.Close() return conn, errors.New(http.StatusText(resp.StatusCode)) From 4b1d7e97d9f0fe87f99481d9e203027db0fc6ef4 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:28:09 +0530 Subject: [PATCH 14/18] kerberos-spn-missing-print-msg --- internal/wrappers/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 8f41ccd26..b2efccfe8 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -218,6 +218,7 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // Validate required SPN parameter if proxySPN == "" { logger.PrintIfVerbose("ERROR: Kerberos SPN is required for Kerberos proxy authentication.") + logger.Print("ERROR: Kerberos SPN is required for Kerberos proxy authentication.") logger.PrintIfVerbose("Please provide SPN using: --proxy-kerberos-spn 'HTTP/proxy.example.com' or set CX_PROXY_KERBEROS_SPN environment variable") logger.PrintIfVerbose("Falling back to basic proxy authentication") // Return a basic client that will fail gracefully From 28b4797e6285d78e80885dcee3595f5b88c136df Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:04:56 +0530 Subject: [PATCH 15/18] spn-error-msg1 --- internal/wrappers/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index b2efccfe8..f844d35fe 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -218,7 +218,7 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // Validate required SPN parameter if proxySPN == "" { logger.PrintIfVerbose("ERROR: Kerberos SPN is required for Kerberos proxy authentication.") - logger.Print("ERROR: Kerberos SPN is required for Kerberos proxy authentication.") + logger.Print("ERROR: Kerberos SPN is required for the Kerberos proxy authentication.") logger.PrintIfVerbose("Please provide SPN using: --proxy-kerberos-spn 'HTTP/proxy.example.com' or set CX_PROXY_KERBEROS_SPN environment variable") logger.PrintIfVerbose("Falling back to basic proxy authentication") // Return a basic client that will fail gracefully From e07f2bfb3e990823e502b41781066e16de466ded Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:07:48 +0530 Subject: [PATCH 16/18] review-changes --- internal/params/flags.go | 2 +- internal/wrappers/client.go | 9 ++--- internal/wrappers/kerberos/proxy-kerberos.go | 37 +++----------------- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/internal/params/flags.go b/internal/params/flags.go index e2a8022c6..31bf51958 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -80,7 +80,7 @@ const ( SastRecommendedExclusionsFlags = "sast-recommended-exclusions" NtlmProxyDomainFlagUsage = "Window domain when using NTLM proxy" KerberosProxySPNFlagUsage = "Service Principal Name (SPN) for Kerberos proxy authentication" - KerberosKrb5ConfFlagUsage = "Path to krb5.conf file for Kerberos (default: /etc/krb5.conf)" + KerberosKrb5ConfFlagUsage = "Path to krb5 configuration file for Kerberos (default: /etc/krb5.conf)" KerberosCcacheFlagUsage = "Path to Kerberos credential cache (optional, uses KRB5CCNAME env or default)" BaseURIFlagUsage = "The base system URI" BaseAuthURIFlag = "base-auth-uri" diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index f844d35fe..04452d74d 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -217,8 +217,8 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // Validate required SPN parameter if proxySPN == "" { - logger.PrintIfVerbose("ERROR: Kerberos SPN is required for Kerberos proxy authentication.") - logger.Print("ERROR: Kerberos SPN is required for the Kerberos proxy authentication.") + logger.PrintIfVerbose("Error: Kerberos SPN is required for Kerberos proxy authentication.") + logger.Print("Error: Kerberos SPN is required for the Kerberos proxy authentication.") logger.PrintIfVerbose("Please provide SPN using: --proxy-kerberos-spn 'HTTP/proxy.example.com' or set CX_PROXY_KERBEROS_SPN environment variable") logger.PrintIfVerbose("Falling back to basic proxy authentication") // Return a basic client that will fail gracefully @@ -229,19 +229,20 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { if krb5ConfPath == "" { krb5ConfPath = kerberos.GetDefaultKrb5ConfPath() } + ccachePath := viper.GetString(commonParams.ProxyKerberosCcacheKey) // Early validation: Check Kerberos setup before creating client // This ensures errors are caught immediately during client creation, not during HTTP requests if err := kerberos.ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN); err != nil { logger.PrintIfVerbose("Error: Kerberos proxy authentication setup failed: " + err.Error()) - fmt.Println(fmt.Sprintf("Error: Kerberos proxy authentication setup failed: %v", err.Error())) + logger.Printf("Error: Kerberos proxy authentication setup failed: %v", err.Error()) os.Exit(0) } logger.PrintIfVerbose("Creating HTTP client using Kerberos Proxy using: " + proxyStr) logger.PrintIfVerbose("Kerberos SPN: " + proxySPN) - logger.PrintIfVerbose("Kerberos krb5.conf: " + krb5ConfPath) + logger.PrintIfVerbose("Kerberos krb5 configuration file: " + krb5ConfPath) kerberosConfig := kerberos.KerberosConfig{ ProxySPN: proxySPN, diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go index 674349279..290556850 100644 --- a/internal/wrappers/kerberos/proxy-kerberos.go +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -66,11 +66,6 @@ func NewKerberosProxyDialContext(dialer *net.Dialer, proxyURL *url.URL, } func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func() (net.Conn, error)) (net.Conn, error) { - // Validate required SPN parameter early - if kerberosConfig.ProxySPN == "" { - log.Printf("Kerberos SPN is required but not provided") - return nil, errors.New("Kerberos SPN is required. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var") - } conn, err := baseDial() if err != nil { @@ -80,36 +75,16 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( // Use default krb5.conf path if not specified krb5ConfPath := kerberosConfig.Krb5ConfPath - if krb5ConfPath == "" { - krb5ConfPath = GetDefaultKrb5ConfPath() - } - - // Check if krb5.conf exists before trying to load it - if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) { - log.Printf("Kerberos configuration file not found at %s", krb5ConfPath) - return conn, errors.New("Kerberos configuration file not found. Please ensure krb5.conf is properly configured") - } // Load krb5.conf krb5cfg, err := config.Load(krb5ConfPath) if err != nil { - log.Printf("Failed to load krb5.conf from %s: %s", krb5ConfPath, err) - return conn, errors.New("failed to load Kerberos configuration. Please check the krb5.conf file") + log.Printf("Failed to load krb5 configuration file from %s: %s", krb5ConfPath, err) + return conn, errors.New("failed to load Kerberos configuration. Please check the krb5 configuration file") } // Load credential cache ccPath := kerberosConfig.CcachePath - if ccPath == "" { - ccPath = getDefaultCCachePath() - } - - // Check if credential cache exists before trying to load it - if ccPath != "" { - if _, err := os.Stat(ccPath); os.IsNotExist(err) { - log.Printf("Kerberos credential cache not found at %s", ccPath) - return conn, errors.New("Kerberos credential cache not found. Please run 'kinit' to obtain Kerberos tickets first") - } - } cc, err := credentials.LoadCCache(ccPath) if err != nil { @@ -191,10 +166,6 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( // This function performs all the same checks as dialAndNegotiate but without actually // making network connections, allowing early detection of configuration problems func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error { - // Validate SPN - if proxySPN == "" { - return errors.New("Kerberos SPN is required. Use --proxy-kerberos-spn flag or CX_PROXY_KERBEROS_SPN env var") - } // Use default krb5.conf path if not specified if krb5ConfPath == "" { @@ -203,13 +174,13 @@ func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error { // Check if krb5.conf exists if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) { - return errors.New("Kerberos configuration file not found. Please ensure krb5.conf is properly configured") + return errors.New("Kerberos configuration file not found. Please ensure krb5 configuration file is properly configured") } // Load krb5.conf to validate it's readable _, err := config.Load(krb5ConfPath) if err != nil { - return errors.New("failed to load Kerberos configuration. Please check the krb5.conf file") + return errors.New("failed to load Kerberos configuration. Please check the krb5 configuration file") } // Get default credential cache path if not specified From beb3d6b8cf460a406c02a8ce39328abf858a837e Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:08:49 +0530 Subject: [PATCH 17/18] msgs-txt-reviewed --- internal/params/flags.go | 6 ++-- internal/wrappers/client.go | 2 +- internal/wrappers/kerberos/proxy-kerberos.go | 31 +++++++------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/internal/params/flags.go b/internal/params/flags.go index 31bf51958..8667ad85f 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -67,7 +67,7 @@ const ( IgnoreProxyFlag = "ignore-proxy" IgnoreProxyFlagUsage = "Ignore proxy configuration" ProxyTypeFlag = "proxy-auth-type" - ProxyTypeFlagUsage = "Proxy authentication type, (basic, ntlm, or kerberos)" + ProxyTypeFlagUsage = "Proxy authentication type (supported types: basic, ntlm or Kerberos)" TimeoutFlag = "timeout" TimeoutFlagUsage = "Timeout for network activity, (default 5 seconds)" NtlmProxyDomainFlag = "proxy-ntlm-domain" @@ -80,8 +80,8 @@ const ( SastRecommendedExclusionsFlags = "sast-recommended-exclusions" NtlmProxyDomainFlagUsage = "Window domain when using NTLM proxy" KerberosProxySPNFlagUsage = "Service Principal Name (SPN) for Kerberos proxy authentication" - KerberosKrb5ConfFlagUsage = "Path to krb5 configuration file for Kerberos (default: /etc/krb5.conf)" - KerberosCcacheFlagUsage = "Path to Kerberos credential cache (optional, uses KRB5CCNAME env or default)" + KerberosKrb5ConfFlagUsage = "Path to Kerberos configuration file(default: /etc/krb5.conf on linux and C:\\Windows\\krb5.ini on windows)" + KerberosCcacheFlagUsage = "Path to Kerberos credential cache (optional, default uses KRB5CCNAME env or OS default)" BaseURIFlagUsage = "The base system URI" BaseAuthURIFlag = "base-auth-uri" BaseAuthURIFlagUsage = "The base system IAM URI" diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 04452d74d..c3c15b377 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -218,7 +218,7 @@ func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { // Validate required SPN parameter if proxySPN == "" { logger.PrintIfVerbose("Error: Kerberos SPN is required for Kerberos proxy authentication.") - logger.Print("Error: Kerberos SPN is required for the Kerberos proxy authentication.") + logger.Print("Error: Kerberos SPN is required for Kerberos proxy authentication.") logger.PrintIfVerbose("Please provide SPN using: --proxy-kerberos-spn 'HTTP/proxy.example.com' or set CX_PROXY_KERBEROS_SPN environment variable") logger.PrintIfVerbose("Falling back to basic proxy authentication") // Return a basic client that will fail gracefully diff --git a/internal/wrappers/kerberos/proxy-kerberos.go b/internal/wrappers/kerberos/proxy-kerberos.go index 290556850..649e50ece 100644 --- a/internal/wrappers/kerberos/proxy-kerberos.go +++ b/internal/wrappers/kerberos/proxy-kerberos.go @@ -20,6 +20,8 @@ import ( "github.com/pkg/errors" ) +const osWindows = "windows" + // NonRetryableError represents an error that should not trigger HTTP request retries type NonRetryableError struct { Message string @@ -66,7 +68,6 @@ func NewKerberosProxyDialContext(dialer *net.Dialer, proxyURL *url.URL, } func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func() (net.Conn, error)) (net.Conn, error) { - conn, err := baseDial() if err != nil { log.Printf("Could not call dial context with proxy: %s", err) @@ -166,21 +167,19 @@ func dialAndNegotiate(addr string, kerberosConfig KerberosConfig, baseDial func( // This function performs all the same checks as dialAndNegotiate but without actually // making network connections, allowing early detection of configuration problems func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error { - - // Use default krb5.conf path if not specified if krb5ConfPath == "" { - krb5ConfPath = GetDefaultKrb5ConfPath() + krb5ConfPath = GetDefaultKrb5ConfPath() // Use default krb5.conf path if not specified } // Check if krb5.conf exists if _, err := os.Stat(krb5ConfPath); os.IsNotExist(err) { - return errors.New("Kerberos configuration file not found. Please ensure krb5 configuration file is properly configured") + 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.") } // Load krb5.conf to validate it's readable - _, err := config.Load(krb5ConfPath) + krb5cfg, err := config.Load(krb5ConfPath) if err != nil { - return errors.New("failed to load Kerberos configuration. Please check the krb5 configuration file") + 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.") } // Get default credential cache path if not specified @@ -191,25 +190,19 @@ func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error { // Check if credential cache exists if ccachePath != "" { if _, err := os.Stat(ccachePath); os.IsNotExist(err) { - return errors.New("Kerberos credential cache not found. Please run 'kinit' to obtain Kerberos tickets first") + 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.") } } // Try to load credential cache to validate it's usable cc, err := credentials.LoadCCache(ccachePath) if err != nil { - return errors.New("failed to load Kerberos credential cache. Please run 'kinit' to obtain valid Kerberos tickets") - } - - // Try to create Kerberos client to validate tickets are valid - krb5cfg, err := config.Load(krb5ConfPath) - if err != nil { - return errors.New("failed to reload Kerberos configuration") + 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.") } _, err = client.NewFromCCache(cc, krb5cfg) if err != nil { - return errors.New("failed to create Kerberos client. Please check your Kerberos tickets with 'klist'") + return errors.New("Failed to create Kerberos client. Please check your Kerberos tickets with 'klist'") } return nil @@ -218,7 +211,7 @@ func ValidateKerberosSetup(krb5ConfPath, ccachePath, proxySPN string) error { // GetDefaultKrb5ConfPath returns the default krb5.conf path for the current platform func GetDefaultKrb5ConfPath() string { switch runtime.GOOS { - case "windows": + case osWindows: // Windows typically uses krb5.ini if windir := os.Getenv("WINDIR"); windir != "" { return filepath.Join(windir, "krb5.ini") @@ -248,9 +241,7 @@ func getDefaultCCachePath() string { } switch runtime.GOOS { - case "windows": - // On Windows, use the default credential cache managed by the system - // The gokrb5 library should handle this automatically with empty string + case osWindows: return "" default: // Linux, macOS, and other Unix-like systems From 6aec75abd6526b2a22b8a22981c90a429fe6d73a Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:14:38 +0530 Subject: [PATCH 18/18] made-30-sec-const --- internal/wrappers/client.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index c3c15b377..e239aa97e 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -46,6 +46,7 @@ const ( contentTypeHeader = "Content-Type" formURLContentType = "application/x-www-form-urlencoded" jsonContentType = "application/json" + defaultDialerDuration = 30 * time.Second ) var ( @@ -187,8 +188,8 @@ func basicProxyClient(timeout uint, proxyStr string) *http.Client { func ntmlProxyClient(timeout uint, proxyStr string) *http.Client { dialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + Timeout: defaultDialerDuration, + KeepAlive: defaultDialerDuration, } u, _ := url.Parse(proxyStr) domainStr := viper.GetString(commonParams.ProxyDomainKey) @@ -207,8 +208,8 @@ func ntmlProxyClient(timeout uint, proxyStr string) *http.Client { func kerberosProxyClient(timeout uint, proxyStr string) *http.Client { dialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + Timeout: defaultDialerDuration, + KeepAlive: defaultDialerDuration, } u, _ := url.Parse(proxyStr)