diff --git a/docs/generated/settings/settings-for-tenants.txt b/docs/generated/settings/settings-for-tenants.txt index 01928db040a5..3bd7545140b4 100644 --- a/docs/generated/settings/settings-for-tenants.txt +++ b/docs/generated/settings/settings-for-tenants.txt @@ -88,6 +88,8 @@ server.hot_ranges_request.node.timeout duration 5m0s the duration allowed for a server.hsts.enabled boolean false if true, HSTS headers will be sent along with all HTTP requests. The headers will contain a max-age setting of one year. Browsers honoring the header will always use HTTPS to access the DB Console. Ensure that TLS is correctly configured prior to enabling. application server.http.base_path string / path to redirect the user to upon succcessful login application server.identity_map.configuration string system-identity to database-username mappings application +server.jwt_authentication.client.custom_ca string sets the custom root CA (appended to system's default CAs) for verifying certificates when interacting with jwt internal HTTPS client application +server.jwt_authentication.client.timeout duration 3s sets the timeout for jwt http client operations application server.log_gc.max_deletions_per_cycle integer 1000 the maximum number of entries to delete on each purge of log-like system tables application server.log_gc.period duration 1h0m0s the period at which log-like system tables are checked for old entries application server.max_connections_per_gateway integer -1 the maximum number of SQL connections per gateway allowed at a given time (note: this will only limit future connection attempts and will not affect already established connections). Negative values result in unlimited number of connections. Superusers are not affected by this limit. application diff --git a/docs/generated/settings/settings.html b/docs/generated/settings/settings.html index e29897bde112..f68888365606 100644 --- a/docs/generated/settings/settings.html +++ b/docs/generated/settings/settings.html @@ -116,6 +116,8 @@
server.hsts.enabled
booleanfalseif true, HSTS headers will be sent along with all HTTP requests. The headers will contain a max-age setting of one year. Browsers honoring the header will always use HTTPS to access the DB Console. Ensure that TLS is correctly configured prior to enabling.Serverless/Dedicated/Self-Hosted
server.http.base_path
string/path to redirect the user to upon succcessful loginServerless/Dedicated/Self-Hosted
server.identity_map.configuration
stringsystem-identity to database-username mappingsServerless/Dedicated/Self-Hosted +
server.jwt_authentication.client.custom_ca
stringsets the custom root CA (appended to system's default CAs) for verifying certificates when interacting with jwt internal HTTPS clientServerless/Dedicated/Self-Hosted +
server.jwt_authentication.client.timeout
duration3ssets the timeout for jwt http client operationsServerless/Dedicated/Self-Hosted
server.log_gc.max_deletions_per_cycle
integer1000the maximum number of entries to delete on each purge of log-like system tablesServerless/Dedicated/Self-Hosted
server.log_gc.period
duration1h0m0sthe period at which log-like system tables are checked for old entriesServerless/Dedicated/Self-Hosted
server.max_connections_per_gateway
integer-1the maximum number of SQL connections per gateway allowed at a given time (note: this will only limit future connection attempts and will not affect already established connections). Negative values result in unlimited number of connections. Superusers are not affected by this limit.Serverless/Dedicated/Self-Hosted diff --git a/pkg/ccl/jwtauthccl/authentication_jwt.go b/pkg/ccl/jwtauthccl/authentication_jwt.go index 8a106b46742e..0308e1d62827 100644 --- a/pkg/ccl/jwtauthccl/authentication_jwt.go +++ b/pkg/ccl/jwtauthccl/authentication_jwt.go @@ -14,6 +14,7 @@ import ( "fmt" "io" "strings" + "time" "github.com/cockroachdb/cockroach/pkg/ccl/utilccl" "github.com/cockroachdb/cockroach/pkg/security/username" @@ -71,6 +72,8 @@ type jwtAuthenticatorConf struct { jwks jwk.Set claim string jwksAutoFetchEnabled bool + clientTimeout time.Duration + customCA string } // reloadConfig locks mutex and then refreshes the values in conf from the cluster settings. @@ -91,6 +94,8 @@ func (authenticator *jwtAuthenticator) reloadConfigLocked( jwks: mustParseJWKS(JWTAuthJWKS.Get(&st.SV)), claim: JWTAuthClaim.Get(&st.SV), jwksAutoFetchEnabled: JWKSAutoFetchEnabled.Get(&st.SV), + clientTimeout: JWTClientTimeout.Get(&st.SV), + customCA: JWTClientCustomCA.Get(&st.SV), } if !authenticator.mu.conf.enabled && conf.enabled { @@ -146,7 +151,8 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin( // The token will be parsed again later to actually verify the signature. unverifiedToken, err := jwt.Parse(tokenBytes) if err != nil { - return errors.Newf("JWT authentication: invalid token") + return errors.WithDetailf( + errors.Newf("JWT authentication: invalid token"), "token parsing failed: %v", err) } // Check for issuer match against configured issuers. @@ -168,9 +174,11 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin( var jwkSet jwk.Set // If auto-fetch is enabled, fetch the JWKS remotely from the issuer's well known jwks url. if authenticator.mu.conf.jwksAutoFetchEnabled { - jwkSet, err = remoteFetchJWKS(ctx, issuerUrl) + jwkSet, err = authenticator.remoteFetchJWKS(ctx, issuerUrl) if err != nil { - return errors.Newf("JWT authentication: unable to validate token") + return errors.WithDetailf( + errors.Newf("JWT authentication: unable to validate token"), + "unable to fetch jwks: %v", err) } } else { jwkSet = authenticator.mu.conf.jwks @@ -179,7 +187,7 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin( // Now that both the issuer and key-id are matched, parse the token again to validate the signature. parsedToken, err := jwt.Parse(tokenBytes, jwt.WithKeySet(jwkSet), jwt.WithValidate(true), jwt.InferAlgorithmFromKey(true)) if err != nil { - return errors.Newf("JWT authentication: invalid token") + return errors.WithDetailf(errors.Newf("JWT authentication: invalid token"), "unable to parse token: %v", err) } // Extract all requested principals from the token. By default, we take it from the subject unless they specify @@ -269,12 +277,14 @@ func (authenticator *jwtAuthenticator) ValidateJWTLogin( } // remoteFetchJWKS fetches the JWKS from the provided URI. -func remoteFetchJWKS(ctx context.Context, issuerUrl string) (jwk.Set, error) { - jwksUrl, err := getJWKSUrl(ctx, issuerUrl) +func (authenticator *jwtAuthenticator) remoteFetchJWKS( + ctx context.Context, issuerUrl string, +) (jwk.Set, error) { + jwksUrl, err := authenticator.getJWKSUrl(ctx, issuerUrl) if err != nil { return nil, err } - body, err := getHttpResponse(ctx, jwksUrl) + body, err := getHttpResponse(ctx, jwksUrl, authenticator) if err != nil { return nil, err } @@ -286,12 +296,14 @@ func remoteFetchJWKS(ctx context.Context, issuerUrl string) (jwk.Set, error) { } // getJWKSUrl returns the JWKS URI from the OpenID configuration endpoint. -func getJWKSUrl(ctx context.Context, issuerUrl string) (string, error) { +func (authenticator *jwtAuthenticator) getJWKSUrl( + ctx context.Context, issuerUrl string, +) (string, error) { type OIDCConfigResponse struct { JWKSUri string `json:"jwks_uri"` } openIdConfigEndpoint := getOpenIdConfigEndpoint(issuerUrl) - body, err := getHttpResponse(ctx, openIdConfigEndpoint) + body, err := getHttpResponse(ctx, openIdConfigEndpoint, authenticator) if err != nil { return "", err } @@ -311,8 +323,11 @@ func getOpenIdConfigEndpoint(issuerUrl string) string { return openIdConfigEndpoint } -var getHttpResponse = func(ctx context.Context, url string) ([]byte, error) { - resp, err := httputil.Get(ctx, url) +var getHttpResponse = func(ctx context.Context, url string, authenticator *jwtAuthenticator) ([]byte, error) { + responseTimeout := authenticator.mu.conf.clientTimeout + httpClient := httputil.NewClientWithTimeoutsCustomCA( + responseTimeout, responseTimeout, authenticator.mu.conf.customCA) + resp, err := httpClient.Get(context.Background(), url) if err != nil { return nil, err } @@ -354,6 +369,12 @@ var ConfigureJWTAuth = func( JWKSAutoFetchEnabled.SetOnChange(&st.SV, func(ctx context.Context) { authenticator.reloadConfig(ambientCtx.AnnotateCtx(ctx), st) }) + JWTClientTimeout.SetOnChange(&st.SV, func(ctx context.Context) { + authenticator.reloadConfig(ambientCtx.AnnotateCtx(ctx), st) + }) + JWTClientCustomCA.SetOnChange(&st.SV, func(ctx context.Context) { + authenticator.reloadConfig(ambientCtx.AnnotateCtx(ctx), st) + }) return &authenticator } diff --git a/pkg/ccl/jwtauthccl/authentication_jwt_test.go b/pkg/ccl/jwtauthccl/authentication_jwt_test.go index ce0d97713003..511f10656f66 100644 --- a/pkg/ccl/jwtauthccl/authentication_jwt_test.go +++ b/pkg/ccl/jwtauthccl/authentication_jwt_test.go @@ -618,7 +618,7 @@ func TestAudienceCheck(t *testing.T) { // mockGetHttpResponseWithLocalFileContent is a mock function for getHttpResponse. This is used to intercept the call to // getHttpResponse and return the content of a local file instead of making a http call. -var mockGetHttpResponseWithLocalFileContent = func(ctx context.Context, url string) ([]byte, error) { +var mockGetHttpResponseWithLocalFileContent = func(ctx context.Context, url string, authenticator *jwtAuthenticator) ([]byte, error) { // remove https:// and replace / with _ in the url to get the testdata file name fileName := "testdata/" + strings.ReplaceAll(strings.ReplaceAll(url, "https://", ""), "/", "_") // read content of the file as a byte array diff --git a/pkg/ccl/jwtauthccl/settings.go b/pkg/ccl/jwtauthccl/settings.go index 995415fcf663..a8984673e1a1 100644 --- a/pkg/ccl/jwtauthccl/settings.go +++ b/pkg/ccl/jwtauthccl/settings.go @@ -13,19 +13,22 @@ import ( "encoding/json" "github.com/cockroachdb/cockroach/pkg/settings" + "github.com/cockroachdb/cockroach/pkg/util/httputil" "github.com/cockroachdb/errors" "github.com/lestrrat-go/jwx/jwk" ) // All cluster settings necessary for the JWT authentication feature. const ( - baseJWTAuthSettingName = "server.jwt_authentication." - JWTAuthAudienceSettingName = baseJWTAuthSettingName + "audience" - JWTAuthEnabledSettingName = baseJWTAuthSettingName + "enabled" - JWTAuthIssuersSettingName = baseJWTAuthSettingName + "issuers" - JWTAuthJWKSSettingName = baseJWTAuthSettingName + "jwks" - JWTAuthClaimSettingName = baseJWTAuthSettingName + "claim" - JWKSAutoFetchEnabledSettingName = baseJWTAuthSettingName + "jwks_auto_fetch.enabled" + baseJWTAuthSettingName = "server.jwt_authentication." + JWTAuthAudienceSettingName = baseJWTAuthSettingName + "audience" + JWTAuthEnabledSettingName = baseJWTAuthSettingName + "enabled" + JWTAuthIssuersSettingName = baseJWTAuthSettingName + "issuers" + JWTAuthJWKSSettingName = baseJWTAuthSettingName + "jwks" + JWTAuthClaimSettingName = baseJWTAuthSettingName + "claim" + JWKSAutoFetchEnabledSettingName = baseJWTAuthSettingName + "jwks_auto_fetch.enabled" + JWTAuthClientTimeoutSettingName = baseJWTAuthSettingName + "client.timeout" + JWTAuthClientCustomCASettingName = baseJWTAuthSettingName + "client.custom_ca" ) // JWTAuthClaim sets the JWT claim that is parsed to get the username. @@ -84,6 +87,22 @@ var JWKSAutoFetchEnabled = settings.RegisterBoolSetting( settings.WithReportable(true), ) +// JWTClientTimeout is a cluster setting used for setting jwt http client interactions. +var JWTClientTimeout = settings.RegisterDurationSetting( + settings.ApplicationLevel, + JWTAuthClientTimeoutSettingName, + "sets the timeout for jwt http client operations", + httputil.StandardHTTPTimeout, + settings.WithPublic) + +var JWTClientCustomCA = settings.RegisterStringSetting( + settings.ApplicationLevel, + JWTAuthClientCustomCASettingName, + "sets the custom root CA (appended to system's default CAs) for verifying "+ + "certificates when interacting with jwt internal HTTPS client", + "", + settings.WithPublic) + func validateJWTAuthIssuers(values *settings.Values, s string) error { var issuers []string diff --git a/pkg/sql/pgwire/auth.go b/pkg/sql/pgwire/auth.go index 224cede13ce2..8369298ac958 100644 --- a/pkg/sql/pgwire/auth.go +++ b/pkg/sql/pgwire/auth.go @@ -553,7 +553,8 @@ func (p *authPipe) LogAuthFailed( p.loggedFailure = true var errStr redact.RedactableString if detailedErr != nil { - errStr = redact.Sprint(detailedErr) + // should we apply redact here? + errStr = redact.Sprint(detailedErr).Redact() } ev := &eventpb.ClientAuthenticationFailed{ CommonConnectionDetails: p.connDetails, diff --git a/pkg/sql/pgwire/auth_methods.go b/pkg/sql/pgwire/auth_methods.go index b2a298b8ea88..b3237b10669d 100644 --- a/pkg/sql/pgwire/auth_methods.go +++ b/pkg/sql/pgwire/auth_methods.go @@ -781,7 +781,10 @@ func authJwtToken( return security.NewErrPasswordUserAuthFailed(user) } if err = jwtVerifier.ValidateJWTLogin(ctx, execCfg.Settings, user, []byte(token), identMap); err != nil { + // We are logging unsafe part of error also. Also, should we look into + // obtaining more specific auth failure reason here? c.LogAuthFailed(ctx, eventpb.AuthFailReason_CREDENTIALS_INVALID, err) + // we are returning unsafe error to client return err } c.LogAuthOK(ctx) diff --git a/pkg/util/httputil/client.go b/pkg/util/httputil/client.go index bbeebc3ef200..f1fa79cbc3b2 100644 --- a/pkg/util/httputil/client.go +++ b/pkg/util/httputil/client.go @@ -12,6 +12,8 @@ package httputil import ( "context" + "crypto/tls" + "crypto/x509" "io" "net" "net/http" @@ -32,6 +34,25 @@ func NewClientWithTimeout(timeout time.Duration) *Client { // NewClientWithTimeouts defines a http.Client with the given dialer and client timeouts. func NewClientWithTimeouts(dialerTimeout, clientTimeout time.Duration) *Client { + return NewClientWithTimeoutsCustomCA(dialerTimeout, clientTimeout, "") +} + +// NewClientWithTimeoutsCustomCA defines a http.Client with the given dialer and client timeouts and custom CA pem. +func NewClientWithTimeoutsCustomCA( + dialerTimeout, clientTimeout time.Duration, customCAPem string, +) *Client { + var tlsConf *tls.Config + if customCAPem != "" { + roots, err := x509.SystemCertPool() + if err != nil { + return nil + } + if !roots.AppendCertsFromPEM([]byte(customCAPem)) { + return nil + } + tlsConf = &tls.Config{RootCAs: roots} + } + t := http.DefaultTransport.(*http.Transport) return &Client{&http.Client{ Timeout: clientTimeout, Transport: &http.Transport{ @@ -39,6 +60,15 @@ func NewClientWithTimeouts(dialerTimeout, clientTimeout time.Duration) *Client { // much higher than on linux). DialContext: (&net.Dialer{Timeout: dialerTimeout}).DialContext, DisableKeepAlives: true, + + Proxy: t.Proxy, + MaxIdleConns: t.MaxIdleConns, + IdleConnTimeout: t.IdleConnTimeout, + TLSHandshakeTimeout: t.TLSHandshakeTimeout, + ExpectContinueTimeout: t.ExpectContinueTimeout, + + // Add our custom CA. + TLSClientConfig: tlsConf, }, }} }