Delete TLS-SNI-01 challenge from ACME
This commit is contained in:
parent
d3edccb839
commit
c4529820f2
8 changed files with 61 additions and 230 deletions
11
acme/acme.go
11
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
35
integration/fixtures/acme/no_challenge_acme.toml
Normal file
35
integration/fixtures/acme/no_challenge_acme.toml
Normal 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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue