1
0
Fork 0

Add ACME store

Signed-off-by: Emile Vauge <emile@vauge.com>
This commit is contained in:
Emile Vauge 2016-08-18 14:20:11 +02:00
parent bea5ad3f13
commit a42845502e
No known key found for this signature in database
GPG key ID: D808B4C167352E59
30 changed files with 781 additions and 374 deletions

View file

@ -1,179 +1,36 @@
package acme
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"github.com/containous/staert"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
"github.com/xenolf/lego/acme"
"golang.org/x/net/context"
"io/ioutil"
fmtlog "log"
"os"
"reflect"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/containous/traefik/safe"
"github.com/xenolf/lego/acme"
)
// Account is used to store lets encrypt registration info
type Account struct {
Email string
Registration *acme.RegistrationResource
PrivateKey []byte
DomainsCertificate DomainsCertificates
}
// 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
}
// 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) {
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
if err != nil {
return err
}
domainsCertificate.Certificate = acmeCert
domainsCertificate.tlsCert = &tlsCert
return nil
}
}
return errors.New("Certificate to renew not found for 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 := []string{}
domains = append(domains, domainsCertificate.Domains.Main)
domains = append(domains, 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
}
func (dc *DomainsCertificate) needRenew() bool {
for _, c := range dc.tlsCert.Certificate {
crt, err := x509.ParseCertificate(c)
if err != nil {
// If there's an error, we assume the cert is broken, and needs update
return true
}
// <= 30 days left, renew certificate
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) {
return true
}
}
return 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'"`
StorageFile string `description:"File used for certificates storage."`
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."`
storageLock sync.RWMutex
client *acme.Client
account *Account
defaultCertificate *tls.Certificate
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."`
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."`
client *acme.Client
defaultCertificate *tls.Certificate
store cluster.Store
challengeProvider *challengeProvider
checkOnDemandDomain func(domain string) bool
}
//Domains parse []Domain
@ -218,11 +75,7 @@ type Domain struct {
}
func (a *ACME) init() error {
if len(a.Store) == 0 {
a.Store = a.StorageFile
}
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
log.Debugf("Generating default certificate...")
// no certificates in TLS config, so we add a default one
cert, err := generateDefaultCertificate()
if err != nil {
@ -232,78 +85,174 @@ func (a *ACME) init() error {
return nil
}
// CreateClusterConfig creates a tls.config from using ACME configuration
func (a *ACME) CreateClusterConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error {
// 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.Store) == 0 {
return errors.New("Empty Store, please provide a filename/key for certs storage")
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
datastore, err := cluster.NewDataStore(
staert.KvSource{
Store: leadership.Store,
Prefix: leadership.Store.Prefix + "/acme/account",
},
leadership.Pool.Ctx(), &Account{},
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
})
if err != nil {
return err
}
a.store = datastore
a.challengeProvider = newMemoryChallengeProvider(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...")
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate: %s", err.Error())
}
}
})
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
}
log.Debugf("buildACMEClient...")
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 {
return err
}
err = transaction.Commit(account)
if err != nil {
return err
}
safe.Go(func() {
a.retrieveCertificates()
if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
}
})
}
return nil
})
return nil
}
// CreateLocalConfig creates a tls.config from using ACME configuration
func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error {
// 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.Store) == 0 {
return errors.New("Empty Store, please provide a filename/key for certs storage")
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
localStore := NewLocalStore(a.Storage)
a.store = localStore
a.challengeProvider = newMemoryChallengeProvider(a.store)
var needRegister bool
var err error
var account *Account
// if certificates in storage, load them
if fileInfo, fileErr := os.Stat(a.Store); fileErr == nil && fileInfo.Size() != 0 {
log.Infof("Loading ACME certificates...")
if fileInfo, fileErr := os.Stat(a.Storage); fileErr == nil && fileInfo.Size() != 0 {
log.Infof("Loading ACME Account...")
// load account
a.account, err = a.loadAccount(a)
object, err := localStore.Load()
if err != nil {
return err
}
account = object.(*Account)
} 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)
account, err = NewAccount(a.Email)
if err != nil {
return err
}
a.account = &Account{
Email: a.Email,
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
}
a.account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}}
needRegister = true
}
a.client, err = a.buildACMEClient()
if err != nil {
return err
}
a.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
wrapperChallengeProvider := newWrapperChallengeProvider()
err = client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider)
log.Infof("buildACMEClient...")
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
}
a.account.Registration = reg
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.Infof("AgreeToTOS...")
err = a.client.AgreeToTOS()
if err != nil {
// Let's Encrypt Subscriber Agreement renew ?
@ -311,45 +260,33 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func
if err != nil {
return err
}
a.account.Registration = reg
account.Registration = reg
err = a.client.AgreeToTOS()
if err != nil {
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", a.account, err.Error())
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
}
}
// save account
err = a.saveAccount()
transaction, _, err := a.store.Begin()
if err != nil {
return err
}
err = transaction.Commit(account)
if err != nil {
return err
}
safe.Go(func() {
a.retrieveCertificates(a.client)
if err := a.renewCertificates(a.client); err != nil {
log.Errorf("Error renewing ACME certificate %+v: %s", a.account, err.Error())
a.retrieveCertificates()
if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
}
})
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if challengeCert, ok := wrapperChallengeProvider.getCertificate(clientHello.ServerName); ok {
return challengeCert, nil
}
if domainCert, ok := a.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(clientHello)
}
return nil, nil
}
ticker := time.NewTicker(24 * time.Hour)
safe.Go(func() {
for range ticker.C {
if err := a.renewCertificates(client, account); err != nil {
if err := a.renewCertificates(); err != nil {
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
}
}
@ -358,26 +295,54 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, CheckOnDemandDomain func
return nil
}
func (a *ACME) retrieveCertificates(client *acme.Client) {
func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
account := a.store.Get().(*Account)
if challengeCert, ok := a.challengeProvider.getCertificate(clientHello.ServerName); ok {
log.Debugf("ACME got challenge %s", clientHello.ServerName)
return challengeCert, nil
}
if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
log.Debugf("ACME got domaincert %s", clientHello.ServerName)
return domainCert.tlsCert, nil
}
if a.OnDemand {
if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(clientHello.ServerName) {
return nil, nil
}
return a.loadCertificateOnDemand(clientHello)
}
log.Debugf("ACME got nothing %s", clientHello.ServerName)
return nil, nil
}
func (a *ACME) retrieveCertificates() {
log.Infof("Retrieving ACME certificates...")
for _, domain := range a.Domains {
// check if cert isn't already loaded
if _, exists := a.account.DomainsCertificate.exists(domain); !exists {
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(client, domains)
certificateResource, err := a.getDomainsCertificates(domains)
if err != nil {
log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
continue
}
_, err = a.account.DomainsCertificate.addCertificateForDomains(certificateResource, domain)
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 = a.saveAccount(); err != nil {
log.Errorf("Error Saving ACME account %+v: %s", a.account, err.Error())
if err = transaction.Commit(account); err != nil {
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
continue
}
}
@ -385,12 +350,18 @@ func (a *ACME) retrieveCertificates(client *acme.Client) {
log.Infof("Retrieved ACME certificates")
}
func (a *ACME) renewCertificates(client *acme.Client) error {
func (a *ACME) renewCertificates() error {
log.Debugf("Testing certificate renew...")
for _, certificateResource := range a.account.DomainsCertificate.Certs {
account := a.store.Get().(*Account)
for _, certificateResource := range account.DomainsCertificate.Certs {
if certificateResource.needRenew() {
transaction, object, err := a.store.Begin()
if err != nil {
return err
}
account = object.(*Account)
log.Debugf("Renewing certificate %+v", certificateResource.Domains)
renewedCert, err := client.RenewCertificate(acme.CertificateResource{
renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{
Domain: certificateResource.Certificate.Domain,
CertURL: certificateResource.Certificate.CertURL,
CertStableURL: certificateResource.Certificate.CertStableURL,
@ -409,13 +380,14 @@ func (a *ACME) renewCertificates(client *acme.Client) error {
PrivateKey: renewedCert.PrivateKey,
Certificate: renewedCert.Certificate,
}
err = a.account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
if err != nil {
log.Errorf("Error renewing certificate: %v", err)
continue
}
if err = a.saveAccount(); err != nil {
log.Errorf("Error saving ACME account: %v", err)
if err = transaction.Commit(account); err != nil {
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
continue
}
}
@ -423,33 +395,45 @@ func (a *ACME) renewCertificates(client *acme.Client) error {
return nil
}
func (a *ACME) buildACMEClient() (*acme.Client, error) {
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, a.account, acme.RSA4096)
client, err := acme.NewClient(caServer, account, acme.RSA4096)
if err != nil {
return nil, err
}
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) {
if certificateResource, ok := a.account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
account := a.store.Get().(*Account)
if certificateResource, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
return certificateResource.tlsCert, nil
}
certificate, err := a.getDomainsCertificates(a.client, []string{clientHello.ServerName})
certificate, err := a.getDomainsCertificates([]string{clientHello.ServerName})
if err != nil {
return nil, err
}
log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName)
cert, err := a.account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: clientHello.ServerName})
transaction, object, err := a.store.Begin()
if err != nil {
return nil, err
}
if err = a.saveAccount(); err != nil {
account = object.(*Account)
cert, err := account.DomainsCertificate.addCertificateForDomains(certificate, Domain{Main: clientHello.ServerName})
if err != nil {
return nil, err
}
if err = transaction.Commit(account); err != nil {
return nil, err
}
return cert.tlsCert, nil
@ -458,6 +442,7 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C
// LoadCertificateForDomains loads certificates from ACME for given domains
func (a *ACME) LoadCertificateForDomains(domains []string) {
safe.Go(func() {
account := a.store.Get().(*Account)
var domain Domain
if len(domains) == 0 {
// no domain
@ -468,64 +453,39 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
} else {
domain = Domain{Main: domains[0]}
}
if _, exists := a.account.DomainsCertificate.exists(domain); exists {
if _, exists := account.DomainsCertificate.exists(domain); exists {
// domain already exists
return
}
certificate, err := a.getDomainsCertificates(a.client, domains)
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)
_, err = a.account.DomainsCertificate.addCertificateForDomains(certificate, domain)
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 = a.saveAccount(); err != nil {
log.Errorf("Error Saving ACME account %+v: %v", a.account, err)
if err = transaction.Commit(account); err != nil {
log.Errorf("Error Saving ACME account %+v: %v", account, err)
return
}
})
}
func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, error) {
a.storageLock.RLock()
defer a.storageLock.RUnlock()
account := Account{
DomainsCertificate: DomainsCertificates{},
}
file, err := ioutil.ReadFile(acmeConfig.Store)
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 store %s", acmeConfig.Store)
return &account, nil
}
func (a *ACME) saveAccount() error {
a.storageLock.Lock()
defer a.storageLock.Unlock()
// write account to file
data, err := json.MarshalIndent(a.account, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(a.Store, data, 0600)
}
func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) {
func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
log.Debugf("Loading ACME certificates %s...", domains)
bundle := true
certificate, failures := client.ObtainCertificate(domains, bundle, nil)
certificate, failures := a.client.ObtainCertificate(domains, bundle, nil)
if len(failures) > 0 {
log.Error(failures)
return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures)