339 lines
11 KiB
Go
339 lines
11 KiB
Go
// Package https facilitates the management of TLS assets and integrates
|
|
// Let's Encrypt functionality into Caddy 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{}
|