mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-06 22:40:31 -05:00
Clean up some significant portions of the TLS management code
This commit is contained in:
parent
0e7635c54b
commit
bedad34b25
6 changed files with 112 additions and 166 deletions
|
@ -23,7 +23,7 @@ func activateHTTPS(cctx caddy.Context) error {
|
|||
|
||||
// place certificates and keys on disk
|
||||
for _, c := range ctx.siteConfigs {
|
||||
err := c.TLS.ObtainCert(operatorPresent)
|
||||
err := c.TLS.ObtainCert(c.TLS.Hostname, operatorPresent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -18,11 +18,13 @@ import (
|
|||
// acmeMu ensures that only one ACME challenge occurs at a time.
|
||||
var acmeMu sync.Mutex
|
||||
|
||||
// ACMEClient is an acme.Client with custom state attached.
|
||||
// ACMEClient is a wrapper over acme.Client with
|
||||
// some custom state attached. It is used to obtain,
|
||||
// renew, and revoke certificates with ACME.
|
||||
type ACMEClient struct {
|
||||
*acme.Client
|
||||
AllowPrompts bool
|
||||
config *Config
|
||||
acmeClient *acme.Client
|
||||
}
|
||||
|
||||
// newACMEClient creates a new ACMEClient given an email and whether
|
||||
|
@ -100,7 +102,11 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
|
|||
}
|
||||
}
|
||||
|
||||
c := &ACMEClient{Client: client, AllowPrompts: allowPrompts, config: config}
|
||||
c := &ACMEClient{
|
||||
AllowPrompts: allowPrompts,
|
||||
config: config,
|
||||
acmeClient: client,
|
||||
}
|
||||
|
||||
if config.DNSProvider == "" {
|
||||
// Use HTTP and TLS-SNI challenges by default
|
||||
|
@ -116,15 +122,15 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
|
|||
|
||||
// See if TLS challenge needs to be handled by our own facilities
|
||||
if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, TLSSNIChallengePort)) {
|
||||
c.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{})
|
||||
c.acmeClient.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{})
|
||||
}
|
||||
|
||||
// Always respect user's bind preferences by using config.ListenHost
|
||||
err := c.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort))
|
||||
err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.SetTLSAddress(net.JoinHostPort(config.ListenHost, ""))
|
||||
err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -145,23 +151,50 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
|
|||
}
|
||||
|
||||
// Use the DNS challenge exclusively
|
||||
c.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
||||
c.SetChallengeProvider(acme.DNS01, prov)
|
||||
c.acmeClient.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
||||
c.acmeClient.SetChallengeProvider(acme.DNS01, prov)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Obtain obtains a single certificate for names. It stores the certificate
|
||||
// on the disk if successful.
|
||||
func (c *ACMEClient) Obtain(names []string) error {
|
||||
// Obtain obtains a single certificate for name. It stores the certificate
|
||||
// on the disk if successful. This function is safe for concurrent use.
|
||||
//
|
||||
// Right now our storage mechanism only supports one name per certificate,
|
||||
// so this function (along with Renew and Revoke) only accepts one domain
|
||||
// as input. It can be easily modified to support SAN certificates if our
|
||||
// storage mechanism is upgraded later.
|
||||
//
|
||||
// Callers who have access to a Config value should use the ObtainCert
|
||||
// method on that instead of this lower-level method.
|
||||
func (c *ACMEClient) Obtain(name string) error {
|
||||
// Get access to ACME storage
|
||||
storage, err := c.config.StorageFor(c.config.CAUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We must lock the obtain with the storage engine
|
||||
if lockObtained, err := storage.LockRegister(name); err != nil {
|
||||
return err
|
||||
} else if !lockObtained {
|
||||
log.Printf("[INFO] Certificate for %v is already being obtained elsewhere", name)
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := storage.UnlockRegister(name); err != nil {
|
||||
log.Printf("[ERROR] Unable to unlock obtain lock for %v: %v", name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
Attempts:
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
namesObtaining.Add(names)
|
||||
namesObtaining.Add([]string{name})
|
||||
acmeMu.Lock()
|
||||
certificate, failures := c.ObtainCertificate(names, true, nil)
|
||||
certificate, failures := c.acmeClient.ObtainCertificate([]string{name}, true, nil)
|
||||
acmeMu.Unlock()
|
||||
namesObtaining.Remove(names)
|
||||
namesObtaining.Remove([]string{name})
|
||||
if len(failures) > 0 {
|
||||
// Error - try to fix it or report it to the user and abort
|
||||
var errMsg string // we'll combine all the failures into a single error message
|
||||
|
@ -178,7 +211,7 @@ Attempts:
|
|||
promptedForAgreement = true
|
||||
}
|
||||
if Agreed || !c.AllowPrompts {
|
||||
err := c.AgreeToTOS()
|
||||
err := c.acmeClient.AgreeToTOS()
|
||||
if err != nil {
|
||||
return errors.New("error agreeing to updated terms: " + err.Error())
|
||||
}
|
||||
|
@ -193,13 +226,9 @@ Attempts:
|
|||
}
|
||||
|
||||
// Success - immediately save the certificate resource
|
||||
storage, err := c.config.StorageFor(c.config.CAUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = saveCertResource(storage, certificate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving assets for %v: %v", names, err)
|
||||
return fmt.Errorf("error saving assets for %v: %v", name, err)
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -208,13 +237,11 @@ Attempts:
|
|||
return nil
|
||||
}
|
||||
|
||||
// Renew renews the managed certificate for name. Right now our storage
|
||||
// mechanism only supports one name per certificate, so this function only
|
||||
// accepts one domain as input. It can be easily modified to support SAN
|
||||
// certificates if, one day, they become desperately needed enough that our
|
||||
// storage mechanism is upgraded to be more complex to support SAN certs.
|
||||
// Renew renews the managed certificate for name. This function is
|
||||
// safe for concurrent use.
|
||||
//
|
||||
// Anyway, this function is safe for concurrent use.
|
||||
// Callers who have access to a Config value should use the RenewCert
|
||||
// method on that instead of this lower-level method.
|
||||
func (c *ACMEClient) Renew(name string) error {
|
||||
// Get access to ACME storage
|
||||
storage, err := c.config.StorageFor(c.config.CAUrl)
|
||||
|
@ -251,7 +278,7 @@ func (c *ACMEClient) Renew(name string) error {
|
|||
for attempts := 0; attempts < 2; attempts++ {
|
||||
namesObtaining.Add([]string{name})
|
||||
acmeMu.Lock()
|
||||
newCertMeta, err = c.RenewCertificate(certMeta, true)
|
||||
newCertMeta, err = c.acmeClient.RenewCertificate(certMeta, true)
|
||||
acmeMu.Unlock()
|
||||
namesObtaining.Remove([]string{name})
|
||||
if err == nil {
|
||||
|
@ -259,10 +286,10 @@ func (c *ACMEClient) Renew(name string) error {
|
|||
break
|
||||
}
|
||||
|
||||
// If the legal terms changed and need to be agreed to again,
|
||||
// we can handle that.
|
||||
// If the legal terms were updated and need to be
|
||||
// agreed to again, we can handle that.
|
||||
if _, ok := err.(acme.TOSError); ok {
|
||||
err := c.AgreeToTOS()
|
||||
err := c.acmeClient.AgreeToTOS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -304,7 +331,7 @@ func (c *ACMEClient) Revoke(name string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = c.Client.RevokeCertificate(siteData.Cert)
|
||||
err = c.acmeClient.RevokeCertificate(siteData.Cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -3,13 +3,9 @@ package caddytls
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
|
@ -116,24 +112,21 @@ type OnDemandState struct {
|
|||
// If it reaches MaxObtain, on-demand issuances must fail.
|
||||
ObtainedCount int32
|
||||
|
||||
// Based on max_certs in tls config, it specifies the
|
||||
// Set from max_certs in tls config, it specifies the
|
||||
// maximum number of certificates that can be issued.
|
||||
MaxObtain int32
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for c.Hostname, as long as a certificate
|
||||
// does not already exist in storage on disk. It only obtains and stores
|
||||
// certificates (and their keys) to disk, it does not load them into memory.
|
||||
// If allowPrompts is true, the user may be shown a prompt. If proxyACME is
|
||||
// true, the relevant ACME challenges will be proxied to the alternate port.
|
||||
func (c *Config) ObtainCert(allowPrompts bool) error {
|
||||
return c.obtainCertName(c.Hostname, allowPrompts)
|
||||
}
|
||||
|
||||
// obtainCertName gets a certificate for name using the ACME config c
|
||||
// if c and name both qualify. It places the certificate in storage.
|
||||
// It is a no-op if the storage already has a certificate for name.
|
||||
func (c *Config) obtainCertName(name string, allowPrompts bool) error {
|
||||
// ObtainCert obtains a certificate for name using c, as long
|
||||
// as a certificate does not already exist in storage for that
|
||||
// name. The name must qualify and c must be flagged as Managed.
|
||||
// This function is a no-op if storage already has a certificate
|
||||
// for name.
|
||||
//
|
||||
// It only obtains and stores certificates (and their keys),
|
||||
// it does not load them into memory. If allowPrompts is true,
|
||||
// the user may be shown a prompt.
|
||||
func (c *Config) ObtainCert(name string, allowPrompts bool) error {
|
||||
if !c.Managed || !HostQualifies(name) {
|
||||
return nil
|
||||
}
|
||||
|
@ -142,29 +135,13 @@ func (c *Config) obtainCertName(name string, allowPrompts bool) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
siteExists, err := storage.SiteExists(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if siteExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We must lock the obtain with the storage engine
|
||||
if lockObtained, err := storage.LockRegister(name); err != nil {
|
||||
return err
|
||||
} else if !lockObtained {
|
||||
log.Printf("[INFO] Certificate for %v is already being obtained elsewhere", name)
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := storage.UnlockRegister(name); err != nil {
|
||||
log.Printf("[ERROR] Unable to unlock obtain lock for %v: %v", name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if c.ACMEEmail == "" {
|
||||
c.ACMEEmail = getEmail(storage, allowPrompts)
|
||||
}
|
||||
|
@ -173,86 +150,17 @@ func (c *Config) obtainCertName(name string, allowPrompts bool) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Obtain([]string{name})
|
||||
return client.Obtain(name)
|
||||
}
|
||||
|
||||
// RenewCert renews the certificate for c.Hostname. If there is already a lock
|
||||
// on renewal, this will not perform the renewal and no error will occur.
|
||||
func (c *Config) RenewCert(allowPrompts bool) error {
|
||||
return c.renewCertName(c.Hostname, allowPrompts)
|
||||
}
|
||||
|
||||
// renewCertName renews the certificate for the given name. If there is already
|
||||
// a lock on renewal, this will not perform the renewal and no error will
|
||||
// occur.
|
||||
func (c *Config) renewCertName(name string, allowPrompts bool) error {
|
||||
storage, err := c.StorageFor(c.CAUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We must lock the renewal with the storage engine
|
||||
if lockObtained, err := storage.LockRegister(name); err != nil {
|
||||
return err
|
||||
} else if !lockObtained {
|
||||
log.Printf("[INFO] Certificate for %v is already being renewed elsewhere", name)
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := storage.UnlockRegister(name); err != nil {
|
||||
log.Printf("[ERROR] Unable to unlock renewal lock for %v: %v", name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Prepare for renewal (load PEM cert, key, and meta)
|
||||
siteData, err := storage.LoadSite(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var certMeta acme.CertificateResource
|
||||
err = json.Unmarshal(siteData.Meta, &certMeta)
|
||||
certMeta.Certificate = siteData.Cert
|
||||
certMeta.PrivateKey = siteData.Key
|
||||
|
||||
// RenewCert renews the certificate for name using c. It stows the
|
||||
// renewed certificate and its assets in storage if successful.
|
||||
func (c *Config) RenewCert(name string, allowPrompts bool) error {
|
||||
client, err := newACMEClient(c, allowPrompts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform renewal and retry if necessary, but not too many times.
|
||||
var newCertMeta acme.CertificateResource
|
||||
var success bool
|
||||
for attempts := 0; attempts < 2; attempts++ {
|
||||
namesObtaining.Add([]string{name})
|
||||
acmeMu.Lock()
|
||||
newCertMeta, err = client.RenewCertificate(certMeta, true)
|
||||
acmeMu.Unlock()
|
||||
namesObtaining.Remove([]string{name})
|
||||
if err == nil {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
|
||||
// If the legal terms were updated and need to be
|
||||
// agreed to again, we can handle that.
|
||||
if _, ok := err.(acme.TOSError); ok {
|
||||
err := client.AgreeToTOS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// For any other kind of error, wait 10s and try again.
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
if !success {
|
||||
return errors.New("too many renewal attempts; last error: " + err.Error())
|
||||
}
|
||||
|
||||
return saveCertResource(storage, newCertMeta)
|
||||
return client.Renew(name)
|
||||
}
|
||||
|
||||
// StorageFor obtains a TLS Storage instance for the given CA URL which should
|
||||
|
|
|
@ -123,7 +123,6 @@ func (s FileStorage) readFile(file string) ([]byte, error) {
|
|||
return nil, ErrNotExist(err)
|
||||
}
|
||||
return b, err
|
||||
|
||||
}
|
||||
|
||||
// SiteExists implements Storage.SiteExists by checking for the presence of
|
||||
|
|
|
@ -172,22 +172,25 @@ func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certi
|
|||
return cg.getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
// looks like it's up to us to do all the work and obtain the cert
|
||||
// looks like it's up to us to do all the work and obtain the cert.
|
||||
// make a chan others can wait on if needed
|
||||
wait = make(chan struct{})
|
||||
obtainCertWaitChans[name] = wait
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
// Unblock waiters and delete waitgroup when we return
|
||||
defer func() {
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
}()
|
||||
|
||||
// do the obtain
|
||||
log.Printf("[INFO] Obtaining new certificate for %s", name)
|
||||
err := cfg.ObtainCert(name, false)
|
||||
|
||||
if err := cfg.obtainCertName(name, false); err != nil {
|
||||
// immediately unblock anyone waiting for it; doing this in
|
||||
// a defer would risk deadlock because of the recursive call
|
||||
// to getCertDuringHandshake below when we return!
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
// Failed to solve challenge, so don't allow another on-demand
|
||||
// issue for this name to be attempted for a little while.
|
||||
failedIssuanceMu.Lock()
|
||||
|
@ -208,7 +211,7 @@ func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certi
|
|||
lastIssueTime = time.Now()
|
||||
lastIssueTimeMu.Unlock()
|
||||
|
||||
// The certificate is already on disk; now just start over to load it and serve it
|
||||
// certificate is already on disk; now just start over to load it and serve it
|
||||
return cg.getCertDuringHandshake(name, true, false)
|
||||
}
|
||||
|
||||
|
@ -265,17 +268,18 @@ func (cg configGroup) renewDynamicCertificate(name string, cfg *Config) (Certifi
|
|||
obtainCertWaitChans[name] = wait
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
// unblock waiters and delete waitgroup when we return
|
||||
defer func() {
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
}()
|
||||
|
||||
// do the renew
|
||||
log.Printf("[INFO] Renewing certificate for %s", name)
|
||||
err := cfg.RenewCert(name, false)
|
||||
|
||||
// immediately unblock anyone waiting for it; doing this in
|
||||
// a defer would risk deadlock because of the recursive call
|
||||
// to getCertDuringHandshake below when we return!
|
||||
obtainCertWaitChansMu.Lock()
|
||||
close(wait)
|
||||
delete(obtainCertWaitChans, name)
|
||||
obtainCertWaitChansMu.Unlock()
|
||||
|
||||
err := cfg.renewCertName(name, false)
|
||||
if err != nil {
|
||||
return Certificate{}, err
|
||||
}
|
||||
|
|
|
@ -99,12 +99,20 @@ func RenewManagedCertificates(allowPrompts bool) (err error) {
|
|||
continue
|
||||
}
|
||||
|
||||
// This works well because managed certs are only associated with one name per config.
|
||||
// Note, the renewal inside here may not actually occur and no error will be returned
|
||||
// due to renewal lock (i.e. because a renewal is already happening). This lack of
|
||||
// error is by intention to force cache invalidation as though it has renewed.
|
||||
err := cert.Config.RenewCert(allowPrompts)
|
||||
// Get the name which we should use to renew this certificate;
|
||||
// we only support managing certificates with one name per cert,
|
||||
// so this should be easy. We can't rely on cert.Config.Hostname
|
||||
// because it may be a wildcard value from the Caddyfile (e.g.
|
||||
// *.something.com) which, as of 2016, is not supported by ACME.
|
||||
var renewName string
|
||||
for _, name := range cert.Names {
|
||||
if name != "" {
|
||||
renewName = name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err := cert.Config.RenewCert(renewName, allowPrompts)
|
||||
if err != nil {
|
||||
if allowPrompts && timeLeft < 0 {
|
||||
// Certificate renewal failed, the operator is present, and the certificate
|
||||
|
|
Loading…
Reference in a new issue