acme: new HTTP and TLS challenges implementations.
This commit is contained in:
parent
49cdb67ddc
commit
05333b9579
13 changed files with 398 additions and 254 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue