Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added IgnoreFilter for ACME domain cert generation #1260

Closed

Conversation

CyrilPeponnet
Copy link

@CyrilPeponnet CyrilPeponnet commented Mar 8, 2017

This Pull Request fixes the following:

  • Avoid to generate ACME certs if a wildcard cert is present
  • Always return the ACME cert if present and fallback to wildcard if present

And add the a new feature ignoreFilters to filter out domains for ACME certificate generation.

The static declaration of domains in KV/TOML will override the filters.

Test cases:

case 1: I have wildcard certificate for domain.tld and I want acme to generate acme certs for only a subset of it.
Use the ignoreFilters with a regular expression like .*\.?[^f][^o][^o]\.domain\.tld (negative lookahead are not supported by golang), to generate acme cert for domains except those like *.foo.domain.tld.

case 2: I have wildcard for domain.tld but I want acme to take precedence over it for statically defined domains.
Ignore the entire domain with a filter like .*\.domain\.tld, and define your domains in ACME configuration (don't forget to restart traefik as this part is not reloaded dynamicaly).

case 3: I have wildcard certificate for domain.tld and I want it to be used instead of already defined acme certs.
You will need to revoke and clean the acme cert from your configuration (if from KV, you will need to base64decode it, strip the cert part you want to remove, reencode, update it and restart traefik).

Let me know if I forgot something.

Fixes #1197

acme/acme.go Outdated
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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the only time wildcardCertificate is used, why not just return it directly? Why store it, and check if it is empty later?

Seems like more code execution unnecessarily.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. Will move this part.

acme/acme.go Outdated
return
}

// Check if our domains are matching our ingoreFilters
if len(a.IgnoreFilters) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is loadCertificateOnDemand the correct location for this code?

This means that the certificate will be requested from acme, but not returned correct?

Wouldn't this be better served in getCertificate before a potential cert request?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case we are in a 'onDemand' for now there is no check with ignoreFilters, this can be added if needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtomcej please advise.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be good.

@CyrilPeponnet CyrilPeponnet force-pushed the acme_fix_and_improvements branch 2 times, most recently from 1f075a0 to 1797ab9 Compare March 9, 2017 16:52
@w3blogfr
Copy link

This feature will be really usefull.

Copy link
Member

@emilevauge emilevauge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyrilPeponnet Few comments but, overall, looks good to me :)
Do you think you could add some tests on this?

acme/acme.go Outdated
}

// Check for existing challenge cert
log.Debugf("Checking ACME challenge for %v", domain)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change this debug log to Getting ACME certificate for %s

acme/acme.go Outdated
return
}

// Check if our domains are matching our ingoreFilters
if len(a.IgnoreFilters) > 0 {
Copy link
Member

@emilevauge emilevauge Apr 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to put all the IgnoreFilters code into a function? It seems to look like https://github.com/containous/traefik/pull/1260/files#diff-faa8f9dd50dafacfcb04a8203ce3deebR542 :)

Copy link
Contributor

@vdemeester vdemeester left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM 👼
/cc @containous/traefik for reviews


// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By having this block here, it will never get executed on-demand, since the on demand domain will have already been requested from LE.

I think that this should be modified to check IF match AND not blacklisted.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it will be executed if the ondemand failed to get a cert (due to a filter for instance)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtomcej ping...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyrilPeponnet, the inversion of certificates checkings you want do has a real interest for the use cases described into the PR but it seems to break the backward compatibily.

If it becomes the default behavior, all users will have to specify a IgnoreFilters value.
That's why it may be good to invers the behaviour ONLY IF IgnoreFilters is specified, so users who will not fill the field will keep the current behavior.

In the way to do this, the wildcard certificate checking could be done in a new function, called at the beginning or the end of getCertificate in function of the size of the IgnoreFilters list.

The code should look like this :

func (a *ACME) getWildCardCertificate(domain string) (*tls.Certificate, nool) {
	// 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 {
			log.Debugf("Found a wildcard certificate to use as fallback: %v", k)
			return a.TLSConfig.NameToCertificate[k], true
		}
	}
	return nil, false
}

func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	domain := types.CanonicalDomain(clientHello.ServerName)
	account := a.store.Get().(*Account)

    if a.IgnoreFilters == nil || a.IgnoreFilters.len == 0 {
		if wildcardCert, ok := a.getWildCardCertificate(domain); ok {
			return wildcardCert, nil
		}
	}
    
	// Check for existing challenge cert
	log.Debugf("Checking ACME challenge for %s", domain)
	if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok {
		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("Returning ACME domain cert for %s", domain)
		return domainCert.tlsCert, nil
	}

	if a.OnDemand {
		if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(domain) {
			return nil, nil
		}
		cert, err := a.loadCertificateOnDemand(clientHello)
		if cert != nil && err == nil {
			return cert, nil
		} else if err != nil {
			log.Errorf("On demand certificate retrieval failed for %v due to %v", domain, err)
		}
	}


    if a.IgnoreFilters != nil && a.IgnoreFilters.len > 0 {
		if wildcardCert, ok := a.getWildCardCertificate(domain); ok {
			return wildcardCert, nil
		}
	}
	log.Debugf("No certificate found to return for %s", domain)
	return nil, nil
}

What is your mind about this suggestion?

cc @dtomcej

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the use case... Sorry it as been a while... Can you please explain?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyrilPeponnet, any thoughts on this ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I've been busy this week. Will try to get a better look later on the day.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the issue I tried to address with this PR was also the fact that if I have both a self signed wildcard cert and an ACME cert, only the wildcard will be returned (case 2 and 3 in PR description).

The purpose of the IgnoreFilters was to filter out some (sub)domains for LE certificate generation. (case 1 in PR description).

So to sum up I'm trying to address:

1 - if LE and wildcard are present, return LE
2 - the IgnoreFilters can be used to filter the LE cert generation

I don't think the part 2 is changing any behavior. Part 1 is.

Now I can see 2 uses cases for Part 1.

  • Current behavior, always return wildcard if any
    • if I have a signed wildcard, it makes sense to use it instead of LE cert (but the LE cert will still exists, hidden, and will be renewed and not used).
  • New behavior, return LE over wildcard,
    • make sense if I use a self signed wildcard cert.

I'm not sure on how to address those two cases as they can both exists in the same traefik instance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks for you reply.

As you just said, the Part1 of your response changes deeply the current behavior if the IgnoreFilter field is used to filter domains checked by LE.

But I believe the suggestion we did (use IgnoreFilter to filter (sub)domains checked by a wildcard certificate) allows to respect the 2 use cases you described :

  1. I have a signed wildcard and I want to use it instead of LE certificate : I don't use IgnoreFilter field, the behavior will remain the same as now.
  2. I have a self-signed wildcard and I prefer to use LE for any (all?) (sub)domains : I add the list of (sub)domains which haven't to be checked by the wildcard certificate into the IgnoreFilter field and the LE certificate will be used.

@CyrilPeponnet Are you agree with this?

cc/ @dtomcej @emilevauge @juliens @ldez

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if by doing this it will conflict with the IgnoreFilter used to allow or not LE certificate requests.

Example:

I have *.domain.tld which is self signed. I want fourme.domain.tld to use LE.

If I set the IgnoreFilter to fourme.domain.tld, given your statement:

  • it will not use the self-signed cert.
  • it will not request a LE as per LoadCertificateForDomains func.

The only way to make it works is to define the domain inside the toml configuration (which is not filtered AFAIK).

The main usage for IgnoreFilter was meant to filter out domains for which I don't want any LE generation.

Am I missing something?

Copy link
Member

@emilevauge emilevauge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
Thanks @CyrilPeponnet
Could rebase one more time before merge?

@emilevauge
Copy link
Member

ping @containous/traefik

@ldez ldez added the priority/P1 need to be fixed in next release label Apr 25, 2017
@ldez ldez modified the milestone: 1.4 May 10, 2017
@ldez ldez added kind/enhancement a new or improved feature. and removed priority/P1 need to be fixed in next release status/1-needs-design-review labels May 10, 2017
@ldez ldez changed the title Added IgnoreFilter for ACME domain cert generation Fix #1197 Added IgnoreFilter for ACME domain cert generation May 13, 2017
@emilevauge
Copy link
Member

@nmengin @juliens @ldez

acme/acme.go Outdated
return
}

// Check if our domains are matching our ingoreFilters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ingoreFilters => ignoreFilters

acme/acme.go Outdated
@@ -496,11 +538,18 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C
if certificateResource, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok {
return certificateResource.tlsCert, nil
}

// Check if our domain is matching our ingoreFilters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ingoreFilters => ignoreFilters

# Optional
#
# Example to ignore certificate generation for domains that are not like *.foo.domain.tld
# IngnoreFilters = ['.*\.?[^foo]\.domain\.tld']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IngnoreFilters => IgnoreFilters

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks for spotting those.

@ldez
Copy link
Member

ldez commented Jun 7, 2017

Due to SemaphoreCI, I close and reopen the PR.

@ldez ldez closed this Jun 7, 2017
@ldez ldez reopened this Jun 7, 2017
acme/acme.go Outdated
if cert != nil && err == nil {
return cert, nil
}
log.Errorf("On demand certicate retrival failed for %v due to %v", domain, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

certicate retrival => certificate retrieval

Copy link
Member

@juliens juliens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

acme/acme.go Outdated
if cert != nil && err == nil {
return cert, nil
}
log.Errorf("On demand certificate retrieval failed for %v due to %v", domain, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An error log is created even if err == nil, it should be nice to add a test before to insert the log.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, but, cert can be nil if not cert found without raising an error. Will try to improve the logging.

@nmengin
Copy link
Contributor

nmengin commented Jul 12, 2017

ping @CyrilPeponnet

@nmengin
Copy link
Contributor

nmengin commented Jul 12, 2017

As discussed with @CyrilPeponnet on Slack, the PR is closed for the moment.
A new PR will be open if necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

More granular way to use ACME certs over default cert.
8 participants