add acme package, refactor acme as resuable API
Signed-off-by: Emile Vauge <emile@vauge.com>
This commit is contained in:
parent
87e8393b07
commit
d9ffc39075
8 changed files with 577 additions and 383 deletions
337
acme.go
337
acme.go
|
@ -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
401
acme/acme.go
Normal 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
56
acme/challengeProvider.go
Normal 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
78
acme/crypto.go
Normal 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)
|
||||||
|
}
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/acme"
|
||||||
"github.com/containous/traefik/provider"
|
"github.com/containous/traefik/provider"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GlobalConfiguration holds global configuration (with providers, etc.).
|
// GlobalConfiguration holds global configuration (with providers, etc.).
|
||||||
|
@ -23,7 +23,7 @@ type GlobalConfiguration struct {
|
||||||
TraefikLogsFile string
|
TraefikLogsFile string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
EntryPoints EntryPoints
|
EntryPoints EntryPoints
|
||||||
ACME *ACME
|
ACME *acme.ACME
|
||||||
DefaultEntryPoints DefaultEntryPoints
|
DefaultEntryPoints DefaultEntryPoints
|
||||||
ProvidersThrottleDuration time.Duration
|
ProvidersThrottleDuration time.Duration
|
||||||
MaxIdleConnsPerHost int
|
MaxIdleConnsPerHost int
|
||||||
|
@ -142,23 +142,6 @@ type TLS struct {
|
||||||
Certificates Certificates
|
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
|
// Certificates defines traefik certificates type
|
||||||
type Certificates []Certificate
|
type Certificates []Certificate
|
||||||
|
|
||||||
|
|
|
@ -255,11 +255,11 @@ Use "traefik [command] --help" for more information about a command.
|
||||||
# storageFile = "acme.json"
|
# storageFile = "acme.json"
|
||||||
|
|
||||||
# Entrypoint to proxy acme challenge to.
|
# 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
|
# 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.
|
# 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.
|
# 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]
|
||||||
[entryPoints.http]
|
|
||||||
address = ":80"
|
|
||||||
[entryPoints.http.redirect]
|
|
||||||
entryPoint = "https"
|
|
||||||
[entryPoints.https]
|
[entryPoints.https]
|
||||||
address = ":443"
|
address = ":443"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
|
# certs used as default certs
|
||||||
|
[[entryPoints.https.tls.certificates]]
|
||||||
|
certFile = "tests/traefik.crt"
|
||||||
|
keyFile = "tests/traefik.key"
|
||||||
[acme]
|
[acme]
|
||||||
email = "test@traefik.io"
|
email = "test@traefik.io"
|
||||||
storageFile = "acme.json"
|
storageFile = "acme.json"
|
||||||
onDemand = true
|
onDemand = true
|
||||||
caServer = "http://172.18.0.1:4000/directory"
|
caServer = "http://172.18.0.1:4000/directory"
|
||||||
entryPoint = "http"
|
entryPoint = "https"
|
||||||
|
|
||||||
[[acme.domains]]
|
[[acme.domains]]
|
||||||
main = "local1.com"
|
main = "local1.com"
|
||||||
|
|
49
server.go
49
server.go
|
@ -102,7 +102,7 @@ func (server *Server) Close() {
|
||||||
func (server *Server) startHTTPServers() {
|
func (server *Server) startHTTPServers() {
|
||||||
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
|
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
|
||||||
for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
|
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 {
|
if err != nil {
|
||||||
log.Fatal("Error preparing server: ", err)
|
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
|
// 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 {
|
if tlsOption == nil {
|
||||||
return nil, 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{}
|
config := &tls.Config{}
|
||||||
var err error
|
config.Certificates = []tls.Certificate{}
|
||||||
config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates))
|
for _, v := range tlsOption.Certificates {
|
||||||
for i, v := range tlsOption.Certificates {
|
cert, err := tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
|
||||||
config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// BuildNameToCertificate parses the CommonName and SubjectAlternateName fields
|
||||||
// in each certificate and populates the config.NameToCertificate map.
|
// 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")
|
log.Info("Server stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) prepareServer(router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
|
func (server *Server) prepareServer(entryPointName string, router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
|
||||||
log.Infof("Preparing server %+v", entryPoint)
|
log.Infof("Preparing server %s %+v", entryPointName, entryPoint)
|
||||||
// middlewares
|
// middlewares
|
||||||
var negroni = negroni.New()
|
var negroni = negroni.New()
|
||||||
for _, middleware := range middlewares {
|
for _, middleware := range middlewares {
|
||||||
negroni.Use(middleware)
|
negroni.Use(middleware)
|
||||||
}
|
}
|
||||||
negroni.UseHandler(router)
|
negroni.UseHandler(router)
|
||||||
tlsConfig, err := server.createTLSConfig(entryPoint.TLS, router)
|
tlsConfig, err := server.createTLSConfig(entryPointName, entryPoint.TLS, router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error creating TLS config %s", err)
|
log.Fatalf("Error creating TLS config %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -75,11 +75,11 @@
|
||||||
# storageFile = "acme.json"
|
# storageFile = "acme.json"
|
||||||
|
|
||||||
# Entrypoint to proxy acme challenge to.
|
# 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
|
# 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.
|
# 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.
|
# WARNING, TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks.
|
||||||
|
|
Loading…
Reference in a new issue