607 lines
17 KiB
Go
607 lines
17 KiB
Go
package acme
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
fmtlog "log"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/ty/fun"
|
|
"github.com/cenk/backoff"
|
|
"github.com/containous/staert"
|
|
"github.com/containous/traefik/cluster"
|
|
"github.com/containous/traefik/log"
|
|
"github.com/containous/traefik/safe"
|
|
"github.com/containous/traefik/types"
|
|
"github.com/eapache/channels"
|
|
"github.com/xenolf/lego/acme"
|
|
"github.com/xenolf/lego/providers/dns"
|
|
)
|
|
|
|
var (
|
|
// OSCPMustStaple enables OSCP stapling as from https://github.com/xenolf/lego/issues/270
|
|
OSCPMustStaple = false
|
|
)
|
|
|
|
// ACME allows to connect to lets encrypt and retrieve certs
|
|
type ACME struct {
|
|
Email string `description:"Email address used for registration"`
|
|
Domains []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'"`
|
|
Storage string `description:"File or key used for certificates storage."`
|
|
StorageFile string // deprecated
|
|
OnDemand bool `description:"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."`
|
|
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
|
CAServer string `description:"CA server to use."`
|
|
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
|
DNSProvider string `description:"Use a DNS based challenge provider rather than HTTPS."`
|
|
DelayDontCheckDNS int `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
|
|
ACMELogging bool `description:"Enable debug logging of ACME actions."`
|
|
client *acme.Client
|
|
defaultCertificate *tls.Certificate
|
|
store cluster.Store
|
|
challengeProvider *challengeProvider
|
|
checkOnDemandDomain func(domain string) bool
|
|
jobs *channels.InfiniteChannel
|
|
TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"`
|
|
}
|
|
|
|
//Domains parse []Domain
|
|
type Domains []Domain
|
|
|
|
//Set []Domain
|
|
func (ds *Domains) Set(str string) error {
|
|
fargs := func(c rune) bool {
|
|
return c == ',' || c == ';'
|
|
}
|
|
// get function
|
|
slice := strings.FieldsFunc(str, fargs)
|
|
if len(slice) < 1 {
|
|
return fmt.Errorf("Parse error ACME.Domain. Imposible to parse %s", str)
|
|
}
|
|
d := Domain{
|
|
Main: slice[0],
|
|
SANs: []string{},
|
|
}
|
|
if len(slice) > 1 {
|
|
d.SANs = slice[1:]
|
|
}
|
|
*ds = append(*ds, d)
|
|
return nil
|
|
}
|
|
|
|
//Get []Domain
|
|
func (ds *Domains) Get() interface{} { return []Domain(*ds) }
|
|
|
|
//String returns []Domain in string
|
|
func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) }
|
|
|
|
//SetValue sets []Domain into the parser
|
|
func (ds *Domains) SetValue(val interface{}) {
|
|
*ds = Domains(val.([]Domain))
|
|
}
|
|
|
|
// Domain holds a domain name with SANs
|
|
type Domain struct {
|
|
Main string
|
|
SANs []string
|
|
}
|
|
|
|
func (a *ACME) init() error {
|
|
if a.ACMELogging {
|
|
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
|
|
} else {
|
|
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
|
}
|
|
// no certificates in TLS config, so we add a default one
|
|
cert, err := generateDefaultCertificate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.defaultCertificate = cert
|
|
// TODO: to remove in the futurs
|
|
if len(a.StorageFile) > 0 && len(a.Storage) == 0 {
|
|
log.Warnf("ACME.StorageFile is deprecated, use ACME.Storage instead")
|
|
a.Storage = a.StorageFile
|
|
}
|
|
a.jobs = channels.NewInfiniteChannel()
|
|
return nil
|
|
}
|
|
|
|
// CreateClusterConfig creates a tls.config using ACME configuration in cluster mode
|
|
func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error {
|
|
err := a.init()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(a.Storage) == 0 {
|
|
return errors.New("Empty Store, please provide a key for certs storage")
|
|
}
|
|
a.checkOnDemandDomain = checkOnDemandDomain
|
|
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
|
tlsConfig.GetCertificate = a.getCertificate
|
|
a.TLSConfig = tlsConfig
|
|
listener := func(object cluster.Object) error {
|
|
account := object.(*Account)
|
|
account.Init()
|
|
if !leadership.IsLeader() {
|
|
a.client, err = a.buildACMEClient(account)
|
|
if err != nil {
|
|
log.Errorf("Error building ACME client %+v: %s", object, err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
datastore, err := cluster.NewDataStore(
|
|
leadership.Pool.Ctx(),
|
|
staert.KvSource{
|
|
Store: leadership.Store,
|
|
Prefix: a.Storage,
|
|
},
|
|
&Account{},
|
|
listener)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.store = datastore
|
|
a.challengeProvider = &challengeProvider{store: a.store}
|
|
|
|
ticker := time.NewTicker(24 * time.Hour)
|
|
leadership.Pool.AddGoCtx(func(ctx context.Context) {
|
|
log.Infof("Starting ACME renew job...")
|
|
defer log.Infof("Stopped ACME renew job...")
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
a.renewCertificates()
|
|
}
|
|
}
|
|
})
|
|
|
|
leadership.AddListener(func(elected bool) error {
|
|
if elected {
|
|
object, err := a.store.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
transaction, object, err := a.store.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account := object.(*Account)
|
|
account.Init()
|
|
var needRegister bool
|
|
if account == nil || len(account.Email) == 0 {
|
|
account, err = NewAccount(a.Email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
needRegister = true
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.client, err = a.buildACMEClient(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if needRegister {
|
|
// New users will need to register; be sure to save it
|
|
log.Debugf("Register...")
|
|
reg, err := a.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.
|
|
log.Debugf("AgreeToTOS...")
|
|
err = a.client.AgreeToTOS()
|
|
if err != nil {
|
|
// Let's Encrypt Subscriber Agreement renew ?
|
|
reg, err := a.client.QueryRegistration()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account.Registration = reg
|
|
err = a.client.AgreeToTOS()
|
|
if err != nil {
|
|
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
|
|
}
|
|
}
|
|
err = transaction.Commit(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.retrieveCertificates()
|
|
a.renewCertificates()
|
|
a.runJobs()
|
|
}
|
|
return nil
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// CreateLocalConfig creates a tls.config using local ACME configuration
|
|
func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error {
|
|
err := a.init()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(a.Storage) == 0 {
|
|
return errors.New("Empty Store, please provide a filename for certs storage")
|
|
}
|
|
a.checkOnDemandDomain = checkOnDemandDomain
|
|
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
|
tlsConfig.GetCertificate = a.getCertificate
|
|
a.TLSConfig = tlsConfig
|
|
localStore := NewLocalStore(a.Storage)
|
|
a.store = localStore
|
|
a.challengeProvider = &challengeProvider{store: a.store}
|
|
|
|
var needRegister bool
|
|
var account *Account
|
|
|
|
if fileInfo, fileErr := os.Stat(a.Storage); fileErr == nil && fileInfo.Size() != 0 {
|
|
log.Infof("Loading ACME Account...")
|
|
// load account
|
|
object, err := localStore.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account = object.(*Account)
|
|
} else {
|
|
log.Infof("Generating ACME Account...")
|
|
account, err = NewAccount(a.Email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
needRegister = true
|
|
}
|
|
|
|
a.client, err = a.buildACMEClient(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if needRegister {
|
|
// New users will need to register; be sure to save it
|
|
log.Infof("Register...")
|
|
reg, err := a.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.
|
|
log.Debugf("AgreeToTOS...")
|
|
err = a.client.AgreeToTOS()
|
|
if err != nil {
|
|
// Let's Encrypt Subscriber Agreement renew ?
|
|
reg, err := a.client.QueryRegistration()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account.Registration = reg
|
|
err = a.client.AgreeToTOS()
|
|
if err != nil {
|
|
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
|
|
}
|
|
}
|
|
// save account
|
|
transaction, _, err := a.store.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = transaction.Commit(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.retrieveCertificates()
|
|
a.renewCertificates()
|
|
a.runJobs()
|
|
|
|
ticker := time.NewTicker(24 * time.Hour)
|
|
safe.Go(func() {
|
|
for range ticker.C {
|
|
a.renewCertificates()
|
|
}
|
|
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
domain := types.CanonicalDomain(clientHello.ServerName)
|
|
account := a.store.Get().(*Account)
|
|
//use regex to test for wildcard certs that might have been added into TLSConfig
|
|
for k := range a.TLSConfig.NameToCertificate {
|
|
selector := "^" + strings.Replace(k, "*.", ".*\\.?", -1) + "$"
|
|
match, _ := regexp.MatchString(selector, domain)
|
|
if match {
|
|
return a.TLSConfig.NameToCertificate[k], nil
|
|
}
|
|
}
|
|
if challengeCert, ok := a.challengeProvider.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
|
|
}
|
|
if a.OnDemand {
|
|
if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(domain) {
|
|
return nil, nil
|
|
}
|
|
return a.loadCertificateOnDemand(clientHello)
|
|
}
|
|
log.Debugf("ACME got nothing %s", domain)
|
|
return nil, nil
|
|
}
|
|
|
|
func (a *ACME) retrieveCertificates() {
|
|
a.jobs.In() <- func() {
|
|
log.Infof("Retrieving ACME certificates...")
|
|
for _, domain := range a.Domains {
|
|
// check if cert isn't already loaded
|
|
account := a.store.Get().(*Account)
|
|
if _, exists := account.DomainsCertificate.exists(domain); !exists {
|
|
domains := []string{}
|
|
domains = append(domains, domain.Main)
|
|
domains = append(domains, domain.SANs...)
|
|
certificateResource, err := a.getDomainsCertificates(domains)
|
|
if err != nil {
|
|
log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
|
|
continue
|
|
}
|
|
transaction, object, err := a.store.Begin()
|
|
if err != nil {
|
|
log.Errorf("Error creating ACME store transaction from domain %s: %s", domain, err.Error())
|
|
continue
|
|
}
|
|
account = object.(*Account)
|
|
_, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain)
|
|
if err != nil {
|
|
log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error())
|
|
continue
|
|
}
|
|
|
|
if err = transaction.Commit(account); err != nil {
|
|
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
log.Infof("Retrieved ACME certificates")
|
|
}
|
|
}
|
|
|
|
func (a *ACME) renewCertificates() {
|
|
a.jobs.In() <- func() {
|
|
log.Debugf("Testing certificate renew...")
|
|
account := a.store.Get().(*Account)
|
|
for _, certificateResource := range account.DomainsCertificate.Certs {
|
|
if certificateResource.needRenew() {
|
|
log.Debugf("Renewing certificate %+v", certificateResource.Domains)
|
|
renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{
|
|
Domain: certificateResource.Certificate.Domain,
|
|
CertURL: certificateResource.Certificate.CertURL,
|
|
CertStableURL: certificateResource.Certificate.CertStableURL,
|
|
PrivateKey: certificateResource.Certificate.PrivateKey,
|
|
Certificate: certificateResource.Certificate.Certificate,
|
|
}, true, OSCPMustStaple)
|
|
if err != nil {
|
|
log.Errorf("Error renewing certificate: %v", err)
|
|
continue
|
|
}
|
|
log.Debugf("Renewed certificate %+v", certificateResource.Domains)
|
|
renewedACMECert := &Certificate{
|
|
Domain: renewedCert.Domain,
|
|
CertURL: renewedCert.CertURL,
|
|
CertStableURL: renewedCert.CertStableURL,
|
|
PrivateKey: renewedCert.PrivateKey,
|
|
Certificate: renewedCert.Certificate,
|
|
}
|
|
transaction, object, err := a.store.Begin()
|
|
if err != nil {
|
|
log.Errorf("Error renewing certificate: %v", err)
|
|
continue
|
|
}
|
|
account = object.(*Account)
|
|
err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
|
|
if err != nil {
|
|
log.Errorf("Error renewing certificate: %v", err)
|
|
continue
|
|
}
|
|
|
|
if err = transaction.Commit(account); err != nil {
|
|
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func dnsOverrideDelay(delay int) error {
|
|
var err error
|
|
if delay > 0 {
|
|
log.Debugf("Delaying %d seconds rather than validating DNS propagation", delay)
|
|
acme.PreCheckDNS = func(_, _ string) (bool, error) {
|
|
time.Sleep(time.Duration(delay) * time.Second)
|
|
return true, nil
|
|
}
|
|
} else if delay < 0 {
|
|
err = fmt.Errorf("Invalid negative DelayDontCheckDNS: %d", delay)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
|
|
log.Debugf("Building ACME client...")
|
|
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
|
|
}
|
|
|
|
if len(a.DNSProvider) > 0 {
|
|
log.Debugf("Using DNS Challenge provider: %s", a.DNSProvider)
|
|
|
|
err = dnsOverrideDelay(a.DelayDontCheckDNS)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var provider acme.ChallengeProvider
|
|
provider, err = dns.NewDNSChallengeProviderByName(a.DNSProvider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
|
err = client.SetChallengeProvider(acme.DNS01, provider)
|
|
} else {
|
|
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
|
|
err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeProvider)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
domain := types.CanonicalDomain(clientHello.ServerName)
|
|
account := a.store.Get().(*Account)
|
|
if certificateResource, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok {
|
|
return certificateResource.tlsCert, nil
|
|
}
|
|
certificate, err := a.getDomainsCertificates([]string{domain})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Debugf("Got certificate on demand for domain %s", domain)
|
|
|
|
transaction, object, err := a.store.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
account = object.(*Account)
|
|
cert, err := account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: domain})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = transaction.Commit(account); err != nil {
|
|
return nil, err
|
|
}
|
|
return cert.tlsCert, nil
|
|
}
|
|
|
|
// LoadCertificateForDomains loads certificates from ACME for given domains
|
|
func (a *ACME) LoadCertificateForDomains(domains []string) {
|
|
a.jobs.In() <- func() {
|
|
log.Debugf("LoadCertificateForDomains %s...", domains)
|
|
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
|
operation := func() error {
|
|
if a.client == nil {
|
|
return fmt.Errorf("ACME client still not built")
|
|
}
|
|
return nil
|
|
}
|
|
notify := func(err error, time time.Duration) {
|
|
log.Errorf("Error getting ACME client: %v, retrying in %s", err, time)
|
|
}
|
|
ebo := backoff.NewExponentialBackOff()
|
|
ebo.MaxElapsedTime = 30 * time.Second
|
|
err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
|
|
if err != nil {
|
|
log.Errorf("Error getting ACME client: %v", err)
|
|
return
|
|
}
|
|
account := a.store.Get().(*Account)
|
|
var domain Domain
|
|
if len(domains) == 0 {
|
|
// no domain
|
|
return
|
|
|
|
} else if len(domains) > 1 {
|
|
domain = Domain{Main: domains[0], SANs: domains[1:]}
|
|
} else {
|
|
domain = Domain{Main: domains[0]}
|
|
}
|
|
if _, exists := account.DomainsCertificate.exists(domain); exists {
|
|
// domain already exists
|
|
return
|
|
}
|
|
certificate, err := a.getDomainsCertificates(domains)
|
|
if err != nil {
|
|
log.Errorf("Error getting ACME certificates %+v : %v", domains, err)
|
|
return
|
|
}
|
|
log.Debugf("Got certificate for domains %+v", domains)
|
|
transaction, object, err := a.store.Begin()
|
|
|
|
if err != nil {
|
|
log.Errorf("Error creating transaction %+v : %v", domains, err)
|
|
return
|
|
}
|
|
account = object.(*Account)
|
|
_, err = account.DomainsCertificate.addCertificateForDomains(certificate, domain)
|
|
if err != nil {
|
|
log.Errorf("Error adding ACME certificates %+v : %v", domains, err)
|
|
return
|
|
}
|
|
if err = transaction.Commit(account); err != nil {
|
|
log.Errorf("Error Saving ACME account %+v: %v", account, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
|
|
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
|
log.Debugf("Loading ACME certificates %s...", domains)
|
|
bundle := true
|
|
certificate, failures := a.client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
|
|
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
|
|
}
|
|
|
|
func (a *ACME) runJobs() {
|
|
safe.Go(func() {
|
|
for job := range a.jobs.Out() {
|
|
function := job.(func())
|
|
function()
|
|
}
|
|
})
|
|
}
|