Skip to content

use TLS to communicate with Consul Connect services #1

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

Merged
Merged
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
3 changes: 3 additions & 0 deletions pkg/config/dynamic/http_config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dynamic

import (
gtls "crypto/tls"
"reflect"
"time"

Expand Down Expand Up @@ -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
Expand Down
37 changes: 30 additions & 7 deletions pkg/provider/consulcatalog/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ 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"
"github.com/traefik/traefik/v2/pkg/log"
"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 {
Expand Down Expand Up @@ -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))
}

Expand Down Expand Up @@ -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
},
}
}

Expand Down Expand Up @@ -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
}
44 changes: 30 additions & 14 deletions pkg/provider/consulcatalog/consul_catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`

Expand All @@ -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"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there no way to teach traefik to trust the Connect CA?

}

// 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"`
Expand All @@ -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"`
Expand All @@ -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)
}

Expand All @@ -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) {
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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,
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions pkg/server/service/roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down