diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 48c5ec8013..5f1f27e7cf 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -1,6 +1,7 @@ package dynamic import ( + gtls "crypto/tls" "reflect" "time" @@ -208,6 +209,8 @@ type ServersTransport struct { Certificates tls.Certificates `description:"Certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty"` MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"` ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,omitempty" export:"true"` + // TODO: Verify with traefik team if there is a better way to expose this + VerifyConnection func(*gtls.Config, gtls.ConnectionState) error `json:"-"` } // +k8s:deepcopy-gen=true diff --git a/pkg/provider/consulcatalog/config.go b/pkg/provider/consulcatalog/config.go index fb20dbd858..6452f713ec 100644 --- a/pkg/provider/consulcatalog/config.go +++ b/pkg/provider/consulcatalog/config.go @@ -2,8 +2,13 @@ package consulcatalog import ( "context" + gtls "crypto/tls" + "crypto/x509" "errors" "fmt" + "net" + "time" + "github.com/hashicorp/consul/api" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/label" @@ -11,7 +16,6 @@ import ( "github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/provider/constraints" "github.com/traefik/traefik/v2/pkg/tls" - "net" ) func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, certInfo *connectCert) *dynamic.Configuration { @@ -67,7 +71,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer confFromLabel.HTTP.ServersTransports = make(map[string]*dynamic.ServersTransport) } - if certInfo != nil { + if item.ExtraConf.ConnectEnabled { confFromLabel.HTTP.ServersTransports[connectTransportName(item.Name)] = certInfo.serverTransport(connectTransportName(item.Name)) } @@ -120,11 +124,29 @@ func (c *connectCert) getLeaf() tls.Certificate { func (c *connectCert) serverTransport(sname string) *dynamic.ServersTransport { return &dynamic.ServersTransport{ - ServerName: sname, - RootCAs: c.getRoot(), + ServerName: sname, + InsecureSkipVerify: true, + RootCAs: c.getRoot(), Certificates: tls.Certificates{ c.getLeaf(), }, + VerifyConnection: func(cfg *gtls.Config, cs gtls.ConnectionState) error { + t := cfg.Time + if t == nil { + t = time.Now + } + // This is basically what Go itself does sans the hostname validation + opts := x509.VerifyOptions{ + Roots: cfg.RootCAs, + CurrentTime: t(), + Intermediates: x509.NewCertPool(), + } + for _, cert := range cs.PeerCertificates[1:] { + opts.Intermediates.AddCert(cert) + } + _, err := cs.PeerCertificates[0].Verify(opts) + return err + }, } } @@ -310,12 +332,13 @@ func (p *Provider) addServer(ctx context.Context, item itemData, loadBalancer *d return errors.New("address is missing") } - loadBalancer.Servers[0].URL = fmt.Sprintf("%s://%s", loadBalancer.Servers[0].Scheme, net.JoinHostPort(item.Address, port)) - loadBalancer.Servers[0].Scheme = "" - if item.ExtraConf.ConnectEnabled { loadBalancer.ServersTransport = connectTransportName(item.Name) + loadBalancer.Servers[0].Scheme = "https" } + loadBalancer.Servers[0].URL = fmt.Sprintf("%s://%s", loadBalancer.Servers[0].Scheme, net.JoinHostPort(item.Address, port)) + loadBalancer.Servers[0].Scheme = "" + return nil } diff --git a/pkg/provider/consulcatalog/consul_catalog.go b/pkg/provider/consulcatalog/consul_catalog.go index e3fd39b364..26b050ad2f 100644 --- a/pkg/provider/consulcatalog/consul_catalog.go +++ b/pkg/provider/consulcatalog/consul_catalog.go @@ -3,15 +3,16 @@ package consulcatalog import ( "context" "fmt" - "github.com/google/uuid" - "github.com/hashicorp/consul/api/watch" - "github.com/hashicorp/go-hclog" - "github.com/sirupsen/logrus" "strconv" "strings" "text/template" "time" + "github.com/google/uuid" + "github.com/hashicorp/consul/api/watch" + "github.com/hashicorp/go-hclog" + "github.com/sirupsen/logrus" + "github.com/cenkalti/backoff/v4" "github.com/hashicorp/consul/api" ptypes "github.com/traefik/paerser/types" @@ -54,8 +55,7 @@ type Provider struct { Cache bool `description:"Use local agent caching for catalog reads." json:"cache,omitempty" toml:"cache,omitempty" yaml:"cache,omitempty" export:"true"` ExposedByDefault bool `description:"Expose containers by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` - ConnectAware bool `description:"Enable Consul Connect support." json:"connectAware,omitEmpty" toml:"connectAware,omitempty" yaml:"connectAware,omitEmpty"` - ConnectNative bool `description:"Register and manage traefik in Consul Catalog as a Connect Native service." json:"connectNative,omitempty" toml:"connectNative,omitempty" yaml:"connectNative,omitempty"` + Connect *ConnectConfig `description:"Consul Connect settings." json:"connectAware,omitEmpty" toml:"connectAware,omitempty" yaml:"connectAware,omitEmpty"` ServiceName string `description:"Name of the traefik service in Consul Catalog." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty"` ServicePort int `description:"Port of the traefik service to register in Consul Catalog" json:"servicePort,omitempty" toml:"servicePort,omitempty" yaml:"servicePort,omitempty"` @@ -64,6 +64,13 @@ type Provider struct { certChan chan *connectCert } +// ConnectConfig holds the configuration of the settings used to communicate through Consul Connect +type ConnectConfig struct { + ConnectAware bool `description:"Enable Consul Connect support." json:"connectAware,omitEmpty" toml:"connectAware,omitempty" yaml:"connectAware,omitEmpty"` + // NOTE: Unless the certificates are issued by a trusted authority, this needs to be set as true + InsecureSkipVerify bool `description:"Skip verifying certificates for communicating with Consul Connect services." json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` +} + // EndpointConfig holds configurations of the endpoint. type EndpointConfig struct { Address string `description:"The address of the Consul server" json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty" export:"true"` @@ -80,6 +87,12 @@ func (c *EndpointConfig) SetDefaults() { c.Address = "http://127.0.0.1:8500" } +// SetDefaults set the default values when using Consul Connect +func (c *ConnectConfig) SetDefaults() { + c.ConnectAware = false + c.InsecureSkipVerify = true +} + // EndpointHTTPAuthConfig holds configurations of the authentication. type EndpointHTTPAuthConfig struct { Username string `description:"Basic Auth username" json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" export:"true"` @@ -90,11 +103,14 @@ type EndpointHTTPAuthConfig struct { func (p *Provider) SetDefaults() { endpoint := &EndpointConfig{} endpoint.SetDefaults() + connect := &ConnectConfig{} + connect.SetDefaults() p.Endpoint = endpoint p.RefreshInterval = ptypes.Duration(15 * time.Second) p.Prefix = "traefik" p.ExposedByDefault = true p.DefaultRule = DefaultTemplateRule + p.Connect = connect p.certChan = make(chan *connectCert) } @@ -111,9 +127,9 @@ func (p *Provider) Init() error { // Provide allows the consul catalog provider to provide configurations to traefik using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { - if p.ConnectAware { + if p.Connect.ConnectAware { pool.GoCtx(p.registerConnectService) - pool.GoCtx(p.watchConnectTls) + pool.GoCtx(p.watchConnectTLS) } pool.GoCtx(func(routineCtx context.Context) { @@ -138,7 +154,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. // the service watcher, otherwise a connect enabled service // that gets resolved before the certificates are available // will cause an error condition. - if p.ConnectAware { + if p.Connect.ConnectAware { certInfo = <-p.certChan } @@ -301,7 +317,7 @@ func contains(values []string, val string) bool { } func (p *Provider) registerConnectService(ctx context.Context) { - if !p.ConnectNative { + if !p.Connect.ConnectAware { return } @@ -318,10 +334,10 @@ func (p *Provider) registerConnectService(ctx context.Context) { return } - serviceId := uuid.New().String() + serviceID := uuid.New().String() operation := func() error { regReq := &api.AgentServiceRegistration{ - ID: serviceId, + ID: serviceID, Kind: api.ServiceKindTypical, Name: p.ServiceName, Port: p.ServicePort, @@ -349,7 +365,7 @@ func (p *Provider) registerConnectService(ctx context.Context) { } <-ctx.Done() - err = client.Agent().ServiceDeregister(serviceId) + err = client.Agent().ServiceDeregister(serviceID) if err != nil { logger.WithError(err).Error("failed to deregister traefik from consul catalog") } @@ -400,7 +416,7 @@ func leafWatcherHandler(logger log.Logger, dest chan<- keyPair) func(watch.Block } } -func (p *Provider) watchConnectTls(ctx context.Context) { +func (p *Provider) watchConnectTLS(ctx context.Context) { ctxLog := log.With(ctx, log.Str(log.ProviderName, "consulcatalog")) logger := log.FromContext(ctxLog) diff --git a/pkg/server/service/roundtripper.go b/pkg/server/service/roundtripper.go index f45568fd47..8849235b4e 100644 --- a/pkg/server/service/roundtripper.go +++ b/pkg/server/service/roundtripper.go @@ -146,6 +146,11 @@ func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error RootCAs: createRootCACertPool(cfg.RootCAs), Certificates: cfg.Certificates.GetCertificates(), } + if cfg.VerifyConnection != nil { + transport.TLSClientConfig.VerifyConnection = func(cs tls.ConnectionState) error { + return cfg.VerifyConnection(transport.TLSClientConfig, cs) + } + } } return newSmartRoundTripper(transport)