mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 10:13:14 -04:00 
			
		
		
		
	* Changed reference to Caddy over to CoreDNS * Removing references to caddy * Fixed misleading error message to reference coredns * Cleaning up references to caddy * Adding clean and deps targets Muscle memory is resulting in "make clean" commands. * Adding test target to makefile * More "Caddy" cleanup
		
			
				
	
	
		
			340 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			340 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Package https facilitates the management of TLS assets and integrates
 | |
| // Let's Encrypt functionality into CoreDNS with first-class support for
 | |
| // creating and renewing certificates automatically. It is designed to
 | |
| // configure sites for HTTPS by default.
 | |
| package https
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"io/ioutil"
 | |
| 	"net"
 | |
| 	"os"
 | |
| 
 | |
| 	"github.com/miekg/coredns/server"
 | |
| 	"github.com/xenolf/lego/acme"
 | |
| )
 | |
| 
 | |
| // Activate sets up TLS for each server config in configs
 | |
| // as needed; this consists of acquiring and maintaining
 | |
| // certificates and keys for qualifying configs and enabling
 | |
| // OCSP stapling for all TLS-enabled configs.
 | |
| //
 | |
| // This function may prompt the user to provide an email
 | |
| // address if none is available through other means. It
 | |
| // prefers the email address specified in the config, but
 | |
| // if that is not available it will check the command line
 | |
| // argument. If absent, it will use the most recent email
 | |
| // address from last time. If there isn't one, the user
 | |
| // will be prompted and shown SA link.
 | |
| //
 | |
| // Also note that calling this function activates asset
 | |
| // management automatically, which keeps certificates
 | |
| // renewed and OCSP stapling updated.
 | |
| //
 | |
| // Activate returns the updated list of configs, since
 | |
| // some may have been appended, for example, to redirect
 | |
| // plaintext HTTP requests to their HTTPS counterpart.
 | |
| // This function only appends; it does not splice.
 | |
| func Activate(configs []server.Config) ([]server.Config, error) {
 | |
| 	// just in case previous caller forgot...
 | |
| 	Deactivate()
 | |
| 
 | |
| 	// pre-screen each config and earmark the ones that qualify for managed TLS
 | |
| 	MarkQualified(configs)
 | |
| 
 | |
| 	// place certificates and keys on disk
 | |
| 	err := ObtainCerts(configs, true, false)
 | |
| 	if err != nil {
 | |
| 		return configs, err
 | |
| 	}
 | |
| 
 | |
| 	// update TLS configurations
 | |
| 	err = EnableTLS(configs, true)
 | |
| 	if err != nil {
 | |
| 		return configs, err
 | |
| 	}
 | |
| 
 | |
| 	// renew all relevant certificates that need renewal. this is important
 | |
| 	// to do right away for a couple reasons, mainly because each restart,
 | |
| 	// the renewal ticker is reset, so if restarts happen more often than
 | |
| 	// the ticker interval, renewals would never happen. but doing
 | |
| 	// it right away at start guarantees that renewals aren't missed.
 | |
| 	err = renewManagedCertificates(true)
 | |
| 	if err != nil {
 | |
| 		return configs, err
 | |
| 	}
 | |
| 
 | |
| 	// keep certificates renewed and OCSP stapling updated
 | |
| 	go maintainAssets(stopChan)
 | |
| 
 | |
| 	return configs, nil
 | |
| }
 | |
| 
 | |
| // Deactivate cleans up long-term, in-memory resources
 | |
| // allocated by calling Activate(). Essentially, it stops
 | |
| // the asset maintainer from running, meaning that certificates
 | |
| // will not be renewed, OCSP staples will not be updated, etc.
 | |
