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:
parent
e3627e9cba
commit
f75f73f3d2
47 changed files with 1573 additions and 1249 deletions
|
@ -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
|
||||
|
|
|
@ -212,7 +212,6 @@
|
|||
storage = "foobar"
|
||||
entryPoint = "foobar"
|
||||
keyType = "foobar"
|
||||
onHostRule = true
|
||||
[acme.dnsChallenge]
|
||||
provider = "foobar"
|
||||
delayBeforeCheck = 42
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: {}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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, ","))
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
50
pkg/types/zz_generated.deepcopy.go
Normal file
50
pkg/types/zz_generated.deepcopy.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue