1
0
Fork 0

Certificate resolvers.

Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
Co-authored-by: Jean-Baptiste Doumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
Ludovic Fernandez 2019-07-19 11:52:04 +02:00 committed by Traefiker Bot
parent e3627e9cba
commit f75f73f3d2
47 changed files with 1573 additions and 1249 deletions

View file

@ -89,23 +89,18 @@ func TestDo_globalConfiguration(t *testing.T) {
},
},
}
config.ACME = &acme.Configuration{
Email: "acme Email",
ACMELogging: true,
CAServer: "CAServer",
Storage: "Storage",
EntryPoint: "EntryPoint",
KeyType: "MyKeyType",
OnHostRule: true,
DNSChallenge: &acmeprovider.DNSChallenge{Provider: "DNSProvider"},
HTTPChallenge: &acmeprovider.HTTPChallenge{
EntryPoint: "MyEntryPoint",
},
TLSChallenge: &acmeprovider.TLSChallenge{},
Domains: []types.Domain{
{
Main: "Domains Main",
SANs: []string{"Domains acme SANs 1", "Domains acme SANs 2", "Domains acme SANs 3"},
config.CertificatesResolvers = map[string]static.CertificateResolver{
"default": {
ACME: &acme.Configuration{
Email: "acme Email",
CAServer: "CAServer",
Storage: "Storage",
KeyType: "MyKeyType",
DNSChallenge: &acmeprovider.DNSChallenge{Provider: "DNSProvider"},
HTTPChallenge: &acmeprovider.HTTPChallenge{
EntryPoint: "MyEntryPoint",
},
TLSChallenge: &acmeprovider.TLSChallenge{},
},
},
}
@ -126,9 +121,6 @@ func TestDo_globalConfiguration(t *testing.T) {
config.API = &static.API{
EntryPoint: "traefik",
Dashboard: true,
Statistics: &types.Statistics{
RecentErrors: 111,
},
DashboardAssets: &assetfs.AssetFS{
Asset: func(path string) ([]byte, error) {
return nil, nil

View file

@ -212,7 +212,6 @@
storage = "foobar"
entryPoint = "foobar"
keyType = "foobar"
onHostRule = true
[acme.dnsChallenge]
provider = "foobar"
delayBeforeCheck = 42

View file

@ -1,6 +1,10 @@
package dynamic
import "reflect"
import (
"reflect"
"github.com/containous/traefik/pkg/types"
)
// +k8s:deepcopy-gen=true
@ -34,7 +38,9 @@ type Router struct {
// RouterTLSConfig holds the TLS configuration for a router
type RouterTLSConfig struct {
Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty"`
Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty"`
}
// +k8s:deepcopy-gen=true

View file

@ -1,6 +1,10 @@
package dynamic
import "reflect"
import (
"reflect"
"github.com/containous/traefik/pkg/types"
)
// +k8s:deepcopy-gen=true
@ -31,8 +35,10 @@ type TCPRouter struct {
// RouterTCPTLSConfig holds the TLS configuration for a router
type RouterTCPTLSConfig struct {
Passthrough bool `json:"passthrough" toml:"passthrough" yaml:"passthrough"`
Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
Passthrough bool `json:"passthrough" toml:"passthrough" yaml:"passthrough"`
Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"`
CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty"`
Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty"`
}
// +k8s:deepcopy-gen=true

View file

@ -30,6 +30,7 @@ package dynamic
import (
tls "github.com/containous/traefik/pkg/tls"
types "github.com/containous/traefik/pkg/types"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -876,7 +877,7 @@ func (in *Router) DeepCopyInto(out *Router) {
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(RouterTLSConfig)
**out = **in
(*in).DeepCopyInto(*out)
}
return
}
@ -894,6 +895,13 @@ func (in *Router) DeepCopy() *Router {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouterTCPTLSConfig) DeepCopyInto(out *RouterTCPTLSConfig) {
*out = *in
if in.Domains != nil {
in, out := &in.Domains, &out.Domains
*out = make([]types.Domain, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -910,6 +918,13 @@ func (in *RouterTCPTLSConfig) DeepCopy() *RouterTCPTLSConfig {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouterTLSConfig) DeepCopyInto(out *RouterTLSConfig) {
*out = *in
if in.Domains != nil {
in, out := &in.Domains, &out.Domains
*out = make([]types.Domain, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -1096,7 +1111,7 @@ func (in *TCPRouter) DeepCopyInto(out *TCPRouter) {
if in.TLS != nil {
in, out := &in.TLS, &out.TLS
*out = new(RouterTCPTLSConfig)
**out = **in
(*in).DeepCopyInto(*out)
}
return
}

View file

@ -44,7 +44,7 @@ func Test_getRootFieldNames(t *testing.T) {
func Test_decodeFileToNode_compare(t *testing.T) {
nodeToml, err := decodeFileToNode("./fixtures/sample.toml",
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "ACME")
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
if err != nil {
t.Fatal(err)
}
@ -59,7 +59,7 @@ func Test_decodeFileToNode_compare(t *testing.T) {
func Test_decodeFileToNode_Toml(t *testing.T) {
node, err := decodeFileToNode("./fixtures/sample.toml",
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "ACME")
"Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers")
if err != nil {
t.Fatal(err)
}
@ -85,42 +85,35 @@ func Test_decodeFileToNode_Toml(t *testing.T) {
{Name: "retryAttempts", Value: "true"},
{Name: "statusCodes", Value: "foobar,foobar"}}},
{Name: "format", Value: "foobar"}}},
{Name: "acme",
Children: []*parser.Node{
{Name: "acmeLogging", Value: "true"},
{Name: "caServer", Value: "foobar"},
{Name: "dnsChallenge", Children: []*parser.Node{
{Name: "delayBeforeCheck", Value: "42"},
{Name: "disablePropagationCheck", Value: "true"},
{Name: "provider", Value: "foobar"},
{Name: "resolvers", Value: "foobar,foobar"},
}},
{Name: "domains", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "main", Value: "foobar"},
{Name: "sans", Value: "foobar,foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "main", Value: "foobar"},
{Name: "sans", Value: "foobar,foobar"},
}},
}},
{Name: "email", Value: "foobar"},
{Name: "entryPoint", Value: "foobar"},
{Name: "httpChallenge", Children: []*parser.Node{
{Name: "entryPoint", Value: "foobar"}}},
{Name: "keyType", Value: "foobar"},
{Name: "onHostRule", Value: "true"},
{Name: "storage", Value: "foobar"},
{Name: "tlsChallenge"},
},
},
{Name: "api", Children: []*parser.Node{
{Name: "dashboard", Value: "true"},
{Name: "entryPoint", Value: "foobar"},
{Name: "middlewares", Value: "foobar,foobar"},
{Name: "statistics", Children: []*parser.Node{
{Name: "recentErrors", Value: "42"}}}}},
{Name: "certificatesResolvers", Children: []*parser.Node{
{Name: "default", Children: []*parser.Node{
{Name: "acme",
Children: []*parser.Node{
{Name: "acmeLogging", Value: "true"},
{Name: "caServer", Value: "foobar"},
{Name: "dnsChallenge", Children: []*parser.Node{
{Name: "delayBeforeCheck", Value: "42"},
{Name: "disablePropagationCheck", Value: "true"},
{Name: "provider", Value: "foobar"},
{Name: "resolvers", Value: "foobar,foobar"},
}},
{Name: "email", Value: "foobar"},
{Name: "entryPoint", Value: "foobar"},
{Name: "httpChallenge", Children: []*parser.Node{
{Name: "entryPoint", Value: "foobar"}}},
{Name: "keyType", Value: "foobar"},
{Name: "storage", Value: "foobar"},
{Name: "tlsChallenge"},
},
},
}},
}},
{Name: "entryPoints", Children: []*parser.Node{
{Name: "EntryPoint0", Children: []*parser.Node{
{Name: "address", Value: "foobar"},
@ -327,42 +320,35 @@ func Test_decodeFileToNode_Yaml(t *testing.T) {
{Name: "retryAttempts", Value: "true"},
{Name: "statusCodes", Value: "foobar,foobar"}}},
{Name: "format", Value: "foobar"}}},
{Name: "acme",
Children: []*parser.Node{
{Name: "acmeLogging", Value: "true"},
{Name: "caServer", Value: "foobar"},
{Name: "dnsChallenge", Children: []*parser.Node{
{Name: "delayBeforeCheck", Value: "42"},
{Name: "disablePropagationCheck", Value: "true"},
{Name: "provider", Value: "foobar"},
{Name: "resolvers", Value: "foobar,foobar"},
}},
{Name: "domains", Children: []*parser.Node{
{Name: "[0]", Children: []*parser.Node{
{Name: "main", Value: "foobar"},
{Name: "sans", Value: "foobar,foobar"},
}},
{Name: "[1]", Children: []*parser.Node{
{Name: "main", Value: "foobar"},
{Name: "sans", Value: "foobar,foobar"},
}},
}},
{Name: "email", Value: "foobar"},
{Name: "entryPoint", Value: "foobar"},
{Name: "httpChallenge", Children: []*parser.Node{
{Name: "entryPoint", Value: "foobar"}}},
{Name: "keyType", Value: "foobar"},
{Name: "onHostRule", Value: "true"},
{Name: "storage", Value: "foobar"},
{Name: "tlsChallenge"},
},
},
{Name: "api", Children: []*parser.Node{
{Name: "dashboard", Value: "true"},
{Name: "entryPoint", Value: "foobar"},
{Name: "middlewares", Value: "foobar,foobar"},
{Name: "statistics", Children: []*parser.Node{
{Name: "recentErrors", Value: "42"}}}}},
{Name: "certificatesResolvers", Children: []*parser.Node{
{Name: "default", Children: []*parser.Node{
{Name: "acme",
Children: []*parser.Node{
{Name: "acmeLogging", Value: "true"},
{Name: "caServer", Value: "foobar"},
{Name: "dnsChallenge", Children: []*parser.Node{
{Name: "delayBeforeCheck", Value: "42"},
{Name: "disablePropagationCheck", Value: "true"},
{Name: "provider", Value: "foobar"},
{Name: "resolvers", Value: "foobar,foobar"},
}},
{Name: "email", Value: "foobar"},
{Name: "entryPoint", Value: "foobar"},
{Name: "httpChallenge", Children: []*parser.Node{
{Name: "entryPoint", Value: "foobar"}}},
{Name: "keyType", Value: "foobar"},
{Name: "storage", Value: "foobar"},
{Name: "tlsChallenge"},
},
},
}},
}},
{Name: "entryPoints", Children: []*parser.Node{
{Name: "EntryPoint0", Children: []*parser.Node{
{Name: "address", Value: "foobar"},

View file

@ -205,30 +205,21 @@
resolvConfig = "foobar"
resolvDepth = 42
[acme]
[certificatesResolvers.default.acme]
email = "foobar"
acmeLogging = true
caServer = "foobar"
storage = "foobar"
entryPoint = "foobar"
keyType = "foobar"
onHostRule = true
[acme.dnsChallenge]
[certificatesResolvers.default.acme.dnsChallenge]
provider = "foobar"
delayBeforeCheck = 42
resolvers = ["foobar", "foobar"]
disablePropagationCheck = true
[acme.httpChallenge]
[certificatesResolvers.default.acme.httpChallenge]
entryPoint = "foobar"
[acme.tlsChallenge]
[[acme.domains]]
main = "foobar"
sans = ["foobar", "foobar"]
[[acme.domains]]
main = "foobar"
sans = ["foobar", "foobar"]
[certificatesResolvers.default.acme.tlsChallenge]
## Dynamic configuration

View file

@ -214,30 +214,23 @@ hostResolver:
cnameFlattening: true
resolvConfig: foobar
resolvDepth: 42
acme:
email: foobar
acmeLogging: true
caServer: foobar
storage: foobar
entryPoint: foobar
keyType: foobar
onHostRule: true
dnsChallenge:
provider: foobar
delayBeforeCheck: 42
resolvers:
- foobar
- foobar
disablePropagationCheck: true
httpChallenge:
entryPoint: foobar
tlsChallenge: {}
domains:
- main: foobar
sans:
- foobar
- foobar
- main: foobar
sans:
- foobar
- foobar
certificatesResolvers:
default:
acme:
email: foobar
acmeLogging: true
caServer: foobar
storage: foobar
entryPoint: foobar
keyType: foobar
dnsChallenge:
provider: foobar
delayBeforeCheck: 42
resolvers:
- foobar
- foobar
disablePropagationCheck: true
httpChallenge:
entryPoint: foobar
tlsChallenge: {}

View file

@ -1,7 +1,8 @@
package static
import (
"errors"
"fmt"
stdlog "log"
"strings"
"time"
@ -23,7 +24,8 @@ import (
"github.com/containous/traefik/pkg/tracing/zipkin"
"github.com/containous/traefik/pkg/types"
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/go-acme/lego/challenge/dns01"
legolog "github.com/go-acme/lego/log"
"github.com/sirupsen/logrus"
)
const (
@ -59,6 +61,11 @@ type Configuration struct {
HostResolver *types.HostResolverConfig `description:"Enable CNAME Flattening." json:"hostResolver,omitempty" toml:"hostResolver,omitempty" yaml:"hostResolver,omitempty" label:"allowEmpty" export:"true"`
CertificatesResolvers map[string]CertificateResolver `description:"Certificates resolvers configuration." json:"certificatesResolvers,omitempty" toml:"certificatesResolvers,omitempty" yaml:"certificatesResolvers,omitempty" export:"true"`
}
// CertificateResolver contains the configuration for the different types of certificates resolver.
type CertificateResolver struct {
ACME *acmeprovider.Configuration `description:"Enable ACME (Let's Encrypt): automatic SSL." json:"acme,omitempty" toml:"acme,omitempty" yaml:"acme,omitempty" export:"true"`
}
@ -194,64 +201,35 @@ func (c *Configuration) SetEffectiveConfiguration() {
c.initACMEProvider()
}
// FIXME handle on new configuration ACME struct
func (c *Configuration) initACMEProvider() {
if c.ACME != nil {
c.ACME.CAServer = getSafeACMECAServer(c.ACME.CAServer)
if c.ACME.DNSChallenge != nil && c.ACME.HTTPChallenge != nil {
log.Warn("Unable to use DNS challenge and HTTP challenge at the same time. Fallback to DNS challenge.")
c.ACME.HTTPChallenge = nil
}
if c.ACME.DNSChallenge != nil && c.ACME.TLSChallenge != nil {
log.Warn("Unable to use DNS challenge and TLS challenge at the same time. Fallback to DNS challenge.")
c.ACME.TLSChallenge = nil
}
if c.ACME.HTTPChallenge != nil && c.ACME.TLSChallenge != nil {
log.Warn("Unable to use HTTP challenge and TLS challenge at the same time. Fallback to TLS challenge.")
c.ACME.HTTPChallenge = nil
for _, resolver := range c.CertificatesResolvers {
if resolver.ACME != nil {
resolver.ACME.CAServer = getSafeACMECAServer(resolver.ACME.CAServer)
}
}
}
// InitACMEProvider create an acme provider from the ACME part of globalConfiguration
func (c *Configuration) InitACMEProvider() (*acmeprovider.Provider, error) {
if c.ACME != nil {
if len(c.ACME.Storage) == 0 {
return nil, errors.New("unable to initialize ACME provider with no storage location for the certificates")
}
return &acmeprovider.Provider{
Configuration: c.ACME,
}, nil
}
return nil, nil
legolog.Logger = stdlog.New(log.WithoutContext().WriterLevel(logrus.DebugLevel), "legolog: ", 0)
}
// ValidateConfiguration validate that configuration is coherent
func (c *Configuration) ValidateConfiguration() {
if c.ACME != nil {
for _, domain := range c.ACME.Domains {
if domain.Main != dns01.UnFqdn(domain.Main) {
log.Warnf("FQDN detected, please remove the trailing dot: %s", domain.Main)
}
for _, san := range domain.SANs {
if san != dns01.UnFqdn(san) {
log.Warnf("FQDN detected, please remove the trailing dot: %s", san)
}
}
func (c *Configuration) ValidateConfiguration() error {
var acmeEmail string
for name, resolver := range c.CertificatesResolvers {
if resolver.ACME == nil {
continue
}
if len(resolver.ACME.Storage) == 0 {
return fmt.Errorf("unable to initialize certificates resolver %q with no storage location for the certificates", name)
}
if acmeEmail != "" && resolver.ACME.Email != acmeEmail {
return fmt.Errorf("unable to initialize certificates resolver %q, all the acme resolvers must use the same email", name)
}
acmeEmail = resolver.ACME.Email
}
// FIXME Validate store config?
// if c.ACME != nil {
// if _, ok := c.EntryPoints[c.ACME.EntryPoint]; !ok {
// log.Fatalf("Unknown entrypoint %q for ACME configuration", c.ACME.EntryPoint)
// }
// else if c.EntryPoints[c.ACME.EntryPoint].TLS == nil {
// log.Fatalf("Entrypoint %q has no TLS configuration for ACME configuration", c.ACME.EntryPoint)
// }
// }
return nil
}
func getSafeACMECAServer(caServerSrc string) string {

View file

@ -17,7 +17,7 @@ import (
var _ challenge.ProviderTimeout = (*challengeHTTP)(nil)
type challengeHTTP struct {
Store Store
Store ChallengeStore
}
// Present presents a challenge to obtain new ACME certificate.
@ -52,7 +52,7 @@ func (p *Provider) Append(router *mux.Router) {
domain = req.Host
}
tokenValue := getTokenValue(ctx, token, domain, p.Store)
tokenValue := getTokenValue(ctx, token, domain, p.ChallengeStore)
if len(tokenValue) > 0 {
rw.WriteHeader(http.StatusOK)
_, err = rw.Write(tokenValue)
@ -66,7 +66,7 @@ func (p *Provider) Append(router *mux.Router) {
}))
}
func getTokenValue(ctx context.Context, token, domain string, store Store) []byte {
func getTokenValue(ctx context.Context, token, domain string, store ChallengeStore) []byte {
logger := log.FromContext(ctx)
logger.Debugf("Retrieving the ACME challenge for token %v...", token)

View file

@ -12,7 +12,7 @@ import (
var _ challenge.Provider = (*challengeTLSALPN)(nil)
type challengeTLSALPN struct {
Store Store
Store ChallengeStore
}
func (c *challengeTLSALPN) Present(domain, token, keyAuth string) error {
@ -37,7 +37,7 @@ func (c *challengeTLSALPN) CleanUp(domain, token, keyAuth string) error {
// GetTLSALPNCertificate Get the temp certificate for ACME TLS-ALPN-O1 challenge.
func (p *Provider) GetTLSALPNCertificate(domain string) (*tls.Certificate, error) {
cert, err := p.Store.GetTLSChallenge(domain)
cert, err := p.ChallengeStore.GetTLSChallenge(domain)
if err != nil {
return nil, err
}

View file

@ -5,7 +5,6 @@ import (
"fmt"
"io/ioutil"
"os"
"regexp"
"sync"
"github.com/containous/traefik/pkg/log"
@ -16,25 +15,34 @@ var _ Store = (*LocalStore)(nil)
// LocalStore Stores implementation for local file
type LocalStore struct {
saveDataChan chan map[string]*StoredData
filename string
storedData *StoredData
SaveDataChan chan *StoredData `json:"-"`
lock sync.RWMutex
lock sync.RWMutex
storedData map[string]*StoredData
}
// NewLocalStore initializes a new LocalStore with a file name
func NewLocalStore(filename string) *LocalStore {
store := &LocalStore{filename: filename, SaveDataChan: make(chan *StoredData)}
store := &LocalStore{filename: filename, saveDataChan: make(chan map[string]*StoredData)}
store.listenSaveAction()
return store
}
func (s *LocalStore) get() (*StoredData, error) {
func (s *LocalStore) save(resolverName string, storedData *StoredData) {
s.lock.Lock()
defer s.lock.Unlock()
s.storedData[resolverName] = storedData
s.saveDataChan <- s.storedData
}
func (s *LocalStore) get(resolverName string) (*StoredData, error) {
s.lock.Lock()
defer s.lock.Unlock()
if s.storedData == nil {
s.storedData = &StoredData{
HTTPChallenges: make(map[string]map[string][]byte),
TLSChallenges: make(map[string]*Certificate),
}
s.storedData = map[string]*StoredData{}
hasData, err := CheckFile(s.filename)
if err != nil {
@ -56,49 +64,40 @@ func (s *LocalStore) get() (*StoredData, error) {
}
if len(file) > 0 {
if err := json.Unmarshal(file, s.storedData); err != nil {
if err := json.Unmarshal(file, &s.storedData); err != nil {
return nil, err
}
}
// Check if ACME Account is in ACME V1 format
if s.storedData.Account != nil && s.storedData.Account.Registration != nil {
isOldRegistration, err := regexp.MatchString(RegistrationURLPathV1Regexp, s.storedData.Account.Registration.URI)
if err != nil {
return nil, err
}
if isOldRegistration {
logger.Debug("Reseting ACME account.")
s.storedData.Account = nil
s.SaveDataChan <- s.storedData
}
}
// Delete all certificates with no value
var certificates []*Certificate
for _, certificate := range s.storedData.Certificates {
if len(certificate.Certificate) == 0 || len(certificate.Key) == 0 {
logger.Debugf("Deleting empty certificate %v for %v", certificate, certificate.Domain.ToStrArray())
continue
var certificates []*CertAndStore
for _, storedData := range s.storedData {
for _, certificate := range storedData.Certificates {
if len(certificate.Certificate.Certificate) == 0 || len(certificate.Key) == 0 {
logger.Debugf("Deleting empty certificate %v for %v", certificate, certificate.Domain.ToStrArray())
continue
}
certificates = append(certificates, certificate)
}
if len(certificates) < len(storedData.Certificates) {
storedData.Certificates = certificates
s.saveDataChan <- s.storedData
}
certificates = append(certificates, certificate)
}
if len(certificates) < len(s.storedData.Certificates) {
s.storedData.Certificates = certificates
s.SaveDataChan <- s.storedData
}
}
}
return s.storedData, nil
if s.storedData[resolverName] == nil {
s.storedData[resolverName] = &StoredData{}
}
return s.storedData[resolverName], nil
}
// listenSaveAction listens to a chan to store ACME data in json format into LocalStore.filename
func (s *LocalStore) listenSaveAction() {
safe.Go(func() {
logger := log.WithoutContext().WithField(log.ProviderName, "acme")
for object := range s.SaveDataChan {
for object := range s.saveDataChan {
data, err := json.MarshalIndent(object, "", " ")
if err != nil {
logger.Error(err)
@ -113,8 +112,8 @@ func (s *LocalStore) listenSaveAction() {
}
// GetAccount returns ACME Account
func (s *LocalStore) GetAccount() (*Account, error) {
storedData, err := s.get()
func (s *LocalStore) GetAccount(resolverName string) (*Account, error) {
storedData, err := s.get(resolverName)
if err != nil {
return nil, err
}
@ -123,21 +122,21 @@ func (s *LocalStore) GetAccount() (*Account, error) {
}
// SaveAccount stores ACME Account
func (s *LocalStore) SaveAccount(account *Account) error {
storedData, err := s.get()
func (s *LocalStore) SaveAccount(resolverName string, account *Account) error {
storedData, err := s.get(resolverName)
if err != nil {
return err
}
storedData.Account = account
s.SaveDataChan <- storedData
s.save(resolverName, storedData)
return nil
}
// GetCertificates returns ACME Certificates list
func (s *LocalStore) GetCertificates() ([]*Certificate, error) {
storedData, err := s.get()
func (s *LocalStore) GetCertificates(resolverName string) ([]*CertAndStore, error) {
storedData, err := s.get(resolverName)
if err != nil {
return nil, err
}
@ -146,20 +145,37 @@ func (s *LocalStore) GetCertificates() ([]*Certificate, error) {
}
// SaveCertificates stores ACME Certificates list
func (s *LocalStore) SaveCertificates(certificates []*Certificate) error {
storedData, err := s.get()
func (s *LocalStore) SaveCertificates(resolverName string, certificates []*CertAndStore) error {
storedData, err := s.get(resolverName)
if err != nil {
return err
}
storedData.Certificates = certificates
s.SaveDataChan <- storedData
s.save(resolverName, storedData)
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 *LocalStore) GetHTTPChallengeToken(token, domain string) ([]byte, error) {
func (s *LocalChallengeStore) GetHTTPChallengeToken(token, domain string) ([]byte, error) {
s.lock.RLock()
defer s.lock.RUnlock()
@ -179,7 +195,7 @@ func (s *LocalStore) GetHTTPChallengeToken(token, domain string) ([]byte, error)
}
// SetHTTPChallengeToken Set the http challenge token in the store
func (s *LocalStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte) error {
func (s *LocalChallengeStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte) error {
s.lock.Lock()
defer s.lock.Unlock()
@ -196,7 +212,7 @@ func (s *LocalStore) SetHTTPChallengeToken(token, domain string, keyAuth []byte)
}
// RemoveHTTPChallengeToken Remove the http challenge token in the store
func (s *LocalStore) RemoveHTTPChallengeToken(token, domain string) error {
func (s *LocalChallengeStore) RemoveHTTPChallengeToken(token, domain string) error {
s.lock.Lock()
defer s.lock.Unlock()
@ -214,7 +230,7 @@ func (s *LocalStore) RemoveHTTPChallengeToken(token, domain string) error {
}
// AddTLSChallenge Add a certificate to the ACME TLS-ALPN-01 certificates storage
func (s *LocalStore) AddTLSChallenge(domain string, cert *Certificate) error {
func (s *LocalChallengeStore) AddTLSChallenge(domain string, cert *Certificate) error {
s.lock.Lock()
defer s.lock.Unlock()
@ -227,7 +243,7 @@ func (s *LocalStore) AddTLSChallenge(domain string, cert *Certificate) error {
}
// GetTLSChallenge Get a certificate from the ACME TLS-ALPN-01 certificates storage
func (s *LocalStore) GetTLSChallenge(domain string) (*Certificate, error) {
func (s *LocalChallengeStore) GetTLSChallenge(domain string) (*Certificate, error) {
s.lock.Lock()
defer s.lock.Unlock()
@ -239,7 +255,7 @@ func (s *LocalStore) GetTLSChallenge(domain string) (*Certificate, error) {
}
// RemoveTLSChallenge Remove a certificate from the ACME TLS-ALPN-01 certificates storage
func (s *LocalStore) RemoveTLSChallenge(domain string) error {
func (s *LocalChallengeStore) RemoveTLSChallenge(domain string) error {
s.lock.Lock()
defer s.lock.Unlock()

View file

@ -6,8 +6,6 @@ import (
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
fmtlog "log"
"net/url"
"reflect"
"strings"
@ -25,10 +23,8 @@ import (
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/lego"
legolog "github.com/go-acme/lego/log"
"github.com/go-acme/lego/providers/dns"
"github.com/go-acme/lego/registration"
"github.com/sirupsen/logrus"
)
var (
@ -39,16 +35,12 @@ var (
// Configuration holds ACME configuration provided by users
type Configuration struct {
Email string `description:"Email address used for registration." json:"email,omitempty" toml:"email,omitempty" yaml:"email,omitempty"`
ACMELogging bool `description:"Enable debug logging of ACME actions." json:"acmeLogging,omitempty" toml:"acmeLogging,omitempty" yaml:"acmeLogging,omitempty"`
CAServer string `description:"CA server to use." json:"caServer,omitempty" toml:"caServer,omitempty" yaml:"caServer,omitempty"`
Storage string `description:"Storage to use." json:"storage,omitempty" toml:"storage,omitempty" yaml:"storage,omitempty"`
EntryPoint string `description:"EntryPoint to use." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty"`
KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'." json:"keyType,omitempty" toml:"keyType,omitempty" yaml:"keyType,omitempty"`
OnHostRule bool `description:"Enable certificate generation on router Host rules." json:"onHostRule,omitempty" toml:"onHostRule,omitempty" yaml:"onHostRule,omitempty"`
DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty"`
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty"`
TLSChallenge *TLSChallenge `description:"Activate TLS-ALPN-01 Challenge." json:"tlsChallenge,omitempty" toml:"tlsChallenge,omitempty" yaml:"tlsChallenge,omitempty" label:"allowEmpty"`
Domains []types.Domain `description:"The list of domains for which certificates are generated on startup. Wildcard domains only accepted with DNSChallenge." json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty"`
}
// SetDefaults sets the default values.
@ -58,6 +50,12 @@ func (a *Configuration) SetDefaults() {
a.KeyType = "RSA4096"
}
// CertAndStore allows mapping a TLS certificate to a TLS store.
type CertAndStore struct {
Certificate
Store string
}
// Certificate is a struct which contains all data needed from an ACME certificate
type Certificate struct {
Domain types.Domain `json:"domain,omitempty" toml:"domain,omitempty" yaml:"domain,omitempty"`
@ -84,11 +82,13 @@ 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"`
certificates []*Certificate
ChallengeStore ChallengeStore
certificates []*CertAndStore
account *Account
client *lego.Client
certsChan chan *Certificate
certsChan chan *CertAndStore
configurationChan chan<- dynamic.Message
tlsManager *traefiktls.Manager
clientMutex sync.Mutex
@ -113,41 +113,20 @@ func (p *Provider) ListenConfiguration(config dynamic.Configuration) {
p.configFromListenerChan <- config
}
// ListenRequest resolves new certificates for a domain from an incoming request and return a valid Certificate to serve (onDemand option)
func (p *Provider) ListenRequest(domain string) (*tls.Certificate, error) {
ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme"))
acmeCert, err := p.resolveCertificate(ctx, types.Domain{Main: domain}, false)
if acmeCert == nil || err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
return &cert, err
}
// Init for compatibility reason the BaseProvider implements an empty Init
func (p *Provider) Init() error {
ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme"))
logger := log.FromContext(ctx)
if p.ACMELogging {
legolog.Logger = fmtlog.New(logger.WriterLevel(logrus.InfoLevel), "legolog: ", 0)
} else {
legolog.Logger = fmtlog.New(ioutil.Discard, "", 0)
}
if len(p.Configuration.Storage) == 0 {
return errors.New("unable to initialize ACME provider with no storage location for the certificates")
}
p.Store = NewLocalStore(p.Configuration.Storage)
var err error
p.account, err = p.Store.GetAccount()
p.account, err = p.Store.GetAccount(p.ResolverName)
if err != nil {
return fmt.Errorf("unable to get ACME account : %v", err)
return fmt.Errorf("unable to get ACME account: %v", err)
}
// Reset Account if caServer changed, thus registration URI can be updated
@ -156,7 +135,7 @@ func (p *Provider) Init() error {
p.account = nil
}
p.certificates, err = p.Store.GetCertificates()
p.certificates, err = p.Store.GetCertificates(p.ResolverName)
if err != nil {
return fmt.Errorf("unable to get ACME certificates : %v", err)
}
@ -188,7 +167,7 @@ func isAccountMatchingCaServer(ctx context.Context, accountURI string, serverURI
// Provide allows the file provider to provide configurations to traefik
// using the given Configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme"))
ctx := log.With(context.Background(), log.Str(log.ProviderName, "acme."+p.ResolverName))
p.pool = pool
@ -198,17 +177,6 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
p.configurationChan = configurationChan
p.refreshCertificates()
p.deleteUnnecessaryDomains(ctx)
for i := 0; i < len(p.Domains); i++ {
domain := p.Domains[i]
safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, true); err != nil {
log.WithoutContext().WithField(log.ProviderName, "acme").
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
}
})
}
p.renewCertificates(ctx)
ticker := time.NewTicker(24 * time.Hour)
@ -275,13 +243,18 @@ func (p *Provider) getClient() (*lego.Client, error) {
// Save the account once before all the certificates generation/storing
// No certificate can be generated if account is not initialized
err = p.Store.SaveAccount(account)
err = p.Store.SaveAccount(p.ResolverName, account)
if err != nil {
return nil, err
}
switch {
case p.DNSChallenge != nil && len(p.DNSChallenge.Provider) > 0:
if (p.DNSChallenge == nil || len(p.DNSChallenge.Provider) == 0) &&
(p.HTTPChallenge == nil || len(p.HTTPChallenge.EntryPoint) == 0) &&
p.TLSChallenge == nil {
return nil, errors.New("ACME challenge not specified, please select TLS or HTTP or DNS Challenge")
}
if p.DNSChallenge != nil && len(p.DNSChallenge.Provider) > 0 {
logger.Debugf("Using DNS Challenge provider: %s", p.DNSChallenge.Provider)
var provider challenge.Provider
@ -304,25 +277,24 @@ func (p *Provider) getClient() (*lego.Client, error) {
if err != nil {
return nil, err
}
}
case p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0:
if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 {
logger.Debug("Using HTTP Challenge provider.")
err = client.Challenge.SetHTTP01Provider(&challengeHTTP{Store: p.Store})
err = client.Challenge.SetHTTP01Provider(&challengeHTTP{Store: p.ChallengeStore})
if err != nil {
return nil, err
}
}
case p.TLSChallenge != nil:
if p.TLSChallenge != nil {
logger.Debug("Using TLS Challenge provider.")
err = client.Challenge.SetTLSALPN01Provider(&challengeTLSALPN{Store: p.Store})
err = client.Challenge.SetTLSALPN01Provider(&challengeTLSALPN{Store: p.ChallengeStore})
if err != nil {
return nil, err
}
default:
return nil, errors.New("ACME challenge not specified, please select TLS or HTTP or DNS Challenge")
}
p.client = client
@ -346,7 +318,7 @@ func (p *Provider) initAccount(ctx context.Context) (*Account, error) {
return p.account, nil
}
func (p *Provider) resolveDomains(ctx context.Context, domains []string) {
func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStore string) {
if len(domains) == 0 {
log.FromContext(ctx).Debug("No domain parsed in provider ACME")
return
@ -362,7 +334,7 @@ func (p *Provider) resolveDomains(ctx context.Context, domains []string) {
}
safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, false); err != nil {
if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
log.FromContext(ctx).Errorf("Unable to obtain ACME certificate for domains %q: %v", strings.Join(domains, ","), err)
}
})
@ -376,32 +348,72 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
case config := <-p.configFromListenerChan:
if config.TCP != nil {
for routerName, route := range config.TCP.Routers {
if route.TLS == nil {
if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
continue
}
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
domains, err := rules.ParseHostSNI(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue
tlsStore := "default"
if len(route.TLS.Domains) > 0 {
for _, domain := range route.TLS.Domains {
if domain.Main != dns01.UnFqdn(domain.Main) {
log.Warnf("FQDN detected, please remove the trailing dot: %s", domain.Main)
}
for _, san := range domain.SANs {
if san != dns01.UnFqdn(san) {
log.Warnf("FQDN detected, please remove the trailing dot: %s", san)
}
}
}
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := 0; i < len(domains); i++ {
domain := domains[i]
safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
log.WithoutContext().WithField(log.ProviderName, "acme."+p.ResolverName).
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
}
})
}
} else {
domains, err := rules.ParseHostSNI(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue
}
p.resolveDomains(ctxRouter, domains, tlsStore)
}
p.resolveDomains(ctxRouter, domains)
}
}
for routerName, route := range config.HTTP.Routers {
if route.TLS == nil {
if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
continue
}
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
domains, err := rules.ParseDomains(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue
tlsStore := "default"
if len(route.TLS.Domains) > 0 {
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := 0; i < len(domains); i++ {
domain := domains[i]
safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
log.WithoutContext().WithField(log.ProviderName, "acme."+p.ResolverName).
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
}
})
}
} else {
domains, err := rules.ParseDomains(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue
}
p.resolveDomains(ctxRouter, domains, tlsStore)
}
p.resolveDomains(ctxRouter, domains)
}
case <-stop:
return
@ -410,14 +422,14 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, domainFromConfigurationFile bool) (*certificate.Resource, error) {
domains, err := p.getValidDomains(ctx, domain, domainFromConfigurationFile)
func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, tlsStore string) (*certificate.Resource, error) {
domains, err := p.getValidDomains(ctx, domain)
if err != nil {
return nil, err
}
// Check provided certificates
uncheckedDomains := p.getUncheckedDomains(ctx, domains, !domainFromConfigurationFile)
uncheckedDomains := p.getUncheckedDomains(ctx, domains, tlsStore)
if len(uncheckedDomains) == 0 {
return nil, nil
}
@ -457,7 +469,7 @@ func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain,
} else {
domain = types.Domain{Main: uncheckedDomains[0]}
}
p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey)
p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey, tlsStore)
return cert, nil
}
@ -480,22 +492,22 @@ func (p *Provider) addResolvingDomains(resolvingDomains []string) {
}
}
func (p *Provider) addCertificateForDomain(domain types.Domain, certificate []byte, key []byte) {
p.certsChan <- &Certificate{Certificate: certificate, Key: key, Domain: domain}
func (p *Provider) addCertificateForDomain(domain types.Domain, certificate []byte, key []byte, tlsStore string) {
p.certsChan <- &CertAndStore{Certificate: Certificate{Certificate: certificate, Key: key, Domain: domain}, Store: tlsStore}
}
// deleteUnnecessaryDomains deletes from the configuration :
// - Duplicated domains
// - Domains which are checked by wildcard domain
func (p *Provider) deleteUnnecessaryDomains(ctx context.Context) {
func deleteUnnecessaryDomains(ctx context.Context, domains []types.Domain) []types.Domain {
var newDomains []types.Domain
logger := log.FromContext(ctx)
for idxDomainToCheck, domainToCheck := range p.Domains {
for idxDomainToCheck, domainToCheck := range domains {
keepDomain := true
for idxDomain, domain := range p.Domains {
for idxDomain, domain := range domains {
if idxDomainToCheck == idxDomain {
continue
}
@ -538,11 +550,11 @@ func (p *Provider) deleteUnnecessaryDomains(ctx context.Context) {
}
}
p.Domains = newDomains
return newDomains
}
func (p *Provider) watchCertificate(ctx context.Context) {
p.certsChan = make(chan *Certificate)
p.certsChan = make(chan *CertAndStore)
p.pool.Go(func(stop chan bool) {
for {
@ -550,9 +562,8 @@ func (p *Provider) watchCertificate(ctx context.Context) {
case cert := <-p.certsChan:
certUpdated := false
for _, domainsCertificate := range p.certificates {
if reflect.DeepEqual(cert.Domain, domainsCertificate.Domain) {
if reflect.DeepEqual(cert.Domain, domainsCertificate.Certificate.Domain) {
domainsCertificate.Certificate = cert.Certificate
domainsCertificate.Key = cert.Key
certUpdated = true
break
}
@ -573,7 +584,7 @@ func (p *Provider) watchCertificate(ctx context.Context) {
}
func (p *Provider) saveCertificates() error {
err := p.Store.SaveCertificates(p.certificates)
err := p.Store.SaveCertificates(p.ResolverName, p.certificates)
p.refreshCertificates()
@ -582,7 +593,7 @@ func (p *Provider) saveCertificates() error {
func (p *Provider) refreshCertificates() {
conf := dynamic.Message{
ProviderName: "ACME",
ProviderName: "acme." + p.ResolverName,
Configuration: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
@ -596,9 +607,10 @@ func (p *Provider) refreshCertificates() {
for _, cert := range p.certificates {
certConf := &traefiktls.CertAndStores{
Certificate: traefiktls.Certificate{
CertFile: traefiktls.FileOrContent(cert.Certificate),
CertFile: traefiktls.FileOrContent(cert.Certificate.Certificate),
KeyFile: traefiktls.FileOrContent(cert.Key),
},
Stores: []string{cert.Store},
}
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
}
@ -611,7 +623,7 @@ func (p *Provider) renewCertificates(ctx context.Context) {
logger.Info("Testing certificate renew...")
for _, cert := range p.certificates {
crt, err := getX509Certificate(ctx, cert)
crt, err := getX509Certificate(ctx, &cert.Certificate)
// If there's an error, we assume the cert is broken, and needs update
// <= 30 days left, renew certificate
if err != nil || crt == nil || crt.NotAfter.Before(time.Now().Add(24*30*time.Hour)) {
@ -626,7 +638,7 @@ func (p *Provider) renewCertificates(ctx context.Context) {
renewedCert, err := client.Certificate.Renew(certificate.Resource{
Domain: cert.Domain.Main,
PrivateKey: cert.Key,
Certificate: cert.Certificate,
Certificate: cert.Certificate.Certificate,
}, true, oscpMustStaple)
if err != nil {
@ -639,20 +651,20 @@ func (p *Provider) renewCertificates(ctx context.Context) {
continue
}
p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey)
p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey, cert.Store)
}
}
}
// Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates
func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, checkConfigurationDomains bool) []string {
func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, tlsStore string) []string {
p.resolvingDomainsMutex.RLock()
defer p.resolvingDomainsMutex.RUnlock()
log.FromContext(ctx).Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck)
allDomains := p.tlsManager.GetStore("default").GetAllDomains()
allDomains := p.tlsManager.GetStore(tlsStore).GetAllDomains()
// Get ACME certificates
for _, cert := range p.certificates {
@ -664,13 +676,6 @@ func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []str
allDomains = append(allDomains, domain)
}
// Get Configuration Domains
if checkConfigurationDomains {
for i := 0; i < len(p.Domains); i++ {
allDomains = append(allDomains, strings.Join(p.Domains[i].ToStrArray(), ","))
}
}
return searchUncheckedDomains(ctx, domainsToCheck, allDomains)
}
@ -712,17 +717,13 @@ func getX509Certificate(ctx context.Context, cert *Certificate) (*x509.Certifica
}
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it
func (p *Provider) getValidDomains(ctx context.Context, domain types.Domain, wildcardAllowed bool) ([]string, error) {
func (p *Provider) getValidDomains(ctx context.Context, domain types.Domain) ([]string, error) {
domains := domain.ToStrArray()
if len(domains) == 0 {
return nil, errors.New("unable to generate a certificate in ACME provider when no domain is given")
}
if strings.HasPrefix(domain.Main, "*") {
if !wildcardAllowed {
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q from a 'Host' rule", strings.Join(domains, ","))
}
if p.DNSChallenge == nil {
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
}

View file

@ -30,7 +30,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
desc string
dynamicCerts *safe.Safe
resolvingDomains map[string]struct{}
acmeCertificates []*Certificate
acmeCertificates []*CertAndStore
domains []string
expectedDomains []string
}{
@ -48,9 +48,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "wildcard already exists in ACME certificates",
domains: []string{"*.traefik.wtf"},
acmeCertificates: []*Certificate{
acmeCertificates: []*CertAndStore{
{
Domain: types.Domain{Main: "*.traefik.wtf"},
Certificate: Certificate{
Domain: types.Domain{Main: "*.traefik.wtf"},
},
},
},
expectedDomains: nil,
@ -69,9 +71,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "domain CN already exists in ACME certificates and SANs to generate",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
acmeCertificates: []*Certificate{
acmeCertificates: []*CertAndStore{
{
Domain: types.Domain{Main: "traefik.wtf"},
Certificate: Certificate{
Domain: types.Domain{Main: "traefik.wtf"},
},
},
},
expectedDomains: []string{"foo.traefik.wtf"},
@ -85,9 +89,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "domain already exists in ACME certificates",
domains: []string{"traefik.wtf"},
acmeCertificates: []*Certificate{
acmeCertificates: []*CertAndStore{
{
Domain: types.Domain{Main: "traefik.wtf"},
Certificate: Certificate{
Domain: types.Domain{Main: "traefik.wtf"},
},
},
},
expectedDomains: nil,
@ -101,9 +107,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "domain matched by wildcard in ACME certificates",
domains: []string{"who.traefik.wtf", "foo.traefik.wtf"},
acmeCertificates: []*Certificate{
acmeCertificates: []*CertAndStore{
{
Domain: types.Domain{Main: "*.traefik.wtf"},
Certificate: Certificate{
Domain: types.Domain{Main: "*.traefik.wtf"},
},
},
},
expectedDomains: nil,
@ -111,9 +119,11 @@ func TestGetUncheckedCertificates(t *testing.T) {
{
desc: "root domain with wildcard in ACME certificates",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
acmeCertificates: []*Certificate{
acmeCertificates: []*CertAndStore{
{
Domain: types.Domain{Main: "*.traefik.wtf"},
Certificate: Certificate{
Domain: types.Domain{Main: "*.traefik.wtf"},
},
},
},
expectedDomains: []string{"traefik.wtf"},
@ -171,7 +181,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
resolvingDomains: test.resolvingDomains,
}
domains := acmeProvider.getUncheckedDomains(context.Background(), test.domains, false)
domains := acmeProvider.getUncheckedDomains(context.Background(), test.domains, "default")
assert.Equal(t, len(test.expectedDomains), len(domains), "Unexpected domains.")
})
}
@ -181,7 +191,6 @@ func TestGetValidDomain(t *testing.T) {
testCases := []struct {
desc string
domains types.Domain
wildcardAllowed bool
dnsChallenge *DNSChallenge
expectedErr string
expectedDomains []string
@ -190,7 +199,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "valid wildcard",
domains: types.Domain{Main: "*.traefik.wtf"},
dnsChallenge: &DNSChallenge{},
wildcardAllowed: true,
expectedErr: "",
expectedDomains: []string{"*.traefik.wtf"},
},
@ -199,22 +207,12 @@ func TestGetValidDomain(t *testing.T) {
domains: types.Domain{Main: "traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
dnsChallenge: &DNSChallenge{},
expectedErr: "",
wildcardAllowed: true,
expectedDomains: []string{"traefik.wtf", "foo.traefik.wtf"},
},
{
desc: "unauthorized wildcard",
domains: types.Domain{Main: "*.traefik.wtf"},
dnsChallenge: &DNSChallenge{},
wildcardAllowed: false,
expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf\" from a 'Host' rule",
expectedDomains: nil,
},
{
desc: "no domain",
domains: types.Domain{},
dnsChallenge: nil,
wildcardAllowed: true,
expectedErr: "unable to generate a certificate in ACME provider when no domain is given",
expectedDomains: nil,
},
@ -222,7 +220,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "no DNSChallenge",
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
dnsChallenge: nil,
wildcardAllowed: true,
expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf,foo.traefik.wtf\" : ACME needs a DNSChallenge",
expectedDomains: nil,
},
@ -230,7 +227,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "unauthorized wildcard with SAN",
domains: types.Domain{Main: "*.*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}},
dnsChallenge: &DNSChallenge{},
wildcardAllowed: true,
expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.*.traefik.wtf,foo.traefik.wtf\" : ACME does not allow '*.*' wildcard domain",
expectedDomains: nil,
},
@ -238,7 +234,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "wildcard and SANs",
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"traefik.wtf"}},
dnsChallenge: &DNSChallenge{},
wildcardAllowed: true,
expectedErr: "",
expectedDomains: []string{"*.traefik.wtf", "traefik.wtf"},
},
@ -246,7 +241,6 @@ func TestGetValidDomain(t *testing.T) {
desc: "wildcard SANs",
domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"*.acme.wtf"}},
dnsChallenge: &DNSChallenge{},
wildcardAllowed: true,
expectedErr: "",
expectedDomains: []string{"*.traefik.wtf", "*.acme.wtf"},
},
@ -259,7 +253,7 @@ func TestGetValidDomain(t *testing.T) {
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}}
domains, err := acmeProvider.getValidDomains(context.Background(), test.domains, test.wildcardAllowed)
domains, err := acmeProvider.getValidDomains(context.Background(), test.domains)
if len(test.expectedErr) > 0 {
assert.EqualError(t, err, test.expectedErr, "Unexpected error.")
@ -439,10 +433,8 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
acmeProvider := Provider{Configuration: &Configuration{Domains: test.domains}}
acmeProvider.deleteUnnecessaryDomains(context.Background())
assert.Equal(t, test.expectedDomains, acmeProvider.Domains, "unexpected domain")
domains := deleteUnnecessaryDomains(context.Background(), test.domains)
assert.Equal(t, test.expectedDomains, domains, "unexpected domain")
})
}
}

View file

@ -1,20 +1,27 @@
package acme
// StoredData represents the data managed by Store
// StoredData represents the data managed by Store.
type StoredData struct {
Account *Account
Certificates []*Certificate
Account *Account
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 {
GetAccount() (*Account, error)
SaveAccount(*Account) error
GetCertificates() ([]*Certificate, error)
SaveCertificates([]*Certificate) error
GetAccount(string) (*Account, error)
SaveAccount(string, *Account) error
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

View file

@ -438,7 +438,10 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
}
if ingressRoute.Spec.TLS != nil {
tlsConf := &dynamic.RouterTLSConfig{}
tlsConf := &dynamic.RouterTLSConfig{
CertResolver: ingressRoute.Spec.TLS.CertResolver,
}
if ingressRoute.Spec.TLS.Options != nil && len(ingressRoute.Spec.TLS.Options.Name) > 0 {
tlsOptionsName := ingressRoute.Spec.TLS.Options.Name
// Is a Kubernetes CRD reference, (i.e. not a cross-provider reference)
@ -537,7 +540,8 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
if ingressRouteTCP.Spec.TLS != nil {
conf.Routers[serviceName].TLS = &dynamic.RouterTCPTLSConfig{
Passthrough: ingressRouteTCP.Spec.TLS.Passthrough,
Passthrough: ingressRouteTCP.Spec.TLS.Passthrough,
CertResolver: ingressRouteTCP.Spec.TLS.CertResolver,
}
if ingressRouteTCP.Spec.TLS.Options != nil && len(ingressRouteTCP.Spec.TLS.Options.Name) > 0 {

View file

@ -32,7 +32,8 @@ type TLS struct {
// certificate details.
SecretName string `json:"secretName"`
// Options is a reference to a TLSOption, that specifies the parameters of the TLS connection.
Options *TLSOptionRef `json:"options"`
Options *TLSOptionRef `json:"options"`
CertResolver string `json:"certResolver"`
}
// TLSOptionRef is a ref to the TLSOption resources.

View file

@ -30,7 +30,8 @@ type TLSTCP struct {
SecretName string `json:"secretName"`
Passthrough bool `json:"passthrough"`
// Options is a reference to a TLSOption, that specifies the parameters of the TLS connection.
Options *TLSOptionTCPRef `json:"options"`
Options *TLSOptionTCPRef `json:"options"`
CertResolver string `json:"certResolver"`
}
// TLSOptionTCPRef is a ref to the TLSOption resources.

View file

@ -11,7 +11,7 @@ import (
)
// NewRouteAppenderFactory Creates a new RouteAppenderFactory
func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPointName string, acmeProvider *acme.Provider) *RouteAppenderFactory {
func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPointName string, acmeProvider []*acme.Provider) *RouteAppenderFactory {
return &RouteAppenderFactory{
staticConfiguration: staticConfiguration,
entryPointName: entryPointName,
@ -23,15 +23,18 @@ func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPoin
type RouteAppenderFactory struct {
staticConfiguration static.Configuration
entryPointName string
acmeProvider *acme.Provider
acmeProvider []*acme.Provider
}
// NewAppender Creates a new RouteAppender
func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, runtimeConfiguration *runtime.Configuration) types.RouteAppender {
aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, runtimeConfiguration)
if r.acmeProvider != nil && r.acmeProvider.HTTPChallenge != nil && r.acmeProvider.HTTPChallenge.EntryPoint == r.entryPointName {
aggregator.AddAppender(r.acmeProvider)
for _, p := range r.acmeProvider {
if p != nil && p.HTTPChallenge != nil && p.HTTPChallenge.EntryPoint == r.entryPointName {
aggregator.AddAppender(p)
break
}
}
return aggregator

View file

@ -79,6 +79,11 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m
return entryPointHandlers
}
type nameAndConfig struct {
routerName string // just so we have it as additional information when logging
TLSConfig *tls.Config
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) {
router := &tcp.Router{}
router.HTTPHandler(handlerHTTP)
@ -86,15 +91,11 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
defaultTLSConf, err := m.tlsManager.Get("default", defaultTLSConfigName)
if err != nil {
return nil, err
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
}
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
type nameAndConfig struct {
routerName string // just so we have it as additional information when logging
TLSConfig *tls.Config
}
// Keyed by domain, then by options reference.
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
@ -156,7 +157,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
} else {
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS option instead", hostSNI), false)
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
routers = append(routers, v.routerName)
}
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)

View file

@ -74,17 +74,22 @@ func (m *Manager) Get(storeName string, configName string) (*tls.Config, error)
m.lock.RLock()
defer m.lock.RUnlock()
var tlsConfig *tls.Config
var err error
config, ok := m.configs[configName]
if !ok {
return nil, fmt.Errorf("unknown TLS options: %s", configName)
err = fmt.Errorf("unknown TLS options: %s", configName)
tlsConfig = &tls.Config{}
}
store := m.getStore(storeName)
tlsConfig, err := buildTLSConfig(config)
if err != nil {
log.Error(err)
tlsConfig = &tls.Config{}
if err == nil {
tlsConfig, err = buildTLSConfig(config)
if err != nil {
tlsConfig = &tls.Config{}
}
}
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
@ -113,7 +118,8 @@ func (m *Manager) Get(storeName string, configName string) (*tls.Config, error)
log.WithoutContext().Debugf("Serving default certificate for request: %q", domainToCheck)
return store.DefaultCertificate, nil
}
return tlsConfig, nil
return tlsConfig, err
}
func (m *Manager) getStore(storeName string) *CertificateStore {
@ -143,7 +149,7 @@ func buildCertificateStore(tlsStore Store) (*CertificateStore, error) {
}
certificateStore.DefaultCertificate = cert
} else {
log.Debug("No default certificate, generate one")
log.Debug("No default certificate, generating one")
cert, err := generate.DefaultCertificate()
if err != nil {
return certificateStore, err

View file

@ -152,11 +152,21 @@ func TestManager_Get(t *testing.T) {
func TestClientAuth(t *testing.T) {
tlsConfigs := map[string]Options{
"eca": {ClientAuth: ClientAuth{}},
"ecat": {ClientAuth: ClientAuth{ClientAuthType: ""}},
"ncc": {ClientAuth: ClientAuth{ClientAuthType: "NoClientCert"}},
"rcc": {ClientAuth: ClientAuth{ClientAuthType: "RequestClientCert"}},
"racc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAnyClientCert"}},
"eca": {
ClientAuth: ClientAuth{},
},
"ecat": {
ClientAuth: ClientAuth{ClientAuthType: ""},
},
"ncc": {
ClientAuth: ClientAuth{ClientAuthType: "NoClientCert"},
},
"rcc": {
ClientAuth: ClientAuth{ClientAuthType: "RequestClientCert"},
},
"racc": {
ClientAuth: ClientAuth{ClientAuthType: "RequireAnyClientCert"},
},
"vccig": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
@ -166,7 +176,9 @@ func TestClientAuth(t *testing.T) {
"vccigwca": {
ClientAuth: ClientAuth{ClientAuthType: "VerifyClientCertIfGiven"},
},
"ravcc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAndVerifyClientCert"}},
"ravcc": {
ClientAuth: ClientAuth{ClientAuthType: "RequireAndVerifyClientCert"},
},
"ravccwca": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
@ -179,7 +191,9 @@ func TestClientAuth(t *testing.T) {
ClientAuthType: "RequireAndVerifyClientCert",
},
},
"ucat": {ClientAuth: ClientAuth{ClientAuthType: "Unknown"}},
"ucat": {
ClientAuth: ClientAuth{ClientAuthType: "Unknown"},
},
}
block, _ := pem.Decode([]byte(localhostCert))
@ -191,6 +205,7 @@ func TestClientAuth(t *testing.T) {
tlsOptionsName string
expectedClientAuth tls.ClientAuthType
expectedRawSubject []byte
expectedError bool
}{
{
desc: "Empty ClientAuth option should get a tls.NoClientCert (default value)",
@ -223,14 +238,16 @@ func TestClientAuth(t *testing.T) {
expectedClientAuth: tls.VerifyClientCertIfGiven,
},
{
desc: "VerifyClientCertIfGiven option without CAFiles yields a default ClientAuthType (NoClientCert)",
desc: "VerifyClientCertIfGiven option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "vccigwca",
expectedClientAuth: tls.NoClientCert,
expectedError: true,
},
{
desc: "RequireAndVerifyClientCert option without CAFiles yields a default ClientAuthType (NoClientCert)",
desc: "RequireAndVerifyClientCert option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravcc",
expectedClientAuth: tls.NoClientCert,
expectedError: true,
},
{
desc: "RequireAndVerifyClientCert option should get a tls.RequireAndVerifyClientCert as ClientAuthType with CA files",
@ -242,11 +259,13 @@ func TestClientAuth(t *testing.T) {
desc: "Unknown option yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ucat",
expectedClientAuth: tls.NoClientCert,
expectedError: true,
},
{
desc: "Bad CA certificate content yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravccwbca",
expectedClientAuth: tls.NoClientCert,
expectedError: true,
},
}
@ -259,6 +278,12 @@ func TestClientAuth(t *testing.T) {
t.Parallel()
config, err := tlsManager.Get("default", test.tlsOptionsName)
if test.expectedError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
if test.expectedRawSubject != nil {

View file

@ -1,10 +1,11 @@
package types
import (
"fmt"
"strings"
)
// +k8s:deepcopy-gen=true
// Domain holds a domain name with SANs.
type Domain struct {
Main string `description:"Default subject name." json:"main,omitempty" toml:"main,omitempty" yaml:"main,omitempty"`
@ -28,44 +29,6 @@ func (d *Domain) Set(domains []string) {
}
}
// Domains parse []Domain.
type Domains []Domain
// Set []Domain
func (ds *Domains) Set(str string) error {
fargs := func(c rune) bool {
return c == ',' || c == ';'
}
// get function
slice := strings.FieldsFunc(str, fargs)
if len(slice) < 1 {
return fmt.Errorf("parse error ACME.Domain. Unable to parse %s", str)
}
d := Domain{
Main: slice[0],
}
if len(slice) > 1 {
d.SANs = slice[1:]
}
*ds = append(*ds, d)
return nil
}
// Get []Domain.
func (ds *Domains) Get() interface{} { return []Domain(*ds) }
// String returns []Domain in string.
func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) }
// SetValue sets []Domain into the parser
func (ds *Domains) SetValue(val interface{}) {
*ds = val.([]Domain)
}
// MatchDomain returns true if a domain match the cert domain.
func MatchDomain(domain string, certDomain string) bool {
if domain == certDomain {

View file

@ -0,0 +1,50 @@
// +build !ignore_autogenerated
/*
The MIT License (MIT)
Copyright (c) 2016-2019 Containous SAS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package types
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Domain) DeepCopyInto(out *Domain) {
*out = *in
if in.SANs != nil {
in, out := &in.SANs, &out.SANs
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Domain.
func (in *Domain) DeepCopy() *Domain {
if in == nil {
return nil
}
out := new(Domain)
in.DeepCopyInto(out)
return out
}