add let's encrypt support
Signed-off-by: Emile Vauge <emile@vauge.com>
This commit is contained in:
parent
087b68e14d
commit
6e484e5c2d
18 changed files with 556 additions and 172 deletions
337
acme.go
Normal file
337
acme.go
Normal file
|
@ -0,0 +1,337 @@
|
|||
/*
|
||||
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue