Skip to content

Commit

Permalink
Added IgnoreFilter for ACME domain cert generation Fix traefik#1197
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrilPeponnet committed Apr 20, 2017
1 parent d866a62 commit 84898b1
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 22 deletions.
106 changes: 85 additions & 21 deletions acme/acme.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,18 @@ var (

// ACME allows to connect to lets encrypt and retrieve certs
type ACME struct {
Email string `description:"Email address used for registration"`
Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
Storage string `description:"File or key used for certificates storage."`
StorageFile string // deprecated
OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."`
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
CAServer string `description:"CA server to use."`
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
DNSProvider string `description:"Use a DNS based challenge provider rather than HTTPS."`
DelayDontCheckDNS int `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
ACMELogging bool `description:"Enable debug logging of ACME actions."`
Email string `description:"Email address used for registration"`
Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
Storage string `description:"File or key used for certificates storage."`
StorageFile string // deprecated
OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."`
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
IgnoreFilters IgnoreFilters `description:"Use regexp filters on hosts to ignore certificate request from Let's Encrypt."`
CAServer string `description:"CA server to use."`
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
DNSProvider string `description:"Use a DNS based challenge provider rather than HTTPS."`
DelayDontCheckDNS int `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
ACMELogging bool `description:"Enable debug logging of ACME actions."`
client *acme.Client
defaultCertificate *tls.Certificate
store cluster.Store
Expand Down Expand Up @@ -92,6 +93,32 @@ type Domain struct {
SANs []string
}

// IgnoreFilters holds ignore filters
type IgnoreFilters []string

//Set adds strings elem into the the parser
//it splits str on , and ;
func (i *IgnoreFilters) Set(str string) error {
fargs := func(c rune) bool {
return c == ',' || c == ';'
}
// get function
slice := strings.FieldsFunc(str, fargs)
*i = append(*i, slice...)
return nil
}

//Get []string
func (i *IgnoreFilters) Get() interface{} { return IgnoreFilters(*i) }

//String return slice in a string
func (i *IgnoreFilters) String() string { return fmt.Sprintf("%v", *i) }

//SetValue sets []string into the parser
func (i *IgnoreFilters) SetValue(val interface{}) {
*i = IgnoreFilters(val.(IgnoreFilters))
}

func (a *ACME) init() error {
if a.ACMELogging {
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
Expand Down Expand Up @@ -169,7 +196,7 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl

leadership.AddListener(func(elected bool) error {
if elected {
object, err := a.store.Load()
_, err := a.store.Load()
if err != nil {
return err
}
Expand Down Expand Up @@ -326,31 +353,48 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func
}

func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
var wildcardCertificate *tls.Certificate
domain := types.CanonicalDomain(clientHello.ServerName)
account := a.store.Get().(*Account)
//use regex to test for wildcard certs that might have been added into TLSConfig

// Check if we have a wildcard cert into TLSConfig that could be used
log.Debugf("Checking wildcard certificate matching for %v", domain)
for k := range a.TLSConfig.NameToCertificate {
selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$"
match, _ := regexp.MatchString(selector, domain)
if match {
return a.TLSConfig.NameToCertificate[k], nil
log.Debugf("Found a wildcard certificate to use as fallback: %v", k)
wildcardCertificate = a.TLSConfig.NameToCertificate[k]
}
}

// Check for existing challenge cert
log.Debugf("Checking ACME challenge for %v", domain)
if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok {
log.Debugf("ACME got challenge %s", domain)
log.Debugf("Returning ACME challenge cert for %s", domain)
return challengeCert, nil
}

// Check for existing ACME domain cert
log.Debugf("Checking ACME domain cert for %v", domain)
if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok {
log.Debugf("ACME got domain cert %s", domain)
log.Debugf("Returning ACME domain cert for %s", domain)
return domainCert.tlsCert, nil
}

if a.OnDemand {
if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(domain) {
return nil, nil
}
return a.loadCertificateOnDemand(clientHello)
}
log.Debugf("ACME got nothing %s", domain)

if wildcardCertificate != nil {
log.Debugf("Returning wildcard certificate for %s", domain)
return wildcardCertificate, nil
}

log.Debugf("No certificate found to return for %s", domain)
return nil, nil
}

Expand Down Expand Up @@ -520,7 +564,7 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C
// LoadCertificateForDomains loads certificates from ACME for given domains
func (a *ACME) LoadCertificateForDomains(domains []string) {
a.jobs.In() <- func() {
log.Debugf("LoadCertificateForDomains %s...", domains)
log.Debugf("Load certificates for domains: %s", domains)
domains = fun.Map(types.CanonicalDomain, domains).([]string)
operation := func() error {
if a.client == nil {
Expand Down Expand Up @@ -549,16 +593,36 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
} else {
domain = Domain{Main: domains[0]}
}

if _, exists := account.DomainsCertificate.exists(domain); exists {
// domain already exists
log.Debugf("ACME Certificate for domain %v exists.", domain)
return
}

// Check if our domains are matching our ingoreFilters
if len(a.IgnoreFilters) > 0 {
for _, filter := range a.IgnoreFilters {
regexp, rerr := regexp.Compile(filter)
if rerr != nil {
log.Warnf("The ACME ignore filter %v, is not a valid regular expression and will be skipped.", filter)
continue
}
for _, d := range domains {
if regexp.MatchString(d) {
log.Debugf("Acme request for domain %v will be ignored as per filter %v", d, filter)
return
}
}
}
}

log.Debugf("Will generate a new acme cert for %v", domains)
certificate, err := a.getDomainsCertificates(domains)
if err != nil {
log.Errorf("Error getting ACME certificates %+v : %v", domains, err)
return
}
log.Debugf("Got certificate for domains %+v", domains)
log.Debugf("Obtained ACME certificate for domains %+v", domains)
transaction, object, err := a.store.Begin()

if err != nil {
Expand All @@ -585,7 +649,7 @@ func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
certificate, failures := a.client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
if len(failures) > 0 {
log.Error(failures)
return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures)
return nil, fmt.Errorf("Cannot obtain certificates %+v", failures)
}
log.Debugf("Loaded ACME certificates %s", domains)
return &Certificate{
Expand Down
2 changes: 1 addition & 1 deletion acme/challengeProvider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type challengeProvider struct {
}

func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
log.Debugf("Challenge GetCertificate %s", domain)
log.Debugf("Get Challenge certificate for %s", domain)
if !strings.HasSuffix(domain, ".acme.invalid") {
return nil, false
}
Expand Down
1 change: 1 addition & 0 deletions cmd/traefik/traefik.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Complete documentation is available at https://traefik.io`,
f.AddParser(reflect.TypeOf(types.Constraints{}), &types.Constraints{})
f.AddParser(reflect.TypeOf(kubernetes.Namespaces{}), &kubernetes.Namespaces{})
f.AddParser(reflect.TypeOf([]acme.Domain{}), &acme.Domains{})
f.AddParser(reflect.TypeOf(acme.IgnoreFilters{}), &acme.IgnoreFilters{})
f.AddParser(reflect.TypeOf(types.Buckets{}), &types.Buckets{})

//add commands
Expand Down
9 changes: 9 additions & 0 deletions docs/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,14 @@ entryPoint = "https"
#
# OnHostRule = true

# Ignore Filter
# Define a list of regexp that will be used as an ignore filter for domains that will not be allowed to get a certificate through let's encrypt.
#
# Optional
#
# Example to ignore certificate generation for domains that are not like *.foo.domain.tld
# IngnoreFilters = ['.*\.?[^foo]\.domain\.tld']

# CA server to use
# Uncomment the line to run on the staging let's encrypt server
# Leave comment to go to prod
Expand All @@ -375,6 +383,7 @@ entryPoint = "https"
# All domains must have A/AAAA records pointing to Traefik
# WARNING, Take note that Let's Encrypt have rate limiting: https://letsencrypt.org/docs/rate-limits
# Each domain & SANs will lead to a certificate request.
# This will override the ignore filter (see above) if set.
#
# [[acme.domains]]
# main = "local1.com"
Expand Down
2 changes: 2 additions & 0 deletions docs/user-guide/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ storageFile = "acme.json"
onDemand = true
caServer = "http://172.18.0.1:4000/directory"
entryPoint = "https"
# Ignore certificate requests for domains like *.local3.com
ignoreFilters = ['*.\.local3.com']
[[acme.domains]]
main = "local1.com"
Expand Down
8 changes: 8 additions & 0 deletions traefik.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@
#
# OnHostRule = true

# Ignore Filter
# Define a list of regexp that will be used as an ignore filter for domains that will not be allowed to get a certificate through let's encrypt.
#
# Optional
#
# Example to ignore certificate generation for domains that are not like *.foo.domain.tld
# IngnoreFilters = ['.*\.?[^foo]\.domain\.tld']

# CA server to use
# Uncomment the line to run on the staging let's encrypt server
# Leave comment to go to prod
Expand Down

0 comments on commit 84898b1

Please sign in to comment.