Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: Make TLSConfig refreshable #545

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-545.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: Make TLSConfig refreshable
links:
- https://github.com/palantir/conjure-go-runtime/pull/545
11 changes: 5 additions & 6 deletions conjure-go-client/httpclient/client_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package httpclient

import (
"context"
"crypto/tls"
"net/http"
"time"

Expand Down Expand Up @@ -63,7 +62,7 @@ type httpClientBuilder struct {
ServiceNameTag metrics.Tag // Service name is not refreshable.
Timeout refreshable.Duration
DialerParams refreshingclient.RefreshableDialerParams
TLSConfig *tls.Config // TODO: Make this refreshing and wire into transport
TLSConfig refreshable.Refreshable
TransportParams refreshingclient.RefreshableTransportParams
Middlewares []Middleware

Expand Down Expand Up @@ -204,7 +203,7 @@ func newClientBuilder() *clientBuilder {
KeepAlive: defaultKeepAlive,
SocksProxyURL: nil,
})),
TLSConfig: defaultTLSConfig,
TLSConfig: refreshable.NewDefaultRefreshable(defaultTLSConfig),
TransportParams: refreshingclient.NewRefreshingTransportParams(refreshable.NewDefaultRefreshable(refreshingclient.TransportParams{
MaxIdleConns: defaultMaxIdleConns,
MaxIdleConnsPerHost: defaultMaxIdleConnsPerHost,
Expand Down Expand Up @@ -249,11 +248,11 @@ func newClientBuilderFromRefreshableConfig(ctx context.Context, config Refreshab
svc1log.SafeParam("updatedServiceName", s))
})

if tlsConfig, err := subscribeTLSConfigUpdateWarning(ctx, config.Security()); err != nil {
tlsConfig, err := newRefreshableTLSConfig(ctx, config.Security())
if err != nil {
return err
} else if tlsConfig != nil {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the subscription to the security config, this should always return a non-nil refreshable.

b.HTTP.TLSConfig = tlsConfig
}
b.HTTP.TLSConfig = tlsConfig

refreshingParams, err := refreshable.NewMapValidatingRefreshable(config, func(i interface{}) (interface{}, error) {
p, err := newValidatedClientParamsFromConfig(ctx, i.(ClientConfig), isHTTPClient)
Expand Down
20 changes: 16 additions & 4 deletions conjure-go-client/httpclient/client_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,25 +352,37 @@ func WithProxyURL(proxyURLString string) ClientOrHTTPClientParam {
})
}

// WithTLSConfig sets the SSL/TLS configuration for the HTTP client's Transport using a copy of the provided config.
// The palantir/pkg/tlsconfig package is recommended to build a tls.Config from sane defaults.
// WithTLSConfig is the same as WithRefreshableTLSConfig, but provides a static config.
func WithTLSConfig(conf *tls.Config) ClientOrHTTPClientParam {
return clientOrHTTPClientParamFunc(func(b *httpClientBuilder) error {
if conf == nil {
b.TLSConfig = nil
} else {
b.TLSConfig = conf.Clone()
b.TLSConfig = refreshable.NewDefaultRefreshable(conf.Clone())
}
return nil
})
}

// WithRefreshableTLSConfig sets the SSL/TLS configuration for the HTTP client's Transport using a copy of the provided config.
// The palantir/pkg/tlsconfig package is recommended to build a tls.Config from sane defaults.
func WithRefreshableTLSConfig(refreshingTLSConfig refreshable.Refreshable) ClientOrHTTPClientParam {
return clientOrHTTPClientParamFunc(func(b *httpClientBuilder) error {
b.TLSConfig = refreshingTLSConfig
return nil
})
}

// WithTLSInsecureSkipVerify sets the InsecureSkipVerify field for the HTTP client's tls config.
// This option should only be used in clients that have way to establish trust with servers.
func WithTLSInsecureSkipVerify() ClientOrHTTPClientParam {
return clientOrHTTPClientParamFunc(func(b *httpClientBuilder) error {
if b.TLSConfig != nil {
b.TLSConfig.InsecureSkipVerify = true
b.TLSConfig = b.TLSConfig.Map(func(confI interface{}) interface{} {
conf := confI.(*tls.Config).Clone()
conf.InsecureSkipVerify = true
return conf
})
}
return nil
})
Expand Down
54 changes: 28 additions & 26 deletions conjure-go-client/httpclient/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/palantir/conjure-go-runtime/v2/conjure-go-client/httpclient/internal/refreshingclient"
"github.com/palantir/pkg/metrics"
"github.com/palantir/pkg/refreshable"
"github.com/palantir/pkg/tlsconfig"
werror "github.com/palantir/witchcraft-go-error"
"github.com/palantir/witchcraft-go-logging/wlog/svclog/svc1log"
Expand Down Expand Up @@ -475,28 +476,32 @@ func newValidatedClientParamsFromConfig(ctx context.Context, config ClientConfig
}, nil
}

func subscribeTLSConfigUpdateWarning(ctx context.Context, security RefreshableSecurityConfig) (*tls.Config, error) {
//TODO: Implement refreshable TLS configuration.
// It is hard to represent all of the configuration (e.g. a dynamic function for GetCertificate) in primitive values friendly to reflect.DeepEqual.
func newRefreshableTLSConfig(ctx context.Context, security RefreshableSecurityConfig) (refreshable.Refreshable, error) {
currentSecurity := security.CurrentSecurityConfig()

security.CAFiles().SubscribeToStringSlice(func(caFiles []string) {
svc1log.FromContext(ctx).Warn("conjure-go-runtime: CAFiles configuration changed but can not be live-reloaded.",
svc1log.SafeParam("existingCAFiles", currentSecurity.CAFiles),
svc1log.SafeParam("ignoredCAFiles", caFiles))
})
security.CertFile().SubscribeToString(func(certFile string) {
svc1log.FromContext(ctx).Warn("conjure-go-runtime: CertFile configuration changed but can not be live-reloaded.",
svc1log.SafeParam("existingCertFile", currentSecurity.CertFile),
svc1log.SafeParam("ignoredCertFile", certFile))
})
security.KeyFile().SubscribeToString(func(keyFile string) {
svc1log.FromContext(ctx).Warn("conjure-go-runtime: KeyFile configuration changed but can not be live-reloaded.",
svc1log.SafeParam("existingKeyFile", currentSecurity.KeyFile),
svc1log.SafeParam("ignoredKeyFile", keyFile))
})
currentTLSConfig, err := newTLSConfig(currentSecurity)
if err != nil {
return nil, err
}
tlsConfig := refreshable.NewDefaultRefreshable(currentTLSConfig)

return newTLSConfig(currentSecurity)
security.SubscribeToSecurityConfig(func(currentSecurity SecurityConfig) {
currentTLSConfig, err := newTLSConfig(currentSecurity)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care that this does not handle changing file contents, only file paths?

if err != nil {
svc1log.FromContext(ctx).Warn("conjure-go-runtime: Current security config is not valid; tls config not reloaded.",
svc1log.SafeParam("existingCertFile", currentSecurity.CertFile),
svc1log.SafeParam("existingKeyFile", currentSecurity.KeyFile),
svc1log.SafeParam("existingCAFiles", currentSecurity.CAFiles),
svc1log.Stacktrace(err))
return
}
if err := tlsConfig.Update(currentTLSConfig); err != nil {
svc1log.FromContext(ctx).Warn("conjure-go-runtime: Internal error updating tls config.",
svc1log.Stacktrace(err))
return
}
})
return tlsConfig, nil
}

func newTLSConfig(security SecurityConfig) (*tls.Config, error) {
Expand All @@ -507,14 +512,11 @@ func newTLSConfig(security SecurityConfig) (*tls.Config, error) {
if security.CertFile != "" && security.KeyFile != "" {
tlsParams = append(tlsParams, tlsconfig.ClientKeyPairFiles(security.CertFile, security.KeyFile))
}
if len(tlsParams) != 0 {
tlsConfig, err := tlsconfig.NewClientConfig(tlsParams...)
if err != nil {
return nil, werror.Wrap(err, "failed to build tlsConfig")
}
return tlsConfig, nil
tlsConfig, err := tlsconfig.NewClientConfig(tlsParams...)
if err != nil {
return nil, werror.Wrap(err, "failed to build tlsConfig")
}
return nil, nil
return tlsConfig, nil
}

func derefDurationPtr(durPtr *time.Duration, defaultVal time.Duration) time.Duration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,33 @@ type TransportParams struct {
HTTP2PingTimeout time.Duration
}

func NewRefreshableTransport(ctx context.Context, p RefreshableTransportParams, tlsConfig *tls.Config, dialer ContextDialer) http.RoundTripper {
func NewRefreshableTransport(ctx context.Context, p RefreshableTransportParams, tlsConfig refreshable.Refreshable, dialer ContextDialer) http.RoundTripper {
curTLSConfig := func() *tls.Config {
if tlsConfig == nil {
return nil
}
return tlsConfig.Current().(*tls.Config)
}

transport := refreshable.NewDefaultRefreshable(newTransport(ctx, p.CurrentTransportParams(), curTLSConfig(), dialer))

p.SubscribeToTransportParams(func(params TransportParams) {
if err := transport.Update(newTransport(ctx, params, curTLSConfig(), dialer)); err != nil {
svc1log.FromContext(ctx).Error("Failed to update HTTP transport with transport params", svc1log.Stacktrace(err))
}
})

if tlsConfig != nil {
tlsConfig.Subscribe(func(tlsConfI interface{}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general concern I had with the original implementation was this subscription because *tls.Config is not compatible with reflect.DeepEqual (it contains closures in struct fields) so other changes could cause us to rebuild our transport (and lose shared connections)

tlsConf := tlsConfI.(*tls.Config)
if err := transport.Update(newTransport(ctx, p.CurrentTransportParams(), tlsConf, dialer)); err != nil {
svc1log.FromContext(ctx).Error("Failed to update HTTP transport with tls config", svc1log.Stacktrace(err))
}
})
}

return &RefreshableTransport{
Refreshable: p.MapTransportParams(func(p TransportParams) interface{} {
return newTransport(ctx, p, tlsConfig, dialer)
}),
Refreshable: transport,
}
}

Expand Down