From d9ffc390758a516422d52f8140691d3d31b3d1a4 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Mon, 21 Mar 2016 11:10:18 +0100 Subject: [PATCH] add acme package, refactor acme as resuable API Signed-off-by: Emile Vauge --- acme.go | 337 -------------------------------- acme/acme.go | 401 ++++++++++++++++++++++++++++++++++++++ acme/challengeProvider.go | 56 ++++++ acme/crypto.go | 78 ++++++++ configuration.go | 21 +- docs/index.md | 14 +- server.go | 49 +++-- traefik.sample.toml | 4 +- 8 files changed, 577 insertions(+), 383 deletions(-) delete mode 100644 acme.go create mode 100644 acme/acme.go create mode 100644 acme/challengeProvider.go create mode 100644 acme/crypto.go diff --git a/acme.go b/acme.go deleted file mode 100644 index d33fced92..000000000 --- a/acme.go +++ /dev/null @@ -1,337 +0,0 @@ -/* -Copyright -*/ -package main - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/json" - "errors" - "fmt" - log "github.com/Sirupsen/logrus" - "github.com/containous/traefik/middlewares" - "github.com/gorilla/mux" - "github.com/xenolf/lego/acme" - "io/ioutil" - fmtlog "log" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "time" -) - -// ACMEAccount is used to store lets encrypt registration info -type ACMEAccount struct { - Email string - Registration *acme.RegistrationResource - PrivateKey []byte - CertificatesMap DomainsCertificates -} - -// DomainsCertificates stores a certificate for multiple domains -type DomainsCertificates []DomainsCertificate - -func (dc DomainsCertificates) getCertificateForDomain(domainToFind string) (*AcmeCertificate, bool) { - for _, domainsCertificate := range dc { - for _, domain := range domainsCertificate.Domains { - if domain == domainToFind { - return domainsCertificate.Certificate, true - } - } - } - return nil, false -} - -// DomainsCertificate contains a certificate for multiple domains -type DomainsCertificate struct { - Domains []string - Certificate *AcmeCertificate -} - -// GetEmail returns email -func (a ACMEAccount) GetEmail() string { - return a.Email -} - -// GetRegistration returns lets encrypt registration resource -func (a ACMEAccount) GetRegistration() *acme.RegistrationResource { - return a.Registration -} - -// GetPrivateKey returns private key -func (a ACMEAccount) GetPrivateKey() crypto.PrivateKey { - if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil { - return privateKey - } - log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey) - return nil -} - -// AcmeCertificate is used to store certificate info -type AcmeCertificate struct { - Domain string - CertURL string - CertStableURL string - PrivateKey []byte - Certificate []byte -} - -func (a *ACME) createACMEConfig(router *middlewares.HandlerSwitcher, proxyRouter *middlewares.HandlerSwitcher) (*tls.Config, error) { - acme.Logger = fmtlog.New(ioutil.Discard, "", 0) - - if len(a.StorageFile) == 0 { - return nil, errors.New("Empty StorageFile, please provide a filenmae for certs storage") - } - - // if certificates in storage, load them - if fileInfo, err := os.Stat(a.StorageFile); err == nil && fileInfo.Size() != 0 { - // load account - acmeAccount, err := a.loadACMEAccount(a) - if err != nil { - return nil, err - } - - // build client - client, err := a.buildACMEClient(acmeAccount) - if err != nil { - return nil, err - } - config := &tls.Config{} - config.Certificates = []tls.Certificate{} - for _, certificateResource := range acmeAccount.CertificatesMap { - cert, err := tls.X509KeyPair(certificateResource.Certificate.Certificate, certificateResource.Certificate.PrivateKey) - if err != nil { - return nil, err - } - leaf, err := x509.ParseCertificate(cert.Certificate[0]) - if err != nil { - return nil, err - } - // <= 30 days left, renew certificate - if leaf.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) { - renewedCert, err := client.RenewCertificate(acme.CertificateResource{ - Domain: certificateResource.Certificate.Domain, - CertURL: certificateResource.Certificate.CertURL, - CertStableURL: certificateResource.Certificate.CertStableURL, - PrivateKey: certificateResource.Certificate.PrivateKey, - Certificate: certificateResource.Certificate.Certificate, - }, false) - if err != nil { - return nil, err - } - log.Debugf("Renewed certificate %s", renewedCert.Domain) - certificateResource.Certificate = &AcmeCertificate{ - Domain: renewedCert.Domain, - CertURL: renewedCert.CertURL, - CertStableURL: renewedCert.CertStableURL, - PrivateKey: renewedCert.PrivateKey, - Certificate: renewedCert.Certificate, - } - if err = a.saveACMEAccount(acmeAccount); err != nil { - return nil, err - } - cert, err = tls.X509KeyPair(renewedCert.Certificate, renewedCert.PrivateKey) - if err != nil { - return nil, err - } - } - config.Certificates = append(config.Certificates, cert) - } - config.BuildNameToCertificate() - if a.OnDemand { - config.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - if !router.GetHandler().Match(&http.Request{URL: &url.URL{}, Host: clientHello.ServerName}, &mux.RouteMatch{}) { - return nil, nil - } - return a.loadCertificateOnDemand(client, acmeAccount, clientHello, proxyRouter) - } - } - return config, nil - } - log.Infof("Loading ACME certificates...") - - // Create a user. New accounts need an email and private key to start - privateKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - return nil, err - } - acmeAccount := &ACMEAccount{ - Email: a.Email, - PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), - } - - client, err := a.buildACMEClient(acmeAccount) - if err != nil { - return nil, err - } - - //client.SetTLSAddress(acmeConfig.TLSAddress) - // New users will need to register; be sure to save it - reg, err := client.Register() - if err != nil { - return nil, err - } - acmeAccount.Registration = reg - - // The client has a URL to the current Let's Encrypt Subscriber - // Agreement. The user will need to agree to it. - err = client.AgreeToTOS() - if err != nil { - return nil, err - } - - config := &tls.Config{} - config.Certificates = []tls.Certificate{} - acmeAccount.CertificatesMap = []DomainsCertificate{} - - for _, domain := range a.Domains { - domains := append([]string{domain.Main}, domain.SANs...) - certificateResource, err := a.getDomainsCertificates(client, domains, proxyRouter) - if err != nil { - return nil, err - } - cert, err := tls.X509KeyPair(certificateResource.Certificate, certificateResource.PrivateKey) - if err != nil { - return nil, err - } - config.Certificates = append(config.Certificates, cert) - acmeAccount.CertificatesMap = append(acmeAccount.CertificatesMap, DomainsCertificate{Domains: domains, Certificate: certificateResource}) - } - // BuildNameToCertificate parses the CommonName and SubjectAlternateName fields - // in each certificate and populates the config.NameToCertificate map. - config.BuildNameToCertificate() - if a.OnDemand { - config.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - if !router.GetHandler().Match(&http.Request{URL: &url.URL{}, Host: clientHello.ServerName}, &mux.RouteMatch{}) { - return nil, nil - } - return a.loadCertificateOnDemand(client, acmeAccount, clientHello, proxyRouter) - } - } - if err = a.saveACMEAccount(acmeAccount); err != nil { - return nil, err - } - return config, nil -} - -func (a *ACME) buildACMEClient(acmeAccount *ACMEAccount) (*acme.Client, error) { - - // A client facilitates communication with the CA server. This CA URL is - // configured for a local dev instance of Boulder running in Docker in a VM. - caServer := "https://acme-v01.api.letsencrypt.org/directory" - if len(a.CAServer) > 0 { - caServer = a.CAServer - } - client, err := acme.NewClient(caServer, acmeAccount, acme.RSA4096) - if err != nil { - return nil, err - } - - return client, nil -} - -// Ask the kernel for a free open port that is ready to use -func (a *ACME) getFreePort() (string, error) { - addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") - if err != nil { - return "", err - } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return "", err - } - defer l.Close() - return l.Addr().String(), nil -} - -func (a *ACME) loadCertificateOnDemand(client *acme.Client, acmeAccount *ACMEAccount, clientHello *tls.ClientHelloInfo, proxyRouter *middlewares.HandlerSwitcher) (*tls.Certificate, error) { - if certificateResource, ok := acmeAccount.CertificatesMap.getCertificateForDomain(clientHello.ServerName); ok { - cert, err := tls.X509KeyPair(certificateResource.Certificate, certificateResource.PrivateKey) - if err != nil { - return nil, err - } - return &cert, nil - } - certificateResource, err := a.getDomainsCertificates(client, []string{clientHello.ServerName}, proxyRouter) - if err != nil { - return nil, err - } - log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName) - acmeAccount.CertificatesMap = append(acmeAccount.CertificatesMap, DomainsCertificate{Domains: []string{clientHello.ServerName}, Certificate: certificateResource}) - if err = a.saveACMEAccount(acmeAccount); err != nil { - return nil, err - } - cert, err := tls.X509KeyPair(certificateResource.Certificate, certificateResource.PrivateKey) - if err != nil { - return nil, err - } - return &cert, nil -} - -func (a *ACME) loadACMEAccount(acmeConfig *ACME) (*ACMEAccount, error) { - a.storageLock.Lock() - defer a.storageLock.Unlock() - acmeAccount := ACMEAccount{ - CertificatesMap: DomainsCertificates{}, - } - file, err := ioutil.ReadFile(acmeConfig.StorageFile) - if err != nil { - return nil, err - } - if err := json.Unmarshal(file, &acmeAccount); err != nil { - return nil, err - } - log.Infof("Loaded ACME config from storage %s", acmeConfig.StorageFile) - return &acmeAccount, nil -} - -func (a *ACME) saveACMEAccount(acmeAccount *ACMEAccount) error { - a.storageLock.Lock() - defer a.storageLock.Unlock() - // write account to file - data, err := json.MarshalIndent(acmeAccount, "", " ") - if err != nil { - return err - } - return ioutil.WriteFile(a.StorageFile, data, 0644) -} - -func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string, proxyRouter *middlewares.HandlerSwitcher) (*AcmeCertificate, error) { - var proxyRoute *mux.Route - proxyRoute = proxyRouter.GetHandler().Get("9141156b44763db2a504b8c63cf6f81c") - if proxyRoute == nil { - proxyRoute = proxyRouter.GetHandler().NewRoute().PathPrefix("/.well-known/acme-challenge/").Name("9141156b44763db2a504b8c63cf6f81c") - } - url, err := url.Parse("http://127.0.0.1:5002") - if err != nil { - return nil, err - } - reverseProxy := httputil.NewSingleHostReverseProxy(url) - proxyRoute.Handler(reverseProxy) - defer proxyRoute.Handler(http.NotFoundHandler()) - // The acme library takes care of completing the challenges to obtain the certificate(s). - // Of course, the hostnames must resolve to this machine or it will fail. - log.Debugf("Loading ACME certificates %s", domains) - bundle := false - client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) - client.SetHTTPAddress("127.0.0.1:5002") - certificate, failures := client.ObtainCertificate(domains, bundle, nil) - if len(failures) > 0 { - log.Error(failures) - return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) - } - return &AcmeCertificate{ - Domain: certificate.Domain, - CertURL: certificate.CertURL, - CertStableURL: certificate.CertStableURL, - PrivateKey: certificate.PrivateKey, - Certificate: certificate.Certificate, - }, nil -} diff --git a/acme/acme.go b/acme/acme.go new file mode 100644 index 000000000..677401111 --- /dev/null +++ b/acme/acme.go @@ -0,0 +1,401 @@ +package acme + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/xenolf/lego/acme" + "io/ioutil" + fmtlog "log" + "os" + "reflect" + "sync" + "time" +) + +// Account is used to store lets encrypt registration info +type Account struct { + Email string + Registration *acme.RegistrationResource + PrivateKey []byte + DomainsCertificate DomainsCertificates +} + +// DomainsCertificates stores a certificate for multiple domains +type DomainsCertificates struct { + Certs []*DomainsCertificate + lock *sync.RWMutex +} + +func (dc *DomainsCertificates) init() error { + if dc.lock == nil { + dc.lock = &sync.RWMutex{} + } + dc.lock.Lock() + defer dc.lock.Unlock() + for _, domainsCertificate := range dc.Certs { + tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey) + if err != nil { + return err + } + domainsCertificate.tlsCert = &tlsCert + } + return nil +} + +func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error { + dc.lock.Lock() + defer dc.lock.Unlock() + + for _, domainsCertificate := range dc.Certs { + if reflect.DeepEqual(domain, domainsCertificate.Domains) { + domainsCertificate.Certificate = acmeCert + tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey) + if err != nil { + return err + } + domainsCertificate.tlsCert = &tlsCert + return nil + } + } + return errors.New("Certificate to renew to found from domain " + domain.Main) +} + +func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) { + dc.lock.Lock() + defer dc.lock.Unlock() + + tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey) + if err != nil { + return nil, err + } + cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert} + dc.Certs = append(dc.Certs, &cert) + return &cert, nil +} + +func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) { + dc.lock.RLock() + defer dc.lock.RUnlock() + for _, domainsCertificate := range dc.Certs { + domains := append([]string{domainsCertificate.Domains.Main}, domainsCertificate.Domains.SANs...) + for _, domain := range domains { + if domain == domainToFind { + return domainsCertificate, true + } + } + } + return nil, false +} + +func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) { + dc.lock.RLock() + defer dc.lock.RUnlock() + for _, domainsCertificate := range dc.Certs { + if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) { + return domainsCertificate, true + } + } + return nil, false +} + +// DomainsCertificate contains a certificate for multiple domains +type DomainsCertificate struct { + Domains Domain + Certificate *Certificate + tlsCert *tls.Certificate +} + +// GetEmail returns email +func (a Account) GetEmail() string { + return a.Email +} + +// GetRegistration returns lets encrypt registration resource +func (a Account) GetRegistration() *acme.RegistrationResource { + return a.Registration +} + +// GetPrivateKey returns private key +func (a Account) GetPrivateKey() crypto.PrivateKey { + if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil { + return privateKey + } + log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey) + return nil +} + +// Certificate is used to store certificate info +type Certificate struct { + Domain string + CertURL string + CertStableURL string + PrivateKey []byte + Certificate []byte +} + +// ACME allows to connect to lets encrypt and retrieve certs +type ACME struct { + Email string + Domains []Domain + StorageFile string + OnDemand bool + CAServer string + EntryPoint string + storageLock sync.RWMutex +} + +// Domain holds a domain name with SANs +type Domain struct { + Main string + SANs []string +} + +// CreateACMEConfig creates a tls.config from using ACME configuration +func (a *ACME) CreateACMEConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error { + acme.Logger = fmtlog.New(ioutil.Discard, "", 0) + // TODO: generate default cert if empty + + if len(a.StorageFile) == 0 { + return errors.New("Empty StorageFile, please provide a filenmae for certs storage") + } + + log.Debugf("Generating default certificate...") + if len(tlsConfig.Certificates) == 0 { + // no certificates in TLS config, so we add a default one + cert, err := generateDefaultCertificate() + if err != nil { + return err + } + tlsConfig.Certificates = append(tlsConfig.Certificates, *cert) + } + var account *Account + var needRegister bool + + // if certificates in storage, load them + if fileInfo, err := os.Stat(a.StorageFile); err == nil && fileInfo.Size() != 0 { + log.Infof("Loading ACME certificates...") + // load account + account, err = a.loadAccount(a) + if err != nil { + return err + } + } else { + log.Infof("Generating ACME Account...") + // Create a user. New accounts need an email and private key to start + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return err + } + account = &Account{ + Email: a.Email, + PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), + } + account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}} + needRegister = true + } + + client, err := a.buildACMEClient(account) + if err != nil { + return err + } + client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01}) + wrapperChallengeProvider := newWrapperChallengeProvider() + client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider) + + if needRegister { + // New users will need to register; be sure to save it + reg, err := client.Register() + if err != nil { + return err + } + account.Registration = reg + } + + // The client has a URL to the current Let's Encrypt Subscriber + // Agreement. The user will need to agree to it. + err = client.AgreeToTOS() + if err != nil { + return err + } + + go func() { + log.Infof("Retrieving ACME certificates...") + for _, domain := range a.Domains { + // check if cert isn't already loaded + if _, exists := account.DomainsCertificate.exists(domain); !exists { + domains := append([]string{domain.Main}, domain.SANs...) + certificateResource, err := a.getDomainsCertificates(client, domains) + if err != nil { + log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error()) + } + _, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain) + if err != nil { + log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error()) + } + if err = a.saveAccount(account); err != nil { + log.Errorf("Error Saving ACME account %+v: %s", account, err.Error()) + } + } + } + log.Infof("Retrieved ACME certificates") + }() + + tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if challengeCert, ok := wrapperChallengeProvider.getCertificate(clientHello.ServerName); ok { + return challengeCert, nil + } + if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { + return domainCert.tlsCert, nil + } + if a.OnDemand { + if CheckOnDemandDomain != nil && !CheckOnDemandDomain(clientHello.ServerName) { + return nil, nil + } + return a.loadCertificateOnDemand(client, account, clientHello) + } + return nil, nil + } + + ticker := time.NewTicker(24 * time.Hour) + go func() { + time.Sleep(24 * time.Hour) + for { + select { + case <-ticker.C: + + if err := a.renewCertificates(client, account); err != nil { + log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error()) + } + } + } + + }() + return nil +} + +func (a *ACME) renewCertificates(client *acme.Client, Account *Account) error { + for _, certificateResource := range Account.DomainsCertificate.Certs { + // <= 7 days left, renew certificate + if certificateResource.tlsCert.Leaf.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) { + log.Debugf("Renewing certificate %+v", certificateResource.Domains) + renewedCert, err := client.RenewCertificate(acme.CertificateResource{ + Domain: certificateResource.Certificate.Domain, + CertURL: certificateResource.Certificate.CertURL, + CertStableURL: certificateResource.Certificate.CertStableURL, + PrivateKey: certificateResource.Certificate.PrivateKey, + Certificate: certificateResource.Certificate.Certificate, + }, false) + if err != nil { + return err + } + log.Debugf("Renewed certificate %+v", certificateResource.Domains) + renewedACMECert := &Certificate{ + Domain: renewedCert.Domain, + CertURL: renewedCert.CertURL, + CertStableURL: renewedCert.CertStableURL, + PrivateKey: renewedCert.PrivateKey, + Certificate: renewedCert.Certificate, + } + err = Account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains) + if err != nil { + return err + } + if err = a.saveAccount(Account); err != nil { + return err + } + } + } + return nil +} + +func (a *ACME) buildACMEClient(Account *Account) (*acme.Client, error) { + + // A client facilitates communication with the CA server. This CA URL is + // configured for a local dev instance of Boulder running in Docker in a VM. + caServer := "https://acme-v01.api.letsencrypt.org/directory" + if len(a.CAServer) > 0 { + caServer = a.CAServer + } + client, err := acme.NewClient(caServer, Account, acme.RSA4096) + if err != nil { + return nil, err + } + + return client, nil +} + +func (a *ACME) loadCertificateOnDemand(client *acme.Client, Account *Account, clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if certificateResource, ok := Account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok { + return certificateResource.tlsCert, nil + } + Certificate, err := a.getDomainsCertificates(client, []string{clientHello.ServerName}) + if err != nil { + return nil, err + } + log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName) + cert, err := Account.DomainsCertificate.addCertificateForDomains(Certificate, Domain{Main: clientHello.ServerName}) + if err != nil { + return nil, err + } + if err = a.saveAccount(Account); err != nil { + return nil, err + } + return cert.tlsCert, nil +} + +func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, error) { + a.storageLock.RLock() + defer a.storageLock.RUnlock() + Account := Account{ + DomainsCertificate: DomainsCertificates{}, + } + file, err := ioutil.ReadFile(acmeConfig.StorageFile) + if err != nil { + return nil, err + } + if err := json.Unmarshal(file, &Account); err != nil { + return nil, err + } + err = Account.DomainsCertificate.init() + if err != nil { + return nil, err + } + log.Infof("Loaded ACME config from storage %s", acmeConfig.StorageFile) + return &Account, nil +} + +func (a *ACME) saveAccount(Account *Account) error { + a.storageLock.Lock() + defer a.storageLock.Unlock() + // write account to file + data, err := json.MarshalIndent(Account, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(a.StorageFile, data, 0644) +} + +func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) { + log.Debugf("Loading ACME certificates %s...", domains) + bundle := false + certificate, failures := client.ObtainCertificate(domains, bundle, nil) + if len(failures) > 0 { + log.Error(failures) + return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) + } + log.Debugf("Loaded ACME certificates %s", domains) + return &Certificate{ + Domain: certificate.Domain, + CertURL: certificate.CertURL, + CertStableURL: certificate.CertStableURL, + PrivateKey: certificate.PrivateKey, + Certificate: certificate.Certificate, + }, nil +} diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go new file mode 100644 index 000000000..8bdda2673 --- /dev/null +++ b/acme/challengeProvider.go @@ -0,0 +1,56 @@ +package acme + +import ( + "crypto/tls" + "sync" + + "crypto/x509" + "github.com/xenolf/lego/acme" +) + +type wrapperChallengeProvider struct { + challengeCerts map[string]*tls.Certificate + lock sync.RWMutex +} + +func newWrapperChallengeProvider() *wrapperChallengeProvider { + return &wrapperChallengeProvider{ + challengeCerts: map[string]*tls.Certificate{}, + } +} + +func (c *wrapperChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { + c.lock.RLock() + defer c.lock.RUnlock() + if cert, ok := c.challengeCerts[domain]; ok { + return cert, true + } + return nil, false +} + +func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error { + cert, err := acme.TLSSNI01ChallengeCert(keyAuth) + if err != nil { + return err + } + cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return err + } + + c.lock.Lock() + defer c.lock.Unlock() + for i := range cert.Leaf.DNSNames { + c.challengeCerts[cert.Leaf.DNSNames[i]] = &cert + } + + return nil + +} + +func (c *wrapperChallengeProvider) CleanUp(domain, token, keyAuth string) error { + c.lock.Lock() + defer c.lock.Unlock() + delete(c.challengeCerts, domain) + return nil +} diff --git a/acme/crypto.go b/acme/crypto.go new file mode 100644 index 000000000..6fa544b70 --- /dev/null +++ b/acme/crypto.go @@ -0,0 +1,78 @@ +package acme + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "fmt" + "math/big" + "time" +) + +func generateDefaultCertificate() (*tls.Certificate, error) { + rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + rsaPrivPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)}) + + randomBytes := make([]byte, 100) + _, err = rand.Read(randomBytes) + if err != nil { + return nil, err + } + zBytes := sha256.Sum256(randomBytes) + z := hex.EncodeToString(zBytes[:sha256.Size]) + domain := fmt.Sprintf("%s.%s.traefik.default", z[:32], z[32:]) + tempCertPEM, err := generatePemCert(rsaPrivKey, domain) + if err != nil { + return nil, err + } + + certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) + if err != nil { + return nil, err + } + + return &certificate, nil +} +func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { + derBytes, err := generateDerCert(privKey, time.Time{}, domain) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil +} + +func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + if expiration.IsZero() { + expiration = time.Now().Add(365) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "TRAEFIK DEFAULT CERT", + }, + NotBefore: time.Now(), + NotAfter: expiration, + + KeyUsage: x509.KeyUsageKeyEncipherment, + BasicConstraintsValid: true, + DNSNames: []string{domain}, + } + + return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) +} diff --git a/configuration.go b/configuration.go index 329cb3c25..16cb42157 100644 --- a/configuration.go +++ b/configuration.go @@ -8,11 +8,11 @@ import ( "strings" "time" + "github.com/containous/traefik/acme" "github.com/containous/traefik/provider" "github.com/containous/traefik/types" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" - "sync" ) // GlobalConfiguration holds global configuration (with providers, etc.). @@ -23,7 +23,7 @@ type GlobalConfiguration struct { TraefikLogsFile string LogLevel string EntryPoints EntryPoints - ACME *ACME + ACME *acme.ACME DefaultEntryPoints DefaultEntryPoints ProvidersThrottleDuration time.Duration MaxIdleConnsPerHost int @@ -142,23 +142,6 @@ type TLS struct { Certificates Certificates } -// ACME allows to connect to lets encrypt and retrieve certs -type ACME struct { - Email string - Domains []Domain - StorageFile string - OnDemand bool - CAServer string - EntryPoint string - storageLock sync.Mutex -} - -// Domain holds a domain name with SANs -type Domain struct { - Main string - SANs []string -} - // Certificates defines traefik certificates type type Certificates []Certificate diff --git a/docs/index.md b/docs/index.md index 55fe6eb8a..2a6ce76b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -255,11 +255,11 @@ Use "traefik [command] --help" for more information about a command. # storageFile = "acme.json" # Entrypoint to proxy acme challenge to. -# WARNING, must point to an entrypoint on port 80 +# WARNING, must point to an entrypoint on port 443 # # Required # -# entryPoint = "http" +# entryPoint = "https" # Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate. # WARNING, TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks. @@ -377,19 +377,19 @@ defaultEntryPoints = ["http", "https"] ``` [entryPoints] - [entryPoints.http] - address = ":80" - [entryPoints.http.redirect] - entryPoint = "https" [entryPoints.https] address = ":443" [entryPoints.https.tls] + # certs used as default certs + [[entryPoints.https.tls.certificates]] + certFile = "tests/traefik.crt" + keyFile = "tests/traefik.key" [acme] email = "test@traefik.io" storageFile = "acme.json" onDemand = true caServer = "http://172.18.0.1:4000/directory" -entryPoint = "http" +entryPoint = "https" [[acme.domains]] main = "local1.com" diff --git a/server.go b/server.go index 1e5f2b796..7abd40a57 100644 --- a/server.go +++ b/server.go @@ -102,7 +102,7 @@ func (server *Server) Close() { func (server *Server) startHTTPServers() { server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration) for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints { - newsrv, err := server.prepareServer(newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, server.loggerMiddleware, metrics) + newsrv, err := server.prepareServer(newServerEntryPointName, newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, server.loggerMiddleware, metrics) if err != nil { log.Fatal("Error preparing server: ", err) } @@ -224,28 +224,41 @@ func (server *Server) listenSignals() { } // creates a TLS config that allows terminating HTTPS for multiple domains using SNI -func (server *Server) createTLSConfig(tlsOption *TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) { +func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) { if tlsOption == nil { return nil, nil } - if server.globalConfiguration.ACME != nil { - if acmeEntrypoint, ok := server.serverEntryPoints[server.globalConfiguration.ACME.EntryPoint]; ok { - return server.globalConfiguration.ACME.createACMEConfig(router, acmeEntrypoint.httpRouter) - } - return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + "for ACME configuration") - } - if len(tlsOption.Certificates) == 0 { - return nil, nil - } config := &tls.Config{} - var err error - config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates)) - for i, v := range tlsOption.Certificates { - config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile) + config.Certificates = []tls.Certificate{} + for _, v := range tlsOption.Certificates { + cert, err := tls.LoadX509KeyPair(v.CertFile, v.KeyFile) if err != nil { return nil, err } + config.Certificates = append(config.Certificates, cert) + } + + if server.globalConfiguration.ACME != nil { + if _, ok := server.serverEntryPoints[server.globalConfiguration.ACME.EntryPoint]; ok { + if entryPointName == server.globalConfiguration.ACME.EntryPoint { + checkOnDemandDomain := func(domain string) bool { + if router.GetHandler().Match(&http.Request{URL: &url.URL{}, Host: domain}, &mux.RouteMatch{}) { + return true + } + return false + } + err := server.globalConfiguration.ACME.CreateACMEConfig(config, checkOnDemandDomain) + if err != nil { + return nil, err + } + } + } else { + return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + " for ACME configuration") + } + } + if len(config.Certificates) == 0 { + return nil, errors.New("No certificates found for TLS entrypoint " + entryPointName) } // BuildNameToCertificate parses the CommonName and SubjectAlternateName fields // in each certificate and populates the config.NameToCertificate map. @@ -267,15 +280,15 @@ func (server *Server) startServer(srv *manners.GracefulServer, globalConfigurati log.Info("Server stopped") } -func (server *Server) prepareServer(router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) { - log.Infof("Preparing server %+v", entryPoint) +func (server *Server) prepareServer(entryPointName string, router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) { + log.Infof("Preparing server %s %+v", entryPointName, entryPoint) // middlewares var negroni = negroni.New() for _, middleware := range middlewares { negroni.Use(middleware) } negroni.UseHandler(router) - tlsConfig, err := server.createTLSConfig(entryPoint.TLS, router) + tlsConfig, err := server.createTLSConfig(entryPointName, entryPoint.TLS, router) if err != nil { log.Fatalf("Error creating TLS config %s", err) return nil, err diff --git a/traefik.sample.toml b/traefik.sample.toml index 8766d495a..e0f05dfff 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -75,11 +75,11 @@ # storageFile = "acme.json" # Entrypoint to proxy acme challenge to. -# WARNING, must point to an entrypoint on port 80 +# WARNING, must point to an entrypoint on port 443 # # Required # -# entryPoint = "http" +# entryPoint = "https" # Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate. # WARNING, TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks.