add acme package, refactor acme as resuable API

Signed-off-by: Emile Vauge <emile@vauge.com>
This commit is contained in:
Emile Vauge 2016-03-21 11:10:18 +01:00
parent 87e8393b07
commit d9ffc39075
No known key found for this signature in database
GPG key ID: D808B4C167352E59
8 changed files with 577 additions and 383 deletions

337
acme.go
View file

@ -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
}

401
acme/acme.go Normal file
View file

@ -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
}

56
acme/challengeProvider.go Normal file
View file

@ -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
}

78
acme/crypto.go Normal file
View file

@ -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)
}

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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.