traefik/pkg/provider/hub/hub.go

213 lines
6.4 KiB
Go
Raw Normal View History

package hub
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/safe"
ttls "github.com/traefik/traefik/v2/pkg/tls"
)
var _ provider.Provider = (*Provider)(nil)
// Entrypoints created for Hub.
const (
APIEntrypoint = "traefikhub-api"
TunnelEntrypoint = "traefikhub-tunl"
)
// Provider holds configurations of the provider.
type Provider struct {
TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
server *http.Server
}
// TLS configures the mTLS connection between Traefik Proxy and the Traefik Hub Agent.
type TLS struct {
Insecure bool `description:"Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
CA ttls.FileOrContent `description:"The certificate authority authenticates the Traefik Hub Agent certificate." json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty" loggable:"false"`
Cert ttls.FileOrContent `description:"The TLS certificate for Traefik Proxy as a TLS client." json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty" loggable:"false"`
Key ttls.FileOrContent `description:"The TLS key for Traefik Proxy as a TLS client." json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"`
}
// Init the provider.
func (p *Provider) Init() error {
return nil
}
// Provide allows the hub provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return fmt.Errorf("listener: %w", err)
}
port := listener.Addr().(*net.TCPAddr).Port
client, err := createAgentClient(p.TLS)
if err != nil {
return fmt.Errorf("creating Hub Agent HTTP client: %w", err)
}
p.server = &http.Server{Handler: newHandler(APIEntrypoint, port, configurationChan, p.TLS, client)}
// TODO: this is going to be leaky (because no context to make it terminate)
// if/when Provide lifecycle differs with Traefik lifecycle.
go func() {
if err = p.server.Serve(listener); err != nil {
log.WithoutContext().WithField(log.ProviderName, "hub").Errorf("Unexpected error while running server: %v", err)
return
}
}()
exposeAPIAndMetrics(configurationChan, APIEntrypoint, port, p.TLS)
return nil
}
func exposeAPIAndMetrics(cfgChan chan<- dynamic.Message, ep string, port int, tlsCfg *TLS) {
cfg := emptyDynamicConfiguration()
patchDynamicConfiguration(cfg, ep, port, tlsCfg)
cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}
}
func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int, tlsCfg *TLS) {
cfg.HTTP.Routers["traefik-hub-agent-api"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "api@internal",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/api`)",
}
cfg.HTTP.Routers["traefik-hub-agent-metrics"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "prometheus@internal",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/metrics`)",
}
cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{
EntryPoints: []string{ep},
Service: "traefik-hub-agent-service",
Rule: "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)",
}
cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: fmt.Sprintf("http://127.0.0.1:%d", port),
},
},
},
}
if tlsCfg == nil {
return
}
if tlsCfg.Insecure {
cfg.TLS.Options["traefik-hub"] = ttls.Options{
MinVersion: "VersionTLS13",
}
return
}
cfg.TLS.Options["traefik-hub"] = ttls.Options{
ClientAuth: ttls.ClientAuth{
CAFiles: []ttls.FileOrContent{tlsCfg.CA},
ClientAuthType: "RequireAndVerifyClientCert",
},
SniStrict: true,
MinVersion: "VersionTLS13",
}
cfg.TLS.Certificates = append(cfg.TLS.Certificates, &ttls.CertAndStores{
Certificate: ttls.Certificate{
CertFile: tlsCfg.Cert,
KeyFile: tlsCfg.Key,
},
})
}
func emptyDynamicConfiguration() *dynamic.Configuration {
return &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]ttls.Store),
Options: make(map[string]ttls.Options),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
}
}
func createAgentClient(tlsCfg *TLS) (http.Client, error) {
var client http.Client
if tlsCfg.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
},
}
return client, nil
}
caContent, err := tlsCfg.CA.Read()
if err != nil {
return client, fmt.Errorf("reading CA: %w", err)
}
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(caContent); !ok {
return client, errors.New("appending CA error")
}
certContent, err := tlsCfg.Cert.Read()
if err != nil {
return client, fmt.Errorf("reading Cert: %w", err)
}
keyContent, err := tlsCfg.Key.Read()
if err != nil {
return client, fmt.Errorf("reading Key: %w", err)
}
certificate, err := tls.X509KeyPair(certContent, keyContent)
if err != nil {
return client, fmt.Errorf("creating key pair: %w", err)
}
// mTLS
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: roots,
Certificates: []tls.Certificate{certificate},
ServerName: "agent.traefik",
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
},
}
return client, nil
}