| func Deactivate() (err error) {
 | |
| 	defer func() {
 | |
| 		if rec := recover(); rec != nil {
 | |
| 			err = errors.New("already deactivated")
 | |
| 		}
 | |
| 	}()
 | |
| 	close(stopChan)
 | |
| 	stopChan = make(chan struct{})
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // MarkQualified scans each config and, if it qualifies for managed
 | |
| // TLS, it sets the Managed field of the TLSConfig to true.
 | |
| func MarkQualified(configs []server.Config) {
 | |
| 	for i := 0; i < len(configs); i++ {
 | |
| 		if ConfigQualifies(configs[i]) {
 | |
| 			configs[i].TLS.Managed = true
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // ObtainCerts obtains certificates for all these configs as long as a
 | |
| // certificate does not already exist on disk. It does not modify the
 | |
| // configs at all; it only obtains and stores certificates and keys to
 | |
| // the disk. If allowPrompts is true, the user may be shown a prompt.
 | |
| // If proxyACME is true, the ACME challenges will be proxied to our alt port.
 | |
| func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error {
 | |
| 	// We group configs by email so we don't make the same clients over and
 | |
| 	// over. This has the potential to prompt the user for an email, but we
 | |
| 	// prevent that by assuming that if we already have a listener that can
 | |
| 	// proxy ACME challenge requests, then the server is already running and
 | |
| 	// the operator is no longer present.
 | |
| 	groupedConfigs := groupConfigsByEmail(configs, allowPrompts)
 | |
| 
 | |
| 	for email, group := range groupedConfigs {
 | |
| 		// Wait as long as we can before creating the client, because it
 | |
| 		// may not be needed, for example, if we already have what we
 | |
| 		// need on disk. Creating a client involves the network and
 | |
| 		// potentially prompting the user, etc., so only do if necessary.
 | |
| 		var client *ACMEClient
 | |
| 
 | |
| 		for _, cfg := range group {
 | |
| 			if existingCertAndKey(cfg.Host) {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// Now we definitely do need a client
 | |
| 			if client == nil {
 | |
| 				var err error
 | |
| 				client, err = NewACMEClient(email, allowPrompts)
 | |
| 				if err != nil {
 | |
| 					return errors.New("error creating client: " + err.Error())
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// c.Configure assumes that allowPrompts == !proxyACME,
 | |
| 			// but that's not always true. For example, a restart where
 | |
| 			// the user isn't present and we're not listening on port 80.
 | |
| 			// TODO: This could probably be refactored better.
 | |
| 			if proxyACME {
 | |
| 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
 | |
| 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
 | |
| 				client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
 | |
| 			} else {
 | |
| 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, ""))
 | |
| 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, ""))
 | |
| 				client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
 | |
| 			}
 | |
| 
 | |
| 			err := client.Obtain([]string{cfg.Host})
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // groupConfigsByEmail groups configs by the email address to be used by an
 | |
| // ACME client. It only groups configs that have TLS enabled and that are
 | |
| // marked as Managed. If userPresent is true, the operator MAY be prompted
 | |
| // for an email address.
 | |
| func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config {
 | |
| 	initMap := make(map[string][]server.Config)
 | |
| 	for _, cfg := range configs {
 | |
| 		if !cfg.TLS.Managed {
 | |
| 			continue
 | |
| 		}
 | |
| 		leEmail := getEmail(cfg, userPresent)
 | |
| 		initMap[leEmail] = append(initMap[leEmail], cfg)
 | |
| 	}
 | |
| 	return initMap
 | |
| }
 | |
| 
 | |
| // EnableTLS configures each config to use TLS according to default settings.
 | |
| // It will only change configs that are marked as managed, and assumes that
 | |
| // certificates and keys are already on disk. If loadCertificates is true,
 | |
| // the certificates will be loaded from disk into the cache for this process
 | |
| // to use. If false, TLS will still be enabled and configured with default
 | |
| // settings, but no certificates will be parsed loaded into the cache, and
 | |
| // the returned error value will always be nil.
 | |
| func EnableTLS(configs []server.Config, loadCertificates bool) error {
 | |
| 	for i := 0; i < len(configs); i++ {
 | |
| 		if !configs[i].TLS.Managed {
 | |
| 			continue
 | |
| 		}
 | |
| 		configs[i].TLS.Enabled = true
 | |
| 		if loadCertificates {
 | |
| 			_, err := cacheManagedCertificate(configs[i].Host, false)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 		setDefaultTLSParams(&configs[i])
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // hostHasOtherPort returns true if there is another config in the list with the same
 | |
| // hostname that has port otherPort, or false otherwise. All the configs are checked
 | |
| // against the hostname of allConfigs[thisConfigIdx].
 | |
| func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool {
 | |
| 	for i, otherCfg := range allConfigs {
 | |
| 		if i == thisConfigIdx {
 | |
| 			continue // has to be a config OTHER than the one we're comparing against
 | |
| 		}
 | |
| 		if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // ConfigQualifies returns true if cfg qualifies for
 | |
| // fully managed TLS (but not on-demand TLS, which is
 | |
| // not considered here). It does NOT check to see if a
 | |
| // cert and key already exist for the config. If the
 | |
| // config does qualify, you should set cfg.TLS.Managed
 | |
| // to true and check that instead, because the process of
 | |
| // setting up the config may make it look like it
 | |
| // doesn't qualify even though it originally did.
 | |
| func ConfigQualifies(cfg server.Config) bool {
 | |
| 	return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key
 | |
| 
 | |
| 		// user can force-disable automatic HTTPS for this host
 | |
| 		cfg.Port != "80" &&
 | |
| 		cfg.TLS.LetsEncryptEmail != "off" &&
 | |
| 
 | |
| 		// we get can't certs for some kinds of hostnames, but
 | |
| 		// on-demand TLS allows empty hostnames at startup
 | |
| 		cfg.TLS.OnDemand
 | |
| }
 | |
| 
 | |
| // existingCertAndKey returns true if the host has a certificate
 | |
| // and private key in storage already, false otherwise.
 | |
| func existingCertAndKey(host string) bool {
 | |
| 	_, err := os.Stat(storage.SiteCertFile(host))
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	_, err = os.Stat(storage.SiteKeyFile(host))
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // saveCertResource saves the certificate resource to disk. This
 | |
| // includes the certificate file itself, the private key, and the
 | |
| // metadata file.
 | |
| func saveCertResource(cert acme.CertificateResource) error {
 | |
| 	err := os.MkdirAll(storage.Site(cert.Domain), 0700)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Save cert
 | |
| 	err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Save private key
 | |
| 	err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Save cert metadata
 | |
| 	jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Revoke revokes the certificate for host via ACME protocol.
 | |
| func Revoke(host string) error {
 | |
| 	if !existingCertAndKey(host) {
 | |
| 		return errors.New("no certificate and key for " + host)
 | |
| 	}
 | |
| 
 | |
| 	email := getEmail(server.Config{Host: host}, true)
 | |
| 	if email == "" {
 | |
| 		return errors.New("email is required to revoke")
 | |
| 	}
 | |
| 
 | |
| 	client, err := NewACMEClient(email, true)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	certFile := storage.SiteCertFile(host)
 | |
| 	certBytes, err := ioutil.ReadFile(certFile)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	err = client.RevokeCertificate(certBytes)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	err = os.Remove(certFile)
 | |
| 	if err != nil {
 | |
| 		return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	// DefaultEmail represents the Let's Encrypt account email to use if none provided
 | |
| 	DefaultEmail string
 | |
| 
 | |
| 	// Agreed indicates whether user has agreed to the Let's Encrypt SA
 | |
| 	Agreed bool
 | |
| 
 | |
| 	// CAUrl represents the base URL to the CA's ACME endpoint
 | |
| 	CAUrl string
 | |
| )
 | |
| 
 | |
| // AlternatePort is the port on which the acme client will open a
 | |
| // listener and solve the CA's challenges. If this alternate port
 | |
| // is used instead of the default port (80 or 443), then the
 | |
| // default port for the challenge must be forwarded to this one.
 | |
| const AlternatePort = "5033"
 | |
| 
 | |
| // KeyType is the type to use for new keys.
 | |
| // This shouldn't need to change except for in tests;
 | |
| // the size can be drastically reduced for speed.
 | |
| var KeyType = acme.EC384
 | |
| 
 | |
| // stopChan is used to signal the maintenance goroutine
 | |
| // to terminate.
 | |
| var stopChan chan struct{}
 |