diff --git a/acme/acme.go b/acme/acme.go index cbf570151..6188fc12a 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -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 { diff --git a/acme/challenge_tls_provider.go b/acme/challenge_tls_provider.go deleted file mode 100644 index ec2d35954..000000000 --- a/acme/challenge_tls_provider.go +++ /dev/null @@ -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) -} diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 67a906fa4..c3364c164 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -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). diff --git a/docs/user-guide/cluster-docker-consul.md b/docs/user-guide/cluster-docker-consul.md index eb506956d..b41534fcc 100644 --- a/docs/user-guide/cluster-docker-consul.md +++ b/docs/user-guide/cluster-docker-consul.md @@ -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 diff --git a/integration/acme_test.go b/integration/acme_test.go index 2f7f3f96b..c1b2b41c8 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -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{ diff --git a/integration/fixtures/acme/no_challenge_acme.toml b/integration/fixtures/acme/no_challenge_acme.toml new file mode 100644 index 000000000..6a728e6f5 --- /dev/null +++ b/integration/fixtures/acme/no_challenge_acme.toml @@ -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" diff --git a/provider/acme/challenge.go b/provider/acme/challenge.go index 91a526d22..0ed638131 100644 --- a/provider/acme/challenge.go +++ b/provider/acme/challenge.go @@ -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 diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 820ad4a8d..321d4a44b 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -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