acme: new HTTP and TLS challenges implementations.

This commit is contained in:
Ludovic Fernandez 2020-10-29 15:40:04 +01:00 committed by GitHub
parent 49cdb67ddc
commit 05333b9579
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 398 additions and 254 deletions

View file

@ -13,6 +13,7 @@ import (
"github.com/coreos/go-systemd/daemon" "github.com/coreos/go-systemd/daemon"
assetfs "github.com/elazarl/go-bindata-assetfs" assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/go-acme/lego/v4/challenge"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/traefik/paerser/cli" "github.com/traefik/paerser/cli"
"github.com/traefik/traefik/v2/autogen/genstatic" "github.com/traefik/traefik/v2/autogen/genstatic"
@ -181,7 +182,16 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
tlsManager := traefiktls.NewManager() 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) serverEntryPointsTCP, err := server.NewTCPEntryPoints(staticConfiguration.EntryPoints)
if err != nil { if err != nil {
@ -214,7 +224,16 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
accessLog := setupAccessLog(staticConfiguration.AccessLog) accessLog := setupAccessLog(staticConfiguration.AccessLog)
chainBuilder := middleware.NewChainBuilder(*staticConfiguration, metricsRegistry, accessLog) chainBuilder := middleware.NewChainBuilder(*staticConfiguration, metricsRegistry, accessLog)
roundTripperManager := service.NewRoundTripperManager() 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) client, plgs, devPlugin, err := initPlugins(staticConfiguration)
if err != nil { if err != nil {
@ -264,7 +283,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
roundTripperManager.Update(conf.HTTP.ServersTransports) 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) { watcher.AddListener(func(conf dynamic.Configuration) {
if metricsRegistry.IsEpEnabled() || metricsRegistry.IsSvcEnabled() { 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{}{} resolverNames := map[string]struct{}{}
for _, p := range acmeProviders { for _, p := range acmeProviders {
resolverNames[p.ResolverName] = struct{}{} 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 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) { return func(conf dynamic.Configuration) {
rtConf := runtime.NewConfig(conf) rtConf := runtime.NewConfig(conf)
routers, udpRouters := routerFactory.CreateRouters(rtConf) 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 { if aviator != nil {
aviator.SetRuntimeConfiguration(rtConf) 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. // initACMEProvider creates an acme provider from the ACME part of globalConfiguration.
func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager) []*acme.Provider { func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager, httpChallengeProvider, tlsChallengeProvider challenge.Provider) []*acme.Provider {
challengeStore := acme.NewLocalChallengeStore()
localStores := map[string]*acme.LocalStore{} localStores := map[string]*acme.LocalStore{}
var resolvers []*acme.Provider var resolvers []*acme.Provider
@ -335,10 +346,11 @@ func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.Pr
} }
p := &acme.Provider{ p := &acme.Provider{
Configuration: resolver.ACME, Configuration: resolver.ACME,
Store: localStores[resolver.ACME.Storage], Store: localStores[resolver.ACME.Storage],
ChallengeStore: challengeStore, ResolverName: name,
ResolverName: name, HTTPChallengeProvider: httpChallengeProvider,
TLSChallengeProvider: tlsChallengeProvider,
} }
if err := providerAggregator.AddProvider(p); err != nil { if err := providerAggregator.AddProvider(p); err != nil {
@ -348,15 +360,12 @@ func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.Pr
p.SetTLSManager(tlsManager) p.SetTLSManager(tlsManager)
if p.TLSChallenge != nil {
tlsManager.TLSAlpnGetter = p.GetTLSALPNCertificate
}
p.SetConfigListenerChan(make(chan dynamic.Configuration)) p.SetConfigListenerChan(make(chan dynamic.Configuration))
resolvers = append(resolvers, p) resolvers = append(resolvers, p)
} }
} }
return resolvers return resolvers
} }

View file

@ -2,85 +2,126 @@ package acme
import ( import (
"context" "context"
"errors"
"fmt"
"net" "net"
"net/http" "net/http"
"net/url"
"regexp"
"sync"
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/http01" "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/log"
"github.com/traefik/traefik/v2/pkg/safe" "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 { // NewChallengeHTTP creates a new ChallengeHTTP.
Store ChallengeStore func NewChallengeHTTP() *ChallengeHTTP {
return &ChallengeHTTP{
httpChallenges: make(map[string]map[string][]byte),
}
} }
// Present presents a challenge to obtain new ACME certificate. // Present presents a challenge to obtain new ACME certificate.
func (c *challengeHTTP) Present(domain, token, keyAuth string) error { func (c *ChallengeHTTP) Present(domain, token, keyAuth string) error {
return c.Store.SetHTTPChallengeToken(token, domain, []byte(keyAuth)) 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. // CleanUp cleans the challenges when certificate is obtained.
func (c *challengeHTTP) CleanUp(domain, token, keyAuth string) error { func (c *ChallengeHTTP) CleanUp(domain, token, _ string) error {
return c.Store.RemoveHTTPChallengeToken(token, domain) 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. // 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 return 60 * time.Second, 5 * time.Second
} }
// CreateHandler creates a HTTP handler to expose the token for the HTTP challenge. func (c *ChallengeHTTP) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
func (p *Provider) CreateHandler(notFoundHandler http.Handler) http.Handler { ctx := log.With(req.Context(), log.Str(log.ProviderName, "acme"))
router := mux.NewRouter().SkipClean(true) logger := log.FromContext(ctx)
router.NotFoundHandler = notFoundHandler
router.Methods(http.MethodGet). token, err := getPathParam(req.URL)
Path(http01.ChallengePath("{token}")). if err != nil {
Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { logger.Errorf("Unable to get token: %v.", err)
vars := mux.Vars(req) rw.WriteHeader(http.StatusNotFound)
return
}
ctx := log.With(context.Background(), log.Str(log.ProviderName, p.ResolverName+".acme")) if token != "" {
logger := log.FromContext(ctx) 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 { tokenValue := c.getTokenValue(ctx, token, domain)
domain, _, err := net.SplitHostPort(req.Host) if len(tokenValue) > 0 {
if err != nil { rw.WriteHeader(http.StatusOK)
logger.Debugf("Unable to split host and port: %v. Fallback to request host.", err) _, err = rw.Write(tokenValue)
domain = req.Host if err != nil {
} logger.Errorf("Unable to write token: %v", err)
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
}
} }
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 := 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 var result []byte
operation := func() error { operation := func() error {
var err error c.lock.RLock()
result, err = store.GetHTTPChallengeToken(token, domain) defer c.lock.RUnlock()
return err
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) { notify := func(err error, time time.Duration) {
@ -97,3 +138,14 @@ func getTokenValue(ctx context.Context, token, domain string, store ChallengeSto
return result 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
}

View file

@ -1,22 +1,45 @@
package acme package acme
import ( import (
"crypto/tls" "fmt"
"sync"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/tlsalpn01" "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/log"
"github.com/traefik/traefik/v2/pkg/safe"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
"github.com/traefik/traefik/v2/pkg/types" "github.com/traefik/traefik/v2/pkg/types"
) )
var _ challenge.Provider = (*challengeTLSALPN)(nil) const providerNameALPN = "tlsalpn.acme"
type challengeTLSALPN struct { // ChallengeTLSALPN TLSALPN challenge provider implements challenge.Provider.
Store ChallengeStore 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 { // NewChallengeTLSALPN creates a new ChallengeTLSALPN.
log.WithoutContext().WithField(log.ProviderName, "acme"). 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) Debugf("TLS Challenge Present temp certificate for %s", domain)
certPEMBlock, keyPEMBlock, err := tlsalpn01.ChallengeBlocks(domain, keyAuth) 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}} 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 { // CleanUp cleans the challenges when certificate is obtained.
log.WithoutContext().WithField(log.ProviderName, "acme"). func (c *ChallengeTLSALPN) CleanUp(domain, _, keyAuth string) error {
log.WithoutContext().WithField(log.ProviderName, providerNameALPN).
Debugf("TLS Challenge CleanUp temp certificate for %s", domain) 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. // Init the provider.
func (p *Provider) GetTLSALPNCertificate(domain string) (*tls.Certificate, error) { func (c *ChallengeTLSALPN) Init() error {
cert, err := p.ChallengeStore.GetTLSChallenge(domain) return nil
if err != nil { }
return nil, err
} // 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 {
if cert == nil { c.configurationChan = configurationChan
return nil, nil
} return nil
}
certificate, err := tls.X509KeyPair(cert.Certificate, cert.Key)
if err != nil { // ListenConfiguration sets a new Configuration into the configurationChan.
return nil, err func (c *ChallengeTLSALPN) ListenConfiguration(conf dynamic.Configuration) {
} for _, certificate := range conf.TLS.Certificates {
if !containsACMETLS1(certificate.Stores) {
return &certificate, nil 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
} }

View file

@ -2,7 +2,6 @@ package acme
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"sync" "sync"
@ -171,112 +170,3 @@ func (s *LocalStore) SaveCertificates(resolverName string, certificates []*CertA
return nil 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
}

View file

@ -82,9 +82,12 @@ type TLSChallenge struct{}
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
type Provider struct { type Provider struct {
*Configuration *Configuration
ResolverName string ResolverName string
Store Store `json:"store,omitempty" toml:"store,omitempty" yaml:"store,omitempty"` Store Store `json:"store,omitempty" toml:"store,omitempty" yaml:"store,omitempty"`
ChallengeStore ChallengeStore
TLSChallengeProvider challenge.Provider
HTTPChallengeProvider challenge.Provider
certificates []*CertAndStore certificates []*CertAndStore
account *Account account *Account
client *lego.Client client *lego.Client
@ -285,7 +288,7 @@ func (p *Provider) getClient() (*lego.Client, error) {
if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 { if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 {
logger.Debug("Using HTTP Challenge provider.") logger.Debug("Using HTTP Challenge provider.")
err = client.Challenge.SetHTTP01Provider(&challengeHTTP{Store: p.ChallengeStore}) err = client.Challenge.SetHTTP01Provider(p.HTTPChallengeProvider)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -294,7 +297,7 @@ func (p *Provider) getClient() (*lego.Client, error) {
if p.TLSChallenge != nil { if p.TLSChallenge != nil {
logger.Debug("Using TLS Challenge provider.") logger.Debug("Using TLS Challenge provider.")
err = client.Challenge.SetTLSALPN01Provider(&challengeTLSALPN{Store: p.ChallengeStore}) err = client.Challenge.SetTLSALPN01Provider(p.TLSChallengeProvider)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -6,12 +6,6 @@ type StoredData struct {
Certificates []*CertAndStore 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. // Store is a generic interface that represents a storage.
type Store interface { type Store interface {
GetAccount(string) (*Account, error) GetAccount(string) (*Account, error)
@ -19,14 +13,3 @@ type Store interface {
GetCertificates(string) ([]*CertAndStore, error) GetCertificates(string) ([]*CertAndStore, error)
SaveCertificates(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
}

View file

@ -73,11 +73,39 @@ func (i *Provider) createConfiguration(ctx context.Context) *dynamic.Configurati
i.redirection(ctx, cfg) i.redirection(ctx, cfg)
i.serverTransport(cfg) i.serverTransport(cfg)
i.acme(cfg)
cfg.HTTP.Services["noop"] = &dynamic.Service{} cfg.HTTP.Services["noop"] = &dynamic.Service{}
return cfg 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) { func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration) {
for name, ep := range i.staticCfg.EntryPoints { for name, ep := range i.staticCfg.EntryPoints {
if ep.HTTP.Redirections == nil { if ep.HTTP.Redirections == nil {

View file

@ -1,6 +1,7 @@
package server package server
import ( import (
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/server/provider" "github.com/traefik/traefik/v2/pkg/server/provider"
@ -77,7 +78,13 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
} }
if configuration.TLS != nil { 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 { for key, store := range configuration.TLS.Stores {
if key != "default" { if key != "default" {
@ -160,3 +167,13 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
return cfg return cfg
} }
func containsACMETLS1(stores []string) bool {
for _, store := range stores {
if store == tlsalpn01.ACMETLS1Protocol {
return true
}
}
return false
}

View file

@ -3,6 +3,7 @@ package server
import ( import (
"testing" "testing"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/tls" "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) { func Test_mergeConfiguration_tlsOptions(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View file

@ -50,7 +50,7 @@ func TestReuseService(t *testing.T) {
roundTripperManager := service.NewRoundTripperManager() roundTripperManager := service.NewRoundTripperManager()
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) 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() tlsManager := tls.NewManager()
factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) 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 := service.NewRoundTripperManager()
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) 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() tlsManager := tls.NewManager()
factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) 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 := service.NewRoundTripperManager()
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) 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() tlsManager := tls.NewManager()
factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil) factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(staticConfig, metrics.NewVoidRegistry(), nil), nil)

View file

@ -6,8 +6,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/traefik/traefik/v2/pkg/config/runtime"
) )
type serviceManager interface { type serviceManager interface {
@ -22,22 +20,19 @@ type InternalHandlers struct {
rest http.Handler rest http.Handler
prometheus http.Handler prometheus http.Handler
ping http.Handler ping http.Handler
acmeHTTP http.Handler
serviceManager serviceManager
} }
// NewInternalHandlers creates a new InternalHandlers. // 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 { func NewInternalHandlers(next serviceManager, apiHandler, rest, metricsHandler, pingHandler, dashboard, acmeHTTP http.Handler) *InternalHandlers {
var apiHandler http.Handler
if api != nil {
apiHandler = api(configuration)
}
return &InternalHandlers{ return &InternalHandlers{
api: apiHandler, api: apiHandler,
dashboard: dashboard, dashboard: dashboard,
rest: rest, rest: rest,
prometheus: metricsHandler, prometheus: metricsHandler,
ping: pingHandler, ping: pingHandler,
acmeHTTP: acmeHTTP,
serviceManager: next, serviceManager: next,
} }
} }
@ -63,6 +58,12 @@ func (m *InternalHandlers) get(serviceName string) (http.Handler, error) {
rw.WriteHeader(http.StatusTeapot) rw.WriteHeader(http.StatusTeapot)
}), nil }), 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": case "api@internal":
if m.api == nil { if m.api == nil {
return nil, errors.New("api is not enabled") return nil, errors.New("api is not enabled")

View file

@ -21,16 +21,18 @@ type ManagerFactory struct {
dashboardHandler http.Handler dashboardHandler http.Handler
metricsHandler http.Handler metricsHandler http.Handler
pingHandler http.Handler pingHandler http.Handler
acmeHTTPHandler http.Handler
routinesPool *safe.Pool routinesPool *safe.Pool
} }
// NewManagerFactory creates a new ManagerFactory. // 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{ factory := &ManagerFactory{
metricsRegistry: metricsRegistry, metricsRegistry: metricsRegistry,
routinesPool: routinesPool, routinesPool: routinesPool,
roundTripperManager: roundTripperManager, roundTripperManager: roundTripperManager,
acmeHTTPHandler: acmeHTTPHandler,
} }
if staticConfiguration.API != nil { if staticConfiguration.API != nil {
@ -62,5 +64,11 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s
// Build creates a service manager. // Build creates a service manager.
func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers { func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers {
svcManager := NewManager(configuration.Services, f.metricsRegistry, f.routinesPool, f.roundTripperManager) 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)
} }

View file

@ -20,12 +20,11 @@ var DefaultTLSOptions = Options{}
// Manager is the TLS option/store/configuration factory. // Manager is the TLS option/store/configuration factory.
type Manager struct { type Manager struct {
storesConfig map[string]Store storesConfig map[string]Store
stores map[string]*CertificateStore stores map[string]*CertificateStore
configs map[string]Options configs map[string]Options
certs []*CertAndStores certs []*CertAndStores
TLSAlpnGetter func(string) (*tls.Certificate, error) lock sync.RWMutex
lock sync.RWMutex
} }
// NewManager creates a new Manager. // NewManager creates a new Manager.
@ -95,6 +94,7 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) {
} }
store := m.getStore(storeName) store := m.getStore(storeName)
acmeTLSStore := m.getStore(tlsalpn01.ACMETLS1Protocol)
if err == nil { if err == nil {
tlsConfig, err = buildTLSConfig(config) 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) { tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
domainToCheck := types.CanonicalDomain(clientHello.ServerName) domainToCheck := types.CanonicalDomain(clientHello.ServerName)
if m.TLSAlpnGetter != nil && isACMETLS(clientHello) { if isACMETLS(clientHello) {
cert, err := m.TLSAlpnGetter(domainToCheck) certificate := acmeTLSStore.GetBestCertificate(clientHello)
if err != nil { if certificate == nil {
return nil, err return nil, fmt.Errorf("no certificate for TLSALPN challenge: %s", domainToCheck)
} }
if cert != nil { return certificate, nil
return cert, nil
}
} }
bestCertificate := store.GetBestCertificate(clientHello) bestCertificate := store.GetBestCertificate(clientHello)