From 05333b9579a0a1d3e0b20c108cede064e03c70e3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 29 Oct 2020 15:40:04 +0100 Subject: [PATCH] acme: new HTTP and TLS challenges implementations. --- cmd/traefik/traefik.go | 55 +++++---- pkg/provider/acme/challenge_http.go | 140 +++++++++++++++------- pkg/provider/acme/challenge_tls.go | 161 +++++++++++++++++++++----- pkg/provider/acme/local_store.go | 110 ------------------ pkg/provider/acme/provider.go | 13 ++- pkg/provider/acme/store.go | 17 --- pkg/provider/traefik/internal.go | 28 +++++ pkg/server/aggregator.go | 19 ++- pkg/server/aggregator_test.go | 50 ++++++++ pkg/server/routerfactory_test.go | 6 +- pkg/server/service/internalhandler.go | 17 +-- pkg/server/service/managerfactory.go | 12 +- pkg/tls/tlsmanager.go | 24 ++-- 13 files changed, 398 insertions(+), 254 deletions(-) diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 66321ead0..e41d89d2d 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -13,6 +13,7 @@ import ( "github.com/coreos/go-systemd/daemon" assetfs "github.com/elazarl/go-bindata-assetfs" + "github.com/go-acme/lego/v4/challenge" "github.com/sirupsen/logrus" "github.com/traefik/paerser/cli" "github.com/traefik/traefik/v2/autogen/genstatic" @@ -181,7 +182,16 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err tlsManager := traefiktls.NewManager() - acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager) + httpChallengeProvider := acme.NewChallengeHTTP() + + tlsChallengeProvider := acme.NewChallengeTLSALPN(time.Duration(staticConfiguration.Providers.ProvidersThrottleDuration)) + + err = providerAggregator.AddProvider(tlsChallengeProvider) + if err != nil { + return nil, err + } + + acmeProviders := initACMEProvider(staticConfiguration, &providerAggregator, tlsManager, httpChallengeProvider, tlsChallengeProvider) serverEntryPointsTCP, err := server.NewTCPEntryPoints(staticConfiguration.EntryPoints) if err != nil { @@ -214,7 +224,16 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err accessLog := setupAccessLog(staticConfiguration.AccessLog) chainBuilder := middleware.NewChainBuilder(*staticConfiguration, metricsRegistry, accessLog) roundTripperManager := service.NewRoundTripperManager() - managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry, roundTripperManager) + + var acmeHTTPHandler http.Handler + for _, p := range acmeProviders { + if p != nil && p.HTTPChallenge != nil { + acmeHTTPHandler = httpChallengeProvider + break + } + } + + managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry, roundTripperManager, acmeHTTPHandler) client, plgs, devPlugin, err := initPlugins(staticConfiguration) if err != nil { @@ -264,7 +283,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err roundTripperManager.Update(conf.HTTP.ServersTransports) }) - watcher.AddListener(switchRouter(routerFactory, acmeProviders, serverEntryPointsTCP, serverEntryPointsUDP, aviator)) + watcher.AddListener(switchRouter(routerFactory, serverEntryPointsTCP, serverEntryPointsUDP, aviator)) watcher.AddListener(func(conf dynamic.Configuration) { if metricsRegistry.IsEpEnabled() || metricsRegistry.IsSvcEnabled() { @@ -277,6 +296,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err } }) + watcher.AddListener(tlsChallengeProvider.ListenConfiguration) + resolverNames := map[string]struct{}{} for _, p := range acmeProviders { resolverNames[p.ResolverName] = struct{}{} @@ -298,21 +319,12 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err return server.NewServer(routinesPool, serverEntryPointsTCP, serverEntryPointsUDP, watcher, chainBuilder, accessLog), nil } -func switchRouter(routerFactory *server.RouterFactory, acmeProviders []*acme.Provider, serverEntryPointsTCP server.TCPEntryPoints, serverEntryPointsUDP server.UDPEntryPoints, aviator *pilot.Pilot) func(conf dynamic.Configuration) { +func switchRouter(routerFactory *server.RouterFactory, serverEntryPointsTCP server.TCPEntryPoints, serverEntryPointsUDP server.UDPEntryPoints, aviator *pilot.Pilot) func(conf dynamic.Configuration) { return func(conf dynamic.Configuration) { rtConf := runtime.NewConfig(conf) routers, udpRouters := routerFactory.CreateRouters(rtConf) - for entryPointName, rt := range routers { - for _, p := range acmeProviders { - if p != nil && p.HTTPChallenge != nil && p.HTTPChallenge.EntryPoint == entryPointName { - rt.HTTPHandler(p.CreateHandler(rt.GetHTTPHandler())) - break - } - } - } - if aviator != nil { aviator.SetRuntimeConfiguration(rtConf) } @@ -323,8 +335,7 @@ func switchRouter(routerFactory *server.RouterFactory, acmeProviders []*acme.Pro } // initACMEProvider creates an acme provider from the ACME part of globalConfiguration. -func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager) []*acme.Provider { - challengeStore := acme.NewLocalChallengeStore() +func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager, httpChallengeProvider, tlsChallengeProvider challenge.Provider) []*acme.Provider { localStores := map[string]*acme.LocalStore{} var resolvers []*acme.Provider @@ -335,10 +346,11 @@ func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.Pr } p := &acme.Provider{ - Configuration: resolver.ACME, - Store: localStores[resolver.ACME.Storage], - ChallengeStore: challengeStore, - ResolverName: name, + Configuration: resolver.ACME, + Store: localStores[resolver.ACME.Storage], + ResolverName: name, + HTTPChallengeProvider: httpChallengeProvider, + TLSChallengeProvider: tlsChallengeProvider, } if err := providerAggregator.AddProvider(p); err != nil { @@ -348,15 +360,12 @@ func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.Pr p.SetTLSManager(tlsManager) - if p.TLSChallenge != nil { - tlsManager.TLSAlpnGetter = p.GetTLSALPNCertificate - } - p.SetConfigListenerChan(make(chan dynamic.Configuration)) resolvers = append(resolvers, p) } } + return resolvers } diff --git a/pkg/provider/acme/challenge_http.go b/pkg/provider/acme/challenge_http.go index 2dbd7d9f5..4a8cf1938 100644 --- a/pkg/provider/acme/challenge_http.go +++ b/pkg/provider/acme/challenge_http.go @@ -2,85 +2,126 @@ package acme import ( "context" + "errors" + "fmt" "net" "net/http" + "net/url" + "regexp" + "sync" "time" "github.com/cenkalti/backoff/v4" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/http01" - "github.com/gorilla/mux" "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/safe" ) -var _ challenge.ProviderTimeout = (*challengeHTTP)(nil) +// ChallengeHTTP HTTP challenge provider implements challenge.Provider. +type ChallengeHTTP struct { + httpChallenges map[string]map[string][]byte + lock sync.RWMutex +} -type challengeHTTP struct { - Store ChallengeStore +// NewChallengeHTTP creates a new ChallengeHTTP. +func NewChallengeHTTP() *ChallengeHTTP { + return &ChallengeHTTP{ + httpChallenges: make(map[string]map[string][]byte), + } } // Present presents a challenge to obtain new ACME certificate. -func (c *challengeHTTP) Present(domain, token, keyAuth string) error { - return c.Store.SetHTTPChallengeToken(token, domain, []byte(keyAuth)) +func (c *ChallengeHTTP) Present(domain, token, keyAuth string) error { + c.lock.Lock() + defer c.lock.Unlock() + + if _, ok := c.httpChallenges[token]; !ok { + c.httpChallenges[token] = map[string][]byte{} + } + + c.httpChallenges[token][domain] = []byte(keyAuth) + + return nil } // CleanUp cleans the challenges when certificate is obtained. -func (c *challengeHTTP) CleanUp(domain, token, keyAuth string) error { - return c.Store.RemoveHTTPChallengeToken(token, domain) +func (c *ChallengeHTTP) CleanUp(domain, token, _ string) error { + c.lock.Lock() + defer c.lock.Unlock() + + if c.httpChallenges == nil && len(c.httpChallenges) == 0 { + return nil + } + + if _, ok := c.httpChallenges[token]; ok { + delete(c.httpChallenges[token], domain) + + if len(c.httpChallenges[token]) == 0 { + delete(c.httpChallenges, token) + } + } + + return nil } // Timeout calculates the maximum of time allowed to resolved an ACME challenge. -func (c *challengeHTTP) Timeout() (timeout, interval time.Duration) { +func (c *ChallengeHTTP) Timeout() (timeout, interval time.Duration) { return 60 * time.Second, 5 * time.Second } -// CreateHandler creates a HTTP handler to expose the token for the HTTP challenge. -func (p *Provider) CreateHandler(notFoundHandler http.Handler) http.Handler { - router := mux.NewRouter().SkipClean(true) - router.NotFoundHandler = notFoundHandler +func (c *ChallengeHTTP) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + ctx := log.With(req.Context(), log.Str(log.ProviderName, "acme")) + logger := log.FromContext(ctx) - router.Methods(http.MethodGet). - Path(http01.ChallengePath("{token}")). - Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) + token, err := getPathParam(req.URL) + if err != nil { + logger.Errorf("Unable to get token: %v.", err) + rw.WriteHeader(http.StatusNotFound) + return + } - ctx := log.With(context.Background(), log.Str(log.ProviderName, p.ResolverName+".acme")) - logger := log.FromContext(ctx) + if token != "" { + domain, _, err := net.SplitHostPort(req.Host) + if err != nil { + logger.Debugf("Unable to split host and port: %v. Fallback to request host.", err) + domain = req.Host + } - if token, ok := vars["token"]; ok { - domain, _, err := net.SplitHostPort(req.Host) - if err != nil { - logger.Debugf("Unable to split host and port: %v. Fallback to request host.", err) - domain = req.Host - } - - tokenValue := getTokenValue(ctx, token, domain, p.ChallengeStore) - if len(tokenValue) > 0 { - rw.WriteHeader(http.StatusOK) - _, err = rw.Write(tokenValue) - if err != nil { - logger.Errorf("Unable to write token: %v", err) - } - return - } + tokenValue := c.getTokenValue(ctx, token, domain) + if len(tokenValue) > 0 { + rw.WriteHeader(http.StatusOK) + _, err = rw.Write(tokenValue) + if err != nil { + logger.Errorf("Unable to write token: %v", err) } - rw.WriteHeader(http.StatusNotFound) - })) + return + } + } - return router + rw.WriteHeader(http.StatusNotFound) } -func getTokenValue(ctx context.Context, token, domain string, store ChallengeStore) []byte { +func (c *ChallengeHTTP) getTokenValue(ctx context.Context, token, domain string) []byte { logger := log.FromContext(ctx) - logger.Debugf("Retrieving the ACME challenge for token %v...", token) + logger.Debugf("Retrieving the ACME challenge for token %s...", token) var result []byte operation := func() error { - var err error - result, err = store.GetHTTPChallengeToken(token, domain) - return err + c.lock.RLock() + defer c.lock.RUnlock() + + if _, ok := c.httpChallenges[token]; !ok { + return fmt.Errorf("cannot find challenge for token %s", token) + } + + var ok bool + result, ok = c.httpChallenges[token][domain] + if !ok { + return fmt.Errorf("cannot find challenge for domain %s", domain) + } + + return nil } notify := func(err error, time time.Duration) { @@ -97,3 +138,14 @@ func getTokenValue(ctx context.Context, token, domain string, store ChallengeSto return result } + +func getPathParam(uri *url.URL) (string, error) { + exp := regexp.MustCompile(fmt.Sprintf(`^%s([^/]+)/?$`, http01.ChallengePath(""))) + parts := exp.FindStringSubmatch(uri.Path) + + if len(parts) != 2 { + return "", errors.New("missing token") + } + + return parts[1], nil +} diff --git a/pkg/provider/acme/challenge_tls.go b/pkg/provider/acme/challenge_tls.go index c5b1f41d2..114e6b749 100644 --- a/pkg/provider/acme/challenge_tls.go +++ b/pkg/provider/acme/challenge_tls.go @@ -1,22 +1,45 @@ package acme import ( - "crypto/tls" + "fmt" + "sync" + "time" - "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/tlsalpn01" + "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" + "github.com/traefik/traefik/v2/pkg/safe" + traefiktls "github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/types" ) -var _ challenge.Provider = (*challengeTLSALPN)(nil) +const providerNameALPN = "tlsalpn.acme" -type challengeTLSALPN struct { - Store ChallengeStore +// ChallengeTLSALPN TLSALPN challenge provider implements challenge.Provider. +type ChallengeTLSALPN struct { + Timeout time.Duration + + chans map[string]chan struct{} + muChans sync.Mutex + + certs map[string]*Certificate + muCerts sync.Mutex + + configurationChan chan<- dynamic.Message } -func (c *challengeTLSALPN) Present(domain, token, keyAuth string) error { - log.WithoutContext().WithField(log.ProviderName, "acme"). +// NewChallengeTLSALPN creates a new ChallengeTLSALPN. +func NewChallengeTLSALPN(timeout time.Duration) *ChallengeTLSALPN { + return &ChallengeTLSALPN{ + Timeout: timeout, + chans: make(map[string]chan struct{}), + certs: make(map[string]*Certificate), + } +} + +// Present presents a challenge to obtain new ACME certificate. +func (c *ChallengeTLSALPN) Present(domain, _, keyAuth string) error { + log.WithoutContext().WithField(log.ProviderName, providerNameALPN). Debugf("TLS Challenge Present temp certificate for %s", domain) certPEMBlock, keyPEMBlock, err := tlsalpn01.ChallengeBlocks(domain, keyAuth) @@ -25,31 +48,113 @@ func (c *challengeTLSALPN) Present(domain, token, keyAuth string) error { } cert := &Certificate{Certificate: certPEMBlock, Key: keyPEMBlock, Domain: types.Domain{Main: "TEMP-" + domain}} - return c.Store.AddTLSChallenge(domain, cert) + + c.muChans.Lock() + ch := make(chan struct{}) + c.chans[string(certPEMBlock)] = ch + c.muChans.Unlock() + + c.muCerts.Lock() + c.certs[keyAuth] = cert + conf := createMessage(c.certs) + c.muCerts.Unlock() + + c.configurationChan <- conf + + timer := time.NewTimer(c.Timeout) + + var errC error + select { + case t := <-timer.C: + timer.Stop() + close(c.chans[string(certPEMBlock)]) + errC = fmt.Errorf("timeout %s", t) + case <-ch: + // noop + } + + c.muChans.Lock() + delete(c.chans, string(certPEMBlock)) + c.muChans.Unlock() + + return errC } -func (c *challengeTLSALPN) CleanUp(domain, token, keyAuth string) error { - log.WithoutContext().WithField(log.ProviderName, "acme"). +// CleanUp cleans the challenges when certificate is obtained. +func (c *ChallengeTLSALPN) CleanUp(domain, _, keyAuth string) error { + log.WithoutContext().WithField(log.ProviderName, providerNameALPN). Debugf("TLS Challenge CleanUp temp certificate for %s", domain) - return c.Store.RemoveTLSChallenge(domain) + c.muCerts.Lock() + delete(c.certs, keyAuth) + conf := createMessage(c.certs) + c.muCerts.Unlock() + + c.configurationChan <- conf + + return nil } -// GetTLSALPNCertificate Get the temp certificate for ACME TLS-ALPN-O1 challenge. -func (p *Provider) GetTLSALPNCertificate(domain string) (*tls.Certificate, error) { - cert, err := p.ChallengeStore.GetTLSChallenge(domain) - if err != nil { - return nil, err - } - - if cert == nil { - return nil, nil - } - - certificate, err := tls.X509KeyPair(cert.Certificate, cert.Key) - if err != nil { - return nil, err - } - - return &certificate, nil +// Init the provider. +func (c *ChallengeTLSALPN) Init() error { + return nil +} + +// Provide allows the provider to provide configurations to traefik using the given configuration channel. +func (c *ChallengeTLSALPN) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error { + c.configurationChan = configurationChan + + return nil +} + +// ListenConfiguration sets a new Configuration into the configurationChan. +func (c *ChallengeTLSALPN) ListenConfiguration(conf dynamic.Configuration) { + for _, certificate := range conf.TLS.Certificates { + if !containsACMETLS1(certificate.Stores) { + continue + } + + c.muChans.Lock() + if _, ok := c.chans[certificate.CertFile.String()]; ok { + close(c.chans[certificate.CertFile.String()]) + } + c.muChans.Unlock() + } +} + +func createMessage(certs map[string]*Certificate) dynamic.Message { + conf := dynamic.Message{ + ProviderName: providerNameALPN, + Configuration: &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + } + + for _, cert := range certs { + certConf := &traefiktls.CertAndStores{ + Certificate: traefiktls.Certificate{ + CertFile: traefiktls.FileOrContent(cert.Certificate), + KeyFile: traefiktls.FileOrContent(cert.Key), + }, + Stores: []string{tlsalpn01.ACMETLS1Protocol}, + } + conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf) + } + + return conf +} + +func containsACMETLS1(stores []string) bool { + for _, store := range stores { + if store == tlsalpn01.ACMETLS1Protocol { + return true + } + } + + return false } diff --git a/pkg/provider/acme/local_store.go b/pkg/provider/acme/local_store.go index 176eae960..7078270ae 100644 --- a/pkg/provider/acme/local_store.go +++ b/pkg/provider/acme/local_store.go @@ -2,7 +2,6 @@ package acme import ( "encoding/json" - "fmt" "io/ioutil" "os" "sync" @@ -171,112 +170,3 @@ func (s *LocalStore) SaveCertificates(resolverName string, certificates []*CertA return nil } - -// LocalChallengeStore is an implementation of the ChallengeStore in memory. -type LocalChallengeStore struct { - storedData *StoredChallengeData - lock sync.RWMutex -} - -// NewLocalChallengeStore initializes a new LocalChallengeStore. -func NewLocalChallengeStore() *LocalChallengeStore { - return &LocalChallengeStore{ - storedData: &StoredChallengeData{ - HTTPChallenges: make(map[string]map[string][]byte), - TLSChallenges: make(map[string]*Certificate), - }, - } -} - -// GetHTTPChallengeToken Get the http challenge token from the store. -func (s *LocalChallengeStore) GetHTTPChallengeToken(token, domain string) ([]byte, error) { - s.lock.RLock() - defer s.lock.RUnlock() - - if s.storedData.HTTPChallenges == nil { - s.storedData.HTTPChallenges = map[string]map[string][]byte{} - } - - if _, ok := s.storedData.HTTPChallenges[token]; !ok { - return nil, fmt.Errorf("cannot find challenge for token %v", token) - } - - result, ok := s.storedData.HTTPChallenges[token][domain] - if !ok { - return nil, fmt.Errorf("cannot find challenge for token %v", token) - } - return result, nil -} - -// SetHTTPChallengeToken Set the http challenge token in the store. -func (s *LocalChallengeStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte) error { - s.lock.Lock() - defer s.lock.Unlock() - - if s.storedData.HTTPChallenges == nil { - s.storedData.HTTPChallenges = map[string]map[string][]byte{} - } - - if _, ok := s.storedData.HTTPChallenges[token]; !ok { - s.storedData.HTTPChallenges[token] = map[string][]byte{} - } - - s.storedData.HTTPChallenges[token][domain] = keyAuth - return nil -} - -// RemoveHTTPChallengeToken Remove the http challenge token in the store. -func (s *LocalChallengeStore) RemoveHTTPChallengeToken(token, domain string) error { - s.lock.Lock() - defer s.lock.Unlock() - - if s.storedData.HTTPChallenges == nil { - return nil - } - - if _, ok := s.storedData.HTTPChallenges[token]; ok { - delete(s.storedData.HTTPChallenges[token], domain) - if len(s.storedData.HTTPChallenges[token]) == 0 { - delete(s.storedData.HTTPChallenges, token) - } - } - return nil -} - -// AddTLSChallenge Add a certificate to the ACME TLS-ALPN-01 certificates storage. -func (s *LocalChallengeStore) AddTLSChallenge(domain string, cert *Certificate) error { - s.lock.Lock() - defer s.lock.Unlock() - - if s.storedData.TLSChallenges == nil { - s.storedData.TLSChallenges = make(map[string]*Certificate) - } - - s.storedData.TLSChallenges[domain] = cert - return nil -} - -// GetTLSChallenge Get a certificate from the ACME TLS-ALPN-01 certificates storage. -func (s *LocalChallengeStore) GetTLSChallenge(domain string) (*Certificate, error) { - s.lock.Lock() - defer s.lock.Unlock() - - if s.storedData.TLSChallenges == nil { - s.storedData.TLSChallenges = make(map[string]*Certificate) - } - - return s.storedData.TLSChallenges[domain], nil -} - -// RemoveTLSChallenge Remove a certificate from the ACME TLS-ALPN-01 certificates storage. -func (s *LocalChallengeStore) RemoveTLSChallenge(domain string) error { - s.lock.Lock() - defer s.lock.Unlock() - - if s.storedData.TLSChallenges == nil { - return nil - } - - delete(s.storedData.TLSChallenges, domain) - return nil -} diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index 20cc271dc..8285413d1 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -82,9 +82,12 @@ type TLSChallenge struct{} // Provider holds configurations of the provider. type Provider struct { *Configuration - ResolverName string - Store Store `json:"store,omitempty" toml:"store,omitempty" yaml:"store,omitempty"` - ChallengeStore ChallengeStore + ResolverName string + Store Store `json:"store,omitempty" toml:"store,omitempty" yaml:"store,omitempty"` + + TLSChallengeProvider challenge.Provider + HTTPChallengeProvider challenge.Provider + certificates []*CertAndStore account *Account client *lego.Client @@ -285,7 +288,7 @@ func (p *Provider) getClient() (*lego.Client, error) { if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 { logger.Debug("Using HTTP Challenge provider.") - err = client.Challenge.SetHTTP01Provider(&challengeHTTP{Store: p.ChallengeStore}) + err = client.Challenge.SetHTTP01Provider(p.HTTPChallengeProvider) if err != nil { return nil, err } @@ -294,7 +297,7 @@ func (p *Provider) getClient() (*lego.Client, error) { if p.TLSChallenge != nil { logger.Debug("Using TLS Challenge provider.") - err = client.Challenge.SetTLSALPN01Provider(&challengeTLSALPN{Store: p.ChallengeStore}) + err = client.Challenge.SetTLSALPN01Provider(p.TLSChallengeProvider) if err != nil { return nil, err } diff --git a/pkg/provider/acme/store.go b/pkg/provider/acme/store.go index 4d8c7965b..6fe899574 100644 --- a/pkg/provider/acme/store.go +++ b/pkg/provider/acme/store.go @@ -6,12 +6,6 @@ type StoredData struct { Certificates []*CertAndStore } -// StoredChallengeData represents the data managed by ChallengeStore. -type StoredChallengeData struct { - HTTPChallenges map[string]map[string][]byte - TLSChallenges map[string]*Certificate -} - // Store is a generic interface that represents a storage. type Store interface { GetAccount(string) (*Account, error) @@ -19,14 +13,3 @@ type Store interface { GetCertificates(string) ([]*CertAndStore, error) SaveCertificates(string, []*CertAndStore) error } - -// ChallengeStore is a generic interface that represents a store for challenge data. -type ChallengeStore interface { - GetHTTPChallengeToken(token, domain string) ([]byte, error) - SetHTTPChallengeToken(token, domain string, keyAuth []byte) error - RemoveHTTPChallengeToken(token, domain string) error - - AddTLSChallenge(domain string, cert *Certificate) error - GetTLSChallenge(domain string) (*Certificate, error) - RemoveTLSChallenge(domain string) error -} diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index d6f1c01f4..322d67780 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -73,11 +73,39 @@ func (i *Provider) createConfiguration(ctx context.Context) *dynamic.Configurati i.redirection(ctx, cfg) i.serverTransport(cfg) + i.acme(cfg) + cfg.HTTP.Services["noop"] = &dynamic.Service{} return cfg } +func (i *Provider) acme(cfg *dynamic.Configuration) { + var eps []string + + uniq := map[string]struct{}{} + for _, resolver := range i.staticCfg.CertificatesResolvers { + if resolver.ACME != nil && resolver.ACME.HTTPChallenge != nil && resolver.ACME.HTTPChallenge.EntryPoint != "" { + if _, ok := uniq[resolver.ACME.HTTPChallenge.EntryPoint]; !ok { + eps = append(eps, resolver.ACME.HTTPChallenge.EntryPoint) + uniq[resolver.ACME.HTTPChallenge.EntryPoint] = struct{}{} + } + } + } + + if len(eps) > 0 { + rt := &dynamic.Router{ + Rule: "PathPrefix(`/.well-known/acme-challenge/`)", + EntryPoints: eps, + Service: "acme-http@internal", + Priority: math.MaxInt32, + } + + cfg.HTTP.Routers["acme-http"] = rt + cfg.HTTP.Services["acme-http"] = &dynamic.Service{} + } +} + func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration) { for name, ep := range i.staticCfg.EntryPoints { if ep.HTTP.Redirections == nil { diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 036e6ea09..856cce022 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -1,6 +1,7 @@ package server import ( + "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/server/provider" @@ -77,7 +78,13 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint } if configuration.TLS != nil { - conf.TLS.Certificates = append(conf.TLS.Certificates, configuration.TLS.Certificates...) + for _, cert := range configuration.TLS.Certificates { + if containsACMETLS1(cert.Stores) && pvd != "tlsalpn.acme" { + continue + } + + conf.TLS.Certificates = append(conf.TLS.Certificates, cert) + } for key, store := range configuration.TLS.Stores { if key != "default" { @@ -160,3 +167,13 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { return cfg } + +func containsACMETLS1(stores []string) bool { + for _, store := range stores { + if store == tlsalpn01.ACMETLS1Protocol { + return true + } + } + + return false +} diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index 7fffbb016..1e6c6e602 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -3,6 +3,7 @@ package server import ( "testing" + "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/stretchr/testify/assert" "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/tls" @@ -122,6 +123,55 @@ func Test_mergeConfiguration(t *testing.T) { } } +func Test_mergeConfiguration_tlsCertificates(t *testing.T) { + testCases := []struct { + desc string + given dynamic.Configurations + expected []*tls.CertAndStores + }{ + { + desc: "Skip temp certificates from another provider than tlsalpn", + given: dynamic.Configurations{ + "provider-1": &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + {Certificate: tls.Certificate{}, Stores: []string{tlsalpn01.ACMETLS1Protocol}}, + }, + }, + }, + }, + expected: nil, + }, + { + desc: "Allows tlsalpn provider to give certificates", + given: dynamic.Configurations{ + "tlsalpn.acme": &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{{ + Certificate: tls.Certificate{CertFile: "foo", KeyFile: "bar"}, + Stores: []string{tlsalpn01.ACMETLS1Protocol}, + }}, + }, + }, + }, + expected: []*tls.CertAndStores{{ + Certificate: tls.Certificate{CertFile: "foo", KeyFile: "bar"}, + Stores: []string{tlsalpn01.ACMETLS1Protocol}, + }}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := mergeConfiguration(test.given, []string{"defaultEP"}) + assert.Equal(t, test.expected, actual.TLS.Certificates) + }) + } +} + func Test_mergeConfiguration_tlsOptions(t *testing.T) { testCases := []struct { desc string diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index ca22b0faf..979bb3518 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -50,7 +50,7 @@ func TestReuseService(t *testing.T) { roundTripperManager := service.NewRoundTripperManager() roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) - managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager) + managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) tlsManager := tls.NewManager() factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) @@ -186,7 +186,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { roundTripperManager := service.NewRoundTripperManager() roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) - managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager) + managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) tlsManager := tls.NewManager() factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) @@ -227,7 +227,7 @@ func TestInternalServices(t *testing.T) { roundTripperManager := service.NewRoundTripperManager() roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) - managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager) + managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) tlsManager := tls.NewManager() factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) diff --git a/pkg/server/service/internalhandler.go b/pkg/server/service/internalhandler.go index 50e4dc9ed..4bd6fd258 100644 --- a/pkg/server/service/internalhandler.go +++ b/pkg/server/service/internalhandler.go @@ -6,8 +6,6 @@ import ( "fmt" "net/http" "strings" - - "github.com/traefik/traefik/v2/pkg/config/runtime" ) type serviceManager interface { @@ -22,22 +20,19 @@ type InternalHandlers struct { rest http.Handler prometheus http.Handler ping http.Handler + acmeHTTP http.Handler serviceManager } // NewInternalHandlers creates a new InternalHandlers. -func NewInternalHandlers(api func(configuration *runtime.Configuration) http.Handler, configuration *runtime.Configuration, rest, metricsHandler, pingHandler, dashboard http.Handler, next serviceManager) *InternalHandlers { - var apiHandler http.Handler - if api != nil { - apiHandler = api(configuration) - } - +func NewInternalHandlers(next serviceManager, apiHandler, rest, metricsHandler, pingHandler, dashboard, acmeHTTP http.Handler) *InternalHandlers { return &InternalHandlers{ api: apiHandler, dashboard: dashboard, rest: rest, prometheus: metricsHandler, ping: pingHandler, + acmeHTTP: acmeHTTP, serviceManager: next, } } @@ -63,6 +58,12 @@ func (m *InternalHandlers) get(serviceName string) (http.Handler, error) { rw.WriteHeader(http.StatusTeapot) }), nil + case "acme-http@internal": + if m.acmeHTTP == nil { + return nil, errors.New("HTTP challenge is not enabled") + } + return m.acmeHTTP, nil + case "api@internal": if m.api == nil { return nil, errors.New("api is not enabled") diff --git a/pkg/server/service/managerfactory.go b/pkg/server/service/managerfactory.go index 3682792fe..438c089b2 100644 --- a/pkg/server/service/managerfactory.go +++ b/pkg/server/service/managerfactory.go @@ -21,16 +21,18 @@ type ManagerFactory struct { dashboardHandler http.Handler metricsHandler http.Handler pingHandler http.Handler + acmeHTTPHandler http.Handler routinesPool *safe.Pool } // NewManagerFactory creates a new ManagerFactory. -func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *safe.Pool, metricsRegistry metrics.Registry, roundTripperManager *RoundTripperManager) *ManagerFactory { +func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *safe.Pool, metricsRegistry metrics.Registry, roundTripperManager *RoundTripperManager, acmeHTTPHandler http.Handler) *ManagerFactory { factory := &ManagerFactory{ metricsRegistry: metricsRegistry, routinesPool: routinesPool, roundTripperManager: roundTripperManager, + acmeHTTPHandler: acmeHTTPHandler, } if staticConfiguration.API != nil { @@ -62,5 +64,11 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s // Build creates a service manager. func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers { svcManager := NewManager(configuration.Services, f.metricsRegistry, f.routinesPool, f.roundTripperManager) - return NewInternalHandlers(f.api, configuration, f.restHandler, f.metricsHandler, f.pingHandler, f.dashboardHandler, svcManager) + + var apiHandler http.Handler + if f.api != nil { + apiHandler = f.api(configuration) + } + + return NewInternalHandlers(svcManager, apiHandler, f.restHandler, f.metricsHandler, f.pingHandler, f.dashboardHandler, f.acmeHTTPHandler) } diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index 0eb09a113..1beebfc64 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -20,12 +20,11 @@ var DefaultTLSOptions = Options{} // Manager is the TLS option/store/configuration factory. type Manager struct { - storesConfig map[string]Store - stores map[string]*CertificateStore - configs map[string]Options - certs []*CertAndStores - TLSAlpnGetter func(string) (*tls.Certificate, error) - lock sync.RWMutex + storesConfig map[string]Store + stores map[string]*CertificateStore + configs map[string]Options + certs []*CertAndStores + lock sync.RWMutex } // NewManager creates a new Manager. @@ -95,6 +94,7 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) { } store := m.getStore(storeName) + acmeTLSStore := m.getStore(tlsalpn01.ACMETLS1Protocol) if err == nil { tlsConfig, err = buildTLSConfig(config) @@ -106,15 +106,13 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) { tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { domainToCheck := types.CanonicalDomain(clientHello.ServerName) - if m.TLSAlpnGetter != nil && isACMETLS(clientHello) { - cert, err := m.TLSAlpnGetter(domainToCheck) - if err != nil { - return nil, err + if isACMETLS(clientHello) { + certificate := acmeTLSStore.GetBestCertificate(clientHello) + if certificate == nil { + return nil, fmt.Errorf("no certificate for TLSALPN challenge: %s", domainToCheck) } - if cert != nil { - return cert, nil - } + return certificate, nil } bestCertificate := store.GetBestCertificate(clientHello)