Delete TLS-SNI-01 challenge from ACME

This commit is contained in:
NicoMen 2018-03-06 14:50:03 +01:00 committed by Traefiker Bot
parent d3edccb839
commit c4529820f2
8 changed files with 61 additions and 230 deletions

View file

@ -36,6 +36,7 @@ var (
)
// ACME allows to connect to lets encrypt and retrieve certs
// Deprecated Please use provider/acme/Provider
type ACME struct {
Email string `description:"Email address used for registration"`
Domains []types.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'"`
@ -53,7 +54,6 @@ type ACME struct {
client *acme.Client
defaultCertificate *tls.Certificate
store cluster.Store
challengeTLSProvider *challengeTLSProvider
challengeHTTPProvider *challengeHTTPProvider
checkOnDemandDomain func(domain string) bool
jobs *channels.InfiniteChannel
@ -159,7 +159,6 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl
}
a.store = datastore
a.challengeTLSProvider = &challengeTLSProvider{store: a.store}
ticker := time.NewTicker(24 * time.Hour)
leadership.Pool.AddGoCtx(func(ctx context.Context) {
@ -249,10 +248,6 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat
return providedCertificate, nil
}
if challengeCert, ok := a.challengeTLSProvider.getCertificate(domain); ok {
log.Debugf("ACME got challenge %s", domain)
return challengeCert, nil
}
if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok {
log.Debugf("ACME got domain cert %s", domain)
return domainCert.tlsCert, nil
@ -431,9 +426,7 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
a.challengeHTTPProvider = &challengeHTTPProvider{store: a.store}
err = client.SetChallengeProvider(acme.HTTP01, a.challengeHTTPProvider)
} else {
log.Debug("Using TLS Challenge provider.")
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeTLSProvider)
return nil, errors.New("ACME challenge not specified, please select HTTP or DNS Challenge")
}
if err != nil {

View file

@ -1,150 +0,0 @@
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"strings"
"sync"
"time"
"github.com/cenk/backoff"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/tls/generate"
"github.com/xenolf/lego/acme"
)
var _ acme.ChallengeProviderTimeout = (*challengeTLSProvider)(nil)
type challengeTLSProvider struct {
store cluster.Store
lock sync.RWMutex
}
func (c *challengeTLSProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
log.Debugf("Looking for an existing ACME challenge for %s...", domain)
if !strings.HasSuffix(domain, ".acme.invalid") {
return nil, false
}
c.lock.RLock()
defer c.lock.RUnlock()
account := c.store.Get().(*Account)
if account.ChallengeCerts == nil {
return nil, false
}
account.Init()
var result *tls.Certificate
operation := func() error {
for _, cert := range account.ChallengeCerts {
for _, dns := range cert.certificate.Leaf.DNSNames {
if domain == dns {
result = cert.certificate
return nil
}
}
}
return fmt.Errorf("cannot find challenge cert for domain %s", domain)
}
notify := func(err error, time time.Duration) {
log.Errorf("Error getting cert: %v, retrying in %s", err, time)
}
ebo := backoff.NewExponentialBackOff()
ebo.MaxElapsedTime = 60 * time.Second
err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
if err != nil {
log.Errorf("Error getting cert: %v", err)
return nil, false
}
return result, true
}
func (c *challengeTLSProvider) Present(domain, token, keyAuth string) error {
log.Debugf("Challenge Present %s", domain)
cert, _, err := tlsSNI01ChallengeCert(keyAuth)
if err != nil {
return err
}
c.lock.Lock()
defer c.lock.Unlock()
transaction, object, err := c.store.Begin()
if err != nil {
return err
}
account := object.(*Account)
if account.ChallengeCerts == nil {
account.ChallengeCerts = map[string]*ChallengeCert{}
}
account.ChallengeCerts[domain] = &cert
return transaction.Commit(account)
}
func (c *challengeTLSProvider) CleanUp(domain, token, keyAuth string) error {
log.Debugf("Challenge CleanUp %s", domain)
c.lock.Lock()
defer c.lock.Unlock()
transaction, object, err := c.store.Begin()
if err != nil {
return err
}
account := object.(*Account)
delete(account.ChallengeCerts, domain)
return transaction.Commit(account)
}
func (c *challengeTLSProvider) Timeout() (timeout, interval time.Duration) {
return 60 * time.Second, 5 * time.Second
}
// tlsSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
func tlsSNI01ChallengeCert(keyAuth string) (ChallengeCert, string, error) {
// generate a new RSA key for the certificates
var tempPrivKey crypto.PrivateKey
tempPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return ChallengeCert{}, "", err
}
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
rsaPrivPEM := pemEncode(rsaPrivKey)
zBytes := sha256.Sum256([]byte(keyAuth))
z := hex.EncodeToString(zBytes[:sha256.Size])
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
tempCertPEM, err := generate.PemCert(rsaPrivKey, domain, time.Time{})
if err != nil {
return ChallengeCert{}, "", err
}
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
if err != nil {
return ChallengeCert{}, "", err
}
return ChallengeCert{Certificate: tempCertPEM, PrivateKey: rsaPrivPEM, certificate: &certificate}, domain, nil
}
func pemEncode(data interface{}) []byte {
var pemBlock *pem.Block
switch key := data.(type) {
case *ecdsa.PrivateKey:
keyBytes, _ := x509.MarshalECPrivateKey(key)
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
case *rsa.PrivateKey:
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
case []byte:
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: data.([]byte)}
}
return pem.EncodeToMemory(pemBlock)
}

View file

@ -38,23 +38,20 @@ storage = "acme.json"
# or `storage = "traefik/acme/account"` if using KV store.
# Entrypoint to proxy acme apply certificates to.
# WARNING, if the TLS-SNI-01 challenge is used, it must point to an entrypoint on port 443
#
# Required
#
entryPoint = "https"
# Use a DNS-01 acme challenge rather than TLS-SNI-01 challenge
# Deprecated, replaced by [acme.dnsChallenge].
#
# Optional (Deprecated, replaced by [acme.dnsChallenge])
# Optional.
#
# dnsProvider = "digitalocean"
# By default, the dnsProvider will verify the TXT DNS challenge record before letting ACME verify.
# If delayDontCheckDNS is greater than zero, avoid this & instead just wait so many seconds.
# Useful if internal networks block external DNS queries.
# Deprecated, replaced by [acme.dnsChallenge.delayBeforeCheck].
#
# Optional (Deprecated, replaced by [acme.dnsChallenge])
# Optional
# Default: 0
#
# delayDontCheckDNS = 0
@ -102,19 +99,19 @@ entryPoint = "https"
# [[acme.domains]]
# main = "local4.com"
# Use a HTTP-01 acme challenge rather than TLS-SNI-01 challenge
# Use a HTTP-01 acme challenge.
#
# Optional but recommend
#
[acme.httpChallenge]
# EntryPoint to use for the challenges.
# EntryPoint to use for the HTTP-01 challenges.
#
# Required
#
entryPoint = "http"
# Use a DNS-01 acme challenge rather than TLS-SNI-01 challenge
# Use a DNS-01 acme challenge rather than HTTP-01 challenge.
#
# Optional
#
@ -137,11 +134,6 @@ entryPoint = "https"
```
!!! note
Even if `TLS-SNI-01` challenge is [disabled](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188) for the moment, it stays the _by default_ ACME Challenge in Træfik.
If `TLS-SNI-01` challenge is not re-enabled in the future, it we will be removed from Træfik.
!!! note
If `TLS-SNI-01` challenge is used, `acme.entryPoint` has to be reachable by Let's Encrypt through the port 443.
If `HTTP-01` challenge is used, `acme.httpChallenge.entryPoint` has to be defined and reachable by Let's Encrypt through the port 80.
These are Let's Encrypt limitations as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72).

View file

@ -11,7 +11,7 @@ When you use Let's Encrypt, you need to store certificates, but not only.
When Træfik generates a new certificate, it configures a challenge and once Let's Encrypt will verify the ownership of the domain, it will ping back the challenge.
If the challenge is not knowing by other Træfik instances, the validation will fail.
For more information about challenge: [Automatic Certificate Management Environment (ACME)](https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni)
For more information about challenge: [Automatic Certificate Management Environment (ACME)](https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http-challenge)
## Prerequisites

View file

@ -32,6 +32,9 @@ const (
// Wildcard domain to check
wildcardDomain = "*.acme.wtf"
// Traefik default certificate
traefikDefaultDomain = "TRAEFIK DEFAULT CERT"
)
func (s *AcmeSuite) SetUpSuite(c *check.C) {
@ -82,6 +85,16 @@ func (s *AcmeSuite) TestACMEProviderOnHost(c *check.C) {
s.retrieveAcmeCertificate(c, testCase)
}
// Test ACME provider with certificate at start and no ACME challenge
func (s *AcmeSuite) TestACMEProviderOnHostWithNoACMEChallenge(c *check.C) {
testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/no_challenge_acme.toml",
onDemand: false,
domainToCheck: traefikDefaultDomain}
s.retrieveAcmeCertificate(c, testCase)
}
// Test OnDemand option with none provided certificate and challenge HTTP-01
func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateHTTP01(c *check.C) {
testCase := AcmeTestCase{

View file

@ -0,0 +1,35 @@
logLevel = "DEBUG"
defaultEntryPoints = ["http", "https"]
[api]
[entryPoints]
[entryPoints.http]
address = ":8081"
[entryPoints.https]
address = ":5001"
[entryPoints.https.tls]
[acme]
email = "test@traefik.io"
storage = "/dev/null"
entryPoint = "https"
OnHostRule = true
caServer = "http://{{.BoulderHost}}:4000/directory"
# No challenge defined
[file]
[backends]
[backends.backend]
[backends.backend.servers.server1]
url = "http://127.0.0.1:9010"
[frontends]
[frontends.frontend]
backend = "backend"
[frontends.frontend.routes.test]
rule = "Host:traefik.acme.wtf"

View file

@ -1,11 +1,6 @@
package acme
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
@ -13,7 +8,6 @@ import (
"github.com/containous/flaeg"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
tlsgenerate "github.com/containous/traefik/tls/generate"
"github.com/xenolf/lego/acme"
)
@ -35,30 +29,6 @@ func dnsOverrideDelay(delay flaeg.Duration) error {
return nil
}
func presentTLSChallenge(domain, keyAuth string) ([]byte, []byte, error) {
log.Debugf("TLS Challenge Present temp certificate for %s", domain)
var tempPrivKey crypto.PrivateKey
tempPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
rsaPrivPEM := tlsgenerate.PemEncode(rsaPrivKey)
zBytes := sha256.Sum256([]byte(keyAuth))
z := hex.EncodeToString(zBytes[:sha256.Size])
domainCert := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
tempCertPEM, err := tlsgenerate.PemCert(rsaPrivKey, domainCert, time.Time{})
if err != nil {
return nil, nil, err
}
return tempCertPEM, rsaPrivPEM, nil
}
func getTokenValue(token, domain string, store Store) []byte {
log.Debugf("Looking for an existing ACME challenge for token %v...", token)
var result []byte

View file

@ -323,12 +323,7 @@ func (p *Provider) getClient() (*acme.Client, error) {
return nil, err
}
} else {
log.Debug("Using TLS Challenge provider.")
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
err = client.SetChallengeProvider(acme.TLSSNI01, p)
if err != nil {
return nil, err
}
return nil, errors.New("ACME challenge not specified, please select HTTP or DNS Challenge")
}
p.client = client
}
@ -338,29 +333,12 @@ func (p *Provider) getClient() (*acme.Client, error) {
// Present presents a challenge to obtain new ACME certificate
func (p *Provider) Present(domain, token, keyAuth string) error {
if p.HTTPChallenge != nil {
return presentHTTPChallenge(domain, token, keyAuth, p.Store)
} else if p.DNSChallenge == nil {
log.Debugf("TLS Challenge CleanUp temp certificate for %s", domain)
tempCertPEM, rsaPrivPEM, err := presentTLSChallenge(domain, keyAuth)
if err != nil {
return err
}
p.addCertificateForDomain(types.Domain{Main: "TEMP-" + domain}, tempCertPEM, rsaPrivPEM)
}
return nil
return presentHTTPChallenge(domain, token, keyAuth, p.Store)
}
// CleanUp cleans the challenges when certificate is obtained
func (p *Provider) CleanUp(domain, token, keyAuth string) error {
if p.HTTPChallenge != nil {
return cleanUpHTTPChallenge(domain, token, p.Store)
} else if p.DNSChallenge == nil {
log.Debugf("TLS Challenge CleanUp temp certificate for %s", domain)
p.deleteCertificateForDomain(types.Domain{Main: "TEMP-" + domain})
}
return nil
return cleanUpHTTPChallenge(domain, token, p.Store)
}
// Provide allows the file provider to provide configurations to traefik