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 // ACME allows to connect to lets encrypt and retrieve certs
// Deprecated Please use provider/acme/Provider
type ACME struct { type ACME struct {
Email string `description:"Email address used for registration"` 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'"` 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 client *acme.Client
defaultCertificate *tls.Certificate defaultCertificate *tls.Certificate
store cluster.Store store cluster.Store
challengeTLSProvider *challengeTLSProvider
challengeHTTPProvider *challengeHTTPProvider challengeHTTPProvider *challengeHTTPProvider
checkOnDemandDomain func(domain string) bool checkOnDemandDomain func(domain string) bool
jobs *channels.InfiniteChannel jobs *channels.InfiniteChannel
@ -159,7 +159,6 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl
} }
a.store = datastore a.store = datastore
a.challengeTLSProvider = &challengeTLSProvider{store: a.store}
ticker := time.NewTicker(24 * time.Hour) ticker := time.NewTicker(24 * time.Hour)
leadership.Pool.AddGoCtx(func(ctx context.Context) { leadership.Pool.AddGoCtx(func(ctx context.Context) {
@ -249,10 +248,6 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat
return providedCertificate, nil 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 { if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok {
log.Debugf("ACME got domain cert %s", domain) log.Debugf("ACME got domain cert %s", domain)
return domainCert.tlsCert, nil return domainCert.tlsCert, nil
@ -431,9 +426,7 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
a.challengeHTTPProvider = &challengeHTTPProvider{store: a.store} a.challengeHTTPProvider = &challengeHTTPProvider{store: a.store}
err = client.SetChallengeProvider(acme.HTTP01, a.challengeHTTPProvider) err = client.SetChallengeProvider(acme.HTTP01, a.challengeHTTPProvider)
} else { } else {
log.Debug("Using TLS Challenge provider.") return nil, errors.New("ACME challenge not specified, please select HTTP or DNS Challenge")
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeTLSProvider)
} }
if err != nil { 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. # or `storage = "traefik/acme/account"` if using KV store.
# Entrypoint to proxy acme apply certificates to. # 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 # Required
# #
entryPoint = "https" 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" # dnsProvider = "digitalocean"
# By default, the dnsProvider will verify the TXT DNS challenge record before letting ACME verify. # Deprecated, replaced by [acme.dnsChallenge.delayBeforeCheck].
# If delayDontCheckDNS is greater than zero, avoid this & instead just wait so many seconds.
# Useful if internal networks block external DNS queries.
# #
# Optional (Deprecated, replaced by [acme.dnsChallenge]) # Optional
# Default: 0 # Default: 0
# #
# delayDontCheckDNS = 0 # delayDontCheckDNS = 0
@ -102,19 +99,19 @@ entryPoint = "https"
# [[acme.domains]] # [[acme.domains]]
# main = "local4.com" # main = "local4.com"
# Use a HTTP-01 acme challenge rather than TLS-SNI-01 challenge # Use a HTTP-01 acme challenge.
# #
# Optional but recommend # Optional but recommend
# #
[acme.httpChallenge] [acme.httpChallenge]
# EntryPoint to use for the challenges. # EntryPoint to use for the HTTP-01 challenges.
# #
# Required # Required
# #
entryPoint = "http" 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 # Optional
# #
@ -137,11 +134,6 @@ entryPoint = "https"
``` ```
!!! note !!! 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. 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). 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. 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. 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 ## Prerequisites

View file

@ -32,6 +32,9 @@ const (
// Wildcard domain to check // Wildcard domain to check
wildcardDomain = "*.acme.wtf" wildcardDomain = "*.acme.wtf"
// Traefik default certificate
traefikDefaultDomain = "TRAEFIK DEFAULT CERT"
) )
func (s *AcmeSuite) SetUpSuite(c *check.C) { func (s *AcmeSuite) SetUpSuite(c *check.C) {
@ -82,6 +85,16 @@ func (s *AcmeSuite) TestACMEProviderOnHost(c *check.C) {
s.retrieveAcmeCertificate(c, testCase) 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 // Test OnDemand option with none provided certificate and challenge HTTP-01
func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateHTTP01(c *check.C) { func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateHTTP01(c *check.C) {
testCase := AcmeTestCase{ 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 package acme
import ( import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"time" "time"
@ -13,7 +8,6 @@ import (
"github.com/containous/flaeg" "github.com/containous/flaeg"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
tlsgenerate "github.com/containous/traefik/tls/generate"
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
) )
@ -35,30 +29,6 @@ func dnsOverrideDelay(delay flaeg.Duration) error {
return nil 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 { func getTokenValue(token, domain string, store Store) []byte {
log.Debugf("Looking for an existing ACME challenge for token %v...", token) log.Debugf("Looking for an existing ACME challenge for token %v...", token)
var result []byte var result []byte

View file

@ -323,12 +323,7 @@ func (p *Provider) getClient() (*acme.Client, error) {
return nil, err return nil, err
} }
} else { } else {
log.Debug("Using TLS Challenge provider.") return nil, errors.New("ACME challenge not specified, please select HTTP or DNS Challenge")
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
err = client.SetChallengeProvider(acme.TLSSNI01, p)
if err != nil {
return nil, err
}
} }
p.client = client p.client = client
} }
@ -338,29 +333,12 @@ func (p *Provider) getClient() (*acme.Client, error) {
// Present presents a challenge to obtain new ACME certificate // Present presents a challenge to obtain new ACME certificate
func (p *Provider) Present(domain, token, keyAuth string) error { func (p *Provider) Present(domain, token, keyAuth string) error {
if p.HTTPChallenge != nil { return presentHTTPChallenge(domain, token, keyAuth, p.Store)
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
} }
// CleanUp cleans the challenges when certificate is obtained // CleanUp cleans the challenges when certificate is obtained
func (p *Provider) CleanUp(domain, token, keyAuth string) error { func (p *Provider) CleanUp(domain, token, keyAuth string) error {
if p.HTTPChallenge != nil { return cleanUpHTTPChallenge(domain, token, p.Store)
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
} }
// Provide allows the file provider to provide configurations to traefik // Provide allows the file provider to provide configurations to traefik