Skip to content

Commit c7026b8

Browse files
committed
fix: Improve http.Client usage for security and performance
This change switches us from maintaining a tls config which we then on-demand initialize an `http.Client` with to instead maintain and reuse an `http.Client` instance. This enables us to utilize the connection pooling which occurs within the `http.Transport` to reduce ssl handshakes and thus reduce latency. In addition this change provides us a central place to configure out `http.Client` (`httputil`). Allowing us to easily set configuration options to reduce the security risks of using an unconfigured `http.Client`. Notably timeouts to reduce DoS risks, and control around following redirects to prevent blind SSRF's. This change will provide significant benefit without making API changes. A future PR will update the service and also cleanup some of the API.
1 parent 021f2d4 commit c7026b8

File tree

6 files changed

+106
-29
lines changed

6 files changed

+106
-29
lines changed

sdk/auth/oauth/oauth.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package oauth
22

33
import (
44
"context"
5-
"crypto/tls"
65
"encoding/json"
76
"fmt"
87
"io"
@@ -17,15 +16,16 @@ import (
1716
"github.com/lestrrat-go/jwx/v2/jwk"
1817
"github.com/lestrrat-go/jwx/v2/jws"
1918
"github.com/lestrrat-go/jwx/v2/jwt"
19+
"github.com/opentdf/platform/sdk/httputil"
2020
)
2121

2222
const (
2323
tokenExpirationBuffer = 10 * time.Second
2424
)
2525

2626
type CertExchangeInfo struct {
27-
TLSConfig *tls.Config
28-
Audience []string
27+
HTTPClient *http.Client
28+
Audience []string
2929
}
3030

3131
type ClientCredentials struct {
@@ -146,6 +146,9 @@ func GetAccessToken(client *http.Client, tokenEndpoint string, scopes []string,
146146
if err != nil {
147147
return nil, err
148148
}
149+
if client == nil {
150+
client = httputil.SafeHTTPClient()
151+
}
149152

150153
resp, err := client.Do(req)
151154
if err != nil {
@@ -249,6 +252,9 @@ func DoTokenExchange(ctx context.Context, client *http.Client, tokenEndpoint str
249252
if err != nil {
250253
return nil, err
251254
}
255+
if client == nil {
256+
client = httputil.SafeHTTPClient()
257+
}
252258
resp, err := client.Do(req)
253259
if err != nil {
254260
return nil, fmt.Errorf("error making request to IdP for token exchange: %w", err)
@@ -313,12 +319,11 @@ func DoCertExchange(ctx context.Context, tokenEndpoint string, exchangeInfo Cert
313319
if err != nil {
314320
return nil, err
315321
}
316-
client := &http.Client{
317-
Transport: &http.Transport{
318-
TLSClientConfig: exchangeInfo.TLSConfig,
319-
},
320-
}
321322

323+
client := exchangeInfo.HTTPClient
324+
if client == nil {
325+
client = httputil.SafeHTTPClient()
326+
}
322327
resp, err := client.Do(req)
323328
if err != nil {
324329
return nil, fmt.Errorf("error making request to IdP for certificate exchange: %w", err)

sdk/auth/oauth/oauth_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/lestrrat-go/jwx/v2/jws"
2424
"github.com/lestrrat-go/jwx/v2/jwt"
2525
"github.com/opentdf/platform/lib/fixtures"
26+
"github.com/opentdf/platform/sdk/httputil"
2627
"github.com/stretchr/testify/require"
2728
"github.com/stretchr/testify/suite"
2829
tc "github.com/testcontainers/testcontainers-go"
@@ -78,12 +79,15 @@ func (s *OAuthSuite) TestCertExchangeFromKeycloak() {
7879
rootCAs, _ := x509.SystemCertPool()
7980
rootCAs.AppendCertsFromPEM(ca)
8081
s.Require().NoError(err)
81-
tlsConfig := tls.Config{
82+
tlsConfig := &tls.Config{
8283
MinVersion: tls.VersionTLS12,
8384
Certificates: []tls.Certificate{cert},
8485
RootCAs: rootCAs,
8586
}
86-
exhcangeInfo := CertExchangeInfo{TLSConfig: &tlsConfig, Audience: []string{"opentdf-sdk"}}
87+
exhcangeInfo := CertExchangeInfo{
88+
HTTPClient: httputil.SafeHTTPClientWithTLSConfig(tlsConfig),
89+
Audience: []string{"opentdf-sdk"},
90+
}
8791

8892
tok, err := DoCertExchange(
8993
context.Background(),

sdk/auth/token_adding_interceptor.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/lestrrat-go/jwx/v2/jwk"
1515
"github.com/lestrrat-go/jwx/v2/jws"
1616
"github.com/lestrrat-go/jwx/v2/jwt"
17+
"github.com/opentdf/platform/sdk/httputil"
1718
"google.golang.org/grpc"
1819
"google.golang.org/grpc/codes"
1920
"google.golang.org/grpc/metadata"
@@ -25,15 +26,22 @@ const (
2526
)
2627

2728
func NewTokenAddingInterceptor(t AccessTokenSource, c *tls.Config) TokenAddingInterceptor {
29+
return NewTokenAddingInterceptorWithClient(t, httputil.SafeHTTPClientWithTLSConfig(c))
30+
}
31+
32+
func NewTokenAddingInterceptorWithClient(t AccessTokenSource, c *http.Client) TokenAddingInterceptor {
33+
if c == nil {
34+
c = httputil.SafeHTTPClient()
35+
}
2836
return TokenAddingInterceptor{
2937
tokenSource: t,
30-
tlsConfig: c,
38+
httpClient: c,
3139
}
3240
}
3341

3442
type TokenAddingInterceptor struct {
3543
tokenSource AccessTokenSource
36-
tlsConfig *tls.Config
44+
httpClient *http.Client
3745
}
3846

3947
func (i TokenAddingInterceptor) AddCredentials(
@@ -45,12 +53,7 @@ func (i TokenAddingInterceptor) AddCredentials(
4553
opts ...grpc.CallOption,
4654
) error {
4755
newMetadata := make([]string, 0)
48-
client := &http.Client{
49-
Transport: &http.Transport{
50-
TLSClientConfig: i.tlsConfig,
51-
},
52-
}
53-
accessToken, err := i.tokenSource.AccessToken(ctx, client)
56+
accessToken, err := i.tokenSource.AccessToken(ctx, i.httpClient)
5457
if err == nil {
5558
newMetadata = append(newMetadata, "Authorization", fmt.Sprintf("DPoP %s", accessToken))
5659
} else {

sdk/httputil/http.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package httputil
2+
3+
import (
4+
"crypto/tls"
5+
"net/http"
6+
"time"
7+
)
8+
9+
const (
10+
defaultTimeout = 120 * time.Second
11+
// defaults to match DefaultTransport - defined to satisfy lint
12+
maxIdleConns = 100
13+
idleConnTimeout = 90 * time.Second
14+
tLSHandshakeTimeout = 10 * time.Second
15+
expectContinueTimeout = 1 * time.Second
16+
)
17+
18+
var preventRedirectCheck = func(_ *http.Request, _ []*http.Request) error {
19+
return http.ErrUseLastResponse // Prevent following redirects
20+
}
21+
22+
var safeHTTPClient = &http.Client{
23+
Transport: http.DefaultTransport,
24+
Timeout: defaultTimeout,
25+
CheckRedirect: preventRedirectCheck,
26+
}
27+
28+
// SafeHTTPClient returns a default http client which has sensible timeouts, won't follow redirects, and enables idle
29+
// connection pooling.
30+
func SafeHTTPClient() *http.Client {
31+
return safeHTTPClient
32+
}
33+
34+
// SafeHTTPClientWithTLSConfig returns a http client which has sensible timeouts, won't follow redirects, and if
35+
// specified a http.Transport with the tls.Config provided.
36+
func SafeHTTPClientWithTLSConfig(cfg *tls.Config) *http.Client {
37+
if cfg == nil {
38+
return safeHTTPClient
39+
}
40+
return SafeHTTPClientWithTransport(&http.Transport{
41+
TLSClientConfig: cfg,
42+
// config below matches DefaultTransport
43+
Proxy: http.ProxyFromEnvironment,
44+
ForceAttemptHTTP2: true,
45+
MaxIdleConns: maxIdleConns,
46+
IdleConnTimeout: idleConnTimeout,
47+
TLSHandshakeTimeout: tLSHandshakeTimeout,
48+
ExpectContinueTimeout: expectContinueTimeout,
49+
})
50+
}
51+
52+
// SafeHTTPClientWithTransport returns a http client which has sensible timeouts, won't follow redirects, and if
53+
// specified the provided http.Transport.
54+
func SafeHTTPClientWithTransport(transport *http.Transport) *http.Client {
55+
if transport == nil {
56+
return safeHTTPClient
57+
}
58+
return &http.Client{
59+
Transport: transport,
60+
// config below matches our values for safeHttpClient
61+
Timeout: defaultTimeout,
62+
CheckRedirect: preventRedirectCheck,
63+
}
64+
}

sdk/options.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package sdk
33
import (
44
"crypto/rsa"
55
"crypto/tls"
6+
"net/http"
67

78
"github.com/opentdf/platform/lib/ocrypto"
89
"github.com/opentdf/platform/sdk/auth"
910
"github.com/opentdf/platform/sdk/auth/oauth"
11+
"github.com/opentdf/platform/sdk/httputil"
1012
"golang.org/x/oauth2"
1113
"google.golang.org/grpc"
1214
"google.golang.org/grpc/credentials"
@@ -20,7 +22,7 @@ type config struct {
2022
// Platform configuration structure is subject to change. Consume via accessor methods.
2123
PlatformConfiguration PlatformConfiguration
2224
dialOption grpc.DialOption
23-
tlsConfig *tls.Config
25+
httpClient *http.Client
2426
clientCredentials *oauth.ClientCredentials
2527
tokenExchange *oauth.TokenExchangeInfo
2628
tokenEndpoint string
@@ -66,7 +68,7 @@ func WithInsecureSkipVerifyConn() Option {
6668
}
6769
c.dialOption = grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
6870
// used by http client
69-
c.tlsConfig = tlsConfig
71+
c.httpClient = httputil.SafeHTTPClientWithTLSConfig(tlsConfig)
7072
}
7173
}
7274

@@ -83,7 +85,7 @@ func WithInsecurePlaintextConn() Option {
8385
c.dialOption = grpc.WithTransportCredentials(insecure.NewCredentials())
8486
// used by http client
8587
// FIXME anything to do here
86-
c.tlsConfig = &tls.Config{}
88+
c.httpClient = httputil.SafeHTTPClient()
8789
}
8890
}
8991

@@ -97,7 +99,7 @@ func WithClientCredentials(clientID, clientSecret string, scopes []string) Optio
9799

98100
func WithTLSCredentials(tls *tls.Config, audience []string) Option {
99101
return func(c *config) {
100-
c.certExchange = &oauth.CertExchangeInfo{TLSConfig: tls, Audience: audience}
102+
c.certExchange = &oauth.CertExchangeInfo{HTTPClient: httputil.SafeHTTPClientWithTLSConfig(tls), Audience: audience}
101103
}
102104
}
103105

sdk/sdk.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/opentdf/platform/protocol/go/wellknownconfiguration"
2828
"github.com/opentdf/platform/sdk/audit"
2929
"github.com/opentdf/platform/sdk/auth"
30+
"github.com/opentdf/platform/sdk/httputil"
3031
"github.com/opentdf/platform/sdk/internal/archive"
3132
"github.com/xeipuuv/gojsonschema"
3233
"google.golang.org/grpc"
@@ -154,7 +155,7 @@ func New(platformEndpoint string, opts ...Option) (*SDK, error) {
154155
return nil, err
155156
}
156157
if accessTokenSource != nil {
157-
interceptor := auth.NewTokenAddingInterceptor(accessTokenSource, cfg.tlsConfig)
158+
interceptor := auth.NewTokenAddingInterceptorWithClient(accessTokenSource, cfg.httpClient)
158159
uci = append(uci, interceptor.AddCredentials)
159160
}
160161

@@ -452,13 +453,11 @@ func getTokenEndpoint(c config) (string, error) {
452453
return "", err
453454
}
454455

455-
httpClient := &http.Client{
456-
Transport: &http.Transport{
457-
TLSClientConfig: c.tlsConfig,
458-
},
456+
client := c.httpClient
457+
if client == nil {
458+
client = httputil.SafeHTTPClient()
459459
}
460-
461-
resp, err := httpClient.Do(req)
460+
resp, err := client.Do(req)
462461
if err != nil {
463462
return "", err
464463
}

0 commit comments

Comments
 (0)