524 lines
16 KiB
Go
524 lines
16 KiB
Go
// Copyright 2015 Matthew Holt
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package certmagic automates the obtaining and renewal of TLS certificates,
|
|
// including TLS & HTTPS best practices such as robust OCSP stapling, caching,
|
|
// HTTP->HTTPS redirects, and more.
|
|
//
|
|
// Its high-level API serves your HTTP handlers over HTTPS if you simply give
|
|
// the domain name(s) and the http.Handler; CertMagic will create and run
|
|
// the HTTPS server for you, fully managing certificates during the lifetime
|
|
// of the server. Similarly, it can be used to start TLS listeners or return
|
|
// a ready-to-use tls.Config -- whatever layer you need TLS for, CertMagic
|
|
// makes it easy. See the HTTPS, Listen, and TLS functions for that.
|
|
//
|
|
// If you need more control, create a Config using New() and then call
|
|
// Manage() on the config; but you'll have to be sure to solve the HTTP
|
|
// and TLS-ALPN challenges yourself (unless you disabled them or use the
|
|
// DNS challenge) by using the provided Config.GetCertificate function
|
|
// in your tls.Config and/or Config.HTTPChallangeHandler in your HTTP
|
|
// handler.
|
|
//
|
|
// See the package's README for more instruction.
|
|
package certmagic
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/xenolf/lego/certcrypto"
|
|
"github.com/xenolf/lego/challenge"
|
|
)
|
|
|
|
// HTTPS serves mux for all domainNames using the HTTP
|
|
// and HTTPS ports, redirecting all HTTP requests to HTTPS.
|
|
//
|
|
// This high-level convenience function is opinionated and
|
|
// applies sane defaults for production use, including
|
|
// timeouts for HTTP requests and responses. To allow very
|
|
// long-lived requests or connections, you should make your
|
|
// own http.Server values and use this package's Listen(),
|
|
// TLS(), or Config.TLSConfig() functions to customize to
|
|
// your needs. For example, servers which need to support
|
|
// large uploads or downloads with slow clients may need to
|
|
// use longer timeouts, thus this function is not suitable.
|
|
//
|
|
// Calling this function signifies your acceptance to
|
|
// the CA's Subscriber Agreement and/or Terms of Service.
|
|
func HTTPS(domainNames []string, mux http.Handler) error {
|
|
if mux == nil {
|
|
mux = http.DefaultServeMux
|
|
}
|
|
|
|
cfg, err := manageWithDefaultConfig(domainNames, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpWg.Add(1)
|
|
defer httpWg.Done()
|
|
|
|
// if we haven't made listeners yet, do so now,
|
|
// and clean them up when all servers are done
|
|
lnMu.Lock()
|
|
if httpLn == nil && httpsLn == nil {
|
|
httpLn, err = net.Listen("tcp", fmt.Sprintf(":%d", HTTPPort))
|
|
if err != nil {
|
|
lnMu.Unlock()
|
|
return err
|
|
}
|
|
|
|
httpsLn, err = tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig())
|
|
if err != nil {
|
|
httpLn.Close()
|
|
httpLn = nil
|
|
lnMu.Unlock()
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
httpWg.Wait()
|
|
lnMu.Lock()
|
|
httpLn.Close()
|
|
httpsLn.Close()
|
|
lnMu.Unlock()
|
|
}()
|
|
}
|
|
hln, hsln := httpLn, httpsLn
|
|
lnMu.Unlock()
|
|
|
|
// create HTTP/S servers that are configured
|
|
// with sane default timeouts and appropriate
|
|
// handlers (the HTTP server solves the HTTP
|
|
// challenge and issues redirects to HTTPS,
|
|
// while the HTTPS server simply serves the
|
|
// user's handler)
|
|
httpServer := &http.Server{
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
ReadTimeout: 5 * time.Second,
|
|
WriteTimeout: 5 * time.Second,
|
|
IdleTimeout: 5 * time.Second,
|
|
Handler: cfg.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler)),
|
|
}
|
|
httpsServer := &http.Server{
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 2 * time.Minute,
|
|
IdleTimeout: 5 * time.Minute,
|
|
Handler: mux,
|
|
}
|
|
|
|
log.Printf("%v Serving HTTP->HTTPS on %s and %s",
|
|
domainNames, hln.Addr(), hsln.Addr())
|
|
|
|
go httpServer.Serve(hln)
|
|
return httpsServer.Serve(hsln)
|
|
}
|
|
|
|
func httpRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
|
toURL := "https://"
|
|
|
|
// since we redirect to the standard HTTPS port, we
|
|
// do not need to include it in the redirect URL
|
|
requestHost, _, err := net.SplitHostPort(r.Host)
|
|
if err != nil {
|
|
requestHost = r.Host // host probably did not contain a port
|
|
}
|
|
|
|
toURL += requestHost
|
|
toURL += r.URL.RequestURI()
|
|
|
|
// get rid of this disgusting unencrypted HTTP connection 🤢
|
|
w.Header().Set("Connection", "close")
|
|
|
|
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
|
|
}
|
|
|
|
// TLS enables management of certificates for domainNames
|
|
// and returns a valid tls.Config.
|
|
//
|
|
// Because this is a convenience function that returns
|
|
// only a tls.Config, it does not assume HTTP is being
|
|
// served on the HTTP port, so the HTTP challenge is
|
|
// disabled (no HTTPChallengeHandler is necessary).
|
|
//
|
|
// Calling this function signifies your acceptance to
|
|
// the CA's Subscriber Agreement and/or Terms of Service.
|
|
func TLS(domainNames []string) (*tls.Config, error) {
|
|
cfg, err := manageWithDefaultConfig(domainNames, true)
|
|
return cfg.TLSConfig(), err
|
|
}
|
|
|
|
// Listen manages certificates for domainName and returns a
|
|
// TLS listener.
|
|
//
|
|
// Because this convenience function returns only a TLS-enabled
|
|
// listener and does not presume HTTP is also being served,
|
|
// the HTTP challenge will be disabled.
|
|
//
|
|
// Calling this function signifies your acceptance to
|
|
// the CA's Subscriber Agreement and/or Terms of Service.
|
|
func Listen(domainNames []string) (net.Listener, error) {
|
|
cfg, err := manageWithDefaultConfig(domainNames, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tls.Listen("tcp", fmt.Sprintf(":%d", HTTPSPort), cfg.TLSConfig())
|
|
}
|
|
|
|
// Manage obtains certificates for domainNames and keeps them
|
|
// renewed using the returned Config.
|
|
//
|
|
// You will need to ensure that you use a TLS config that gets
|
|
// certificates from this Config and that the HTTP and TLS-ALPN
|
|
// challenges can be solved. The easiest way to do this is to
|
|
// use cfg.TLSConfig() as your TLS config and to wrap your
|
|
// HTTP handler with cfg.HTTPChallengeHandler(). If you don't
|
|
// have an HTTP server, you will need to disable the HTTP
|
|
// challenge.
|
|
//
|
|
// If you already have a TLS config you want to use, you can
|
|
// simply set its GetCertificate field to cfg.GetCertificate.
|
|
//
|
|
// Calling this function signifies your acceptance to
|
|
// the CA's Subscriber Agreement and/or Terms of Service.
|
|
func Manage(domainNames []string) (cfg *Config, err error) {
|
|
return manageWithDefaultConfig(domainNames, false)
|
|
}
|
|
|
|
// manageWithDefaultConfig returns a TLS configuration that
|
|
// is fully managed for the given names, optionally
|
|
// with the HTTP challenge disabled.
|
|
func manageWithDefaultConfig(domainNames []string, disableHTTPChallenge bool) (*Config, error) {
|
|
cfg := NewDefault()
|
|
cfg.DisableHTTPChallenge = disableHTTPChallenge
|
|
return cfg, cfg.Manage(domainNames)
|
|
}
|
|
|
|
// OnDemandConfig contains some state relevant for providing
|
|
// on-demand TLS.
|
|
type OnDemandConfig struct {
|
|
// If set, this function will be the absolute
|
|
// authority on whether the hostname (according
|
|
// to SNI) is allowed to try to get a cert.
|
|
DecisionFunc func(name string) error
|
|
|
|
// If no DecisionFunc is set, this whitelist
|
|
// is the absolute authority as to whether
|
|
// a certificate should be allowed to be tried.
|
|
// Names are compared against SNI value.
|
|
HostWhitelist []string
|
|
|
|
// If no DecisionFunc or HostWhitelist are set,
|
|
// then an HTTP request will be made to AskURL
|
|
// to determine if a certificate should be
|
|
// obtained. If the request fails or the response
|
|
// is anything other than 2xx status code, the
|
|
// issuance will be denied.
|
|
AskURL *url.URL
|
|
|
|
// If no DecisionFunc, HostWhitelist, or AskURL
|
|
// are set, then only this many certificates may
|
|
// be obtained on-demand; this field is required
|
|
// if all others are empty, otherwise, all cert
|
|
// issuances will fail.
|
|
MaxObtain int32
|
|
|
|
// The number of certificates that have been issued on-demand
|
|
// by this config. It is only safe to modify this count atomically.
|
|
// If it reaches MaxObtain, on-demand issuances must fail.
|
|
obtainedCount int32
|
|
}
|
|
|
|
// Allowed returns whether the issuance for name is allowed according to o.
|
|
func (o *OnDemandConfig) Allowed(name string) error {
|
|
// The decision function has absolute authority, if set
|
|
if o.DecisionFunc != nil {
|
|
return o.DecisionFunc(name)
|
|
}
|
|
|
|
// Otherwise, the host whitelist has decision authority
|
|
if len(o.HostWhitelist) > 0 {
|
|
return o.checkWhitelistForObtainingNewCerts(name)
|
|
}
|
|
|
|
// Otherwise, a URL is checked for permission to issue this cert
|
|
if o.AskURL != nil {
|
|
return o.checkURLForObtainingNewCerts(name)
|
|
}
|
|
|
|
// Otherwise use the limit defined by the "max_certs" setting
|
|
return o.checkLimitsForObtainingNewCerts(name)
|
|
}
|
|
|
|
func (o *OnDemandConfig) whitelistContains(name string) bool {
|
|
for _, n := range o.HostWhitelist {
|
|
if strings.ToLower(n) == strings.ToLower(name) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (o *OnDemandConfig) checkWhitelistForObtainingNewCerts(name string) error {
|
|
if !o.whitelistContains(name) {
|
|
return fmt.Errorf("%s: name is not whitelisted", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *OnDemandConfig) checkURLForObtainingNewCerts(name string) error {
|
|
client := http.Client{
|
|
Timeout: 10 * time.Second,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return fmt.Errorf("following http redirects is not allowed")
|
|
},
|
|
}
|
|
|
|
// Copy the URL from the config in order to modify it for this request
|
|
askURL := new(url.URL)
|
|
*askURL = *o.AskURL
|
|
|
|
query := askURL.Query()
|
|
query.Set("domain", name)
|
|
askURL.RawQuery = query.Encode()
|
|
|
|
resp, err := client.Get(askURL.String())
|
|
if err != nil {
|
|
return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", o.AskURL, name, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
return fmt.Errorf("certificate for hostname '%s' not allowed, non-2xx status code %d returned from %v", name, resp.StatusCode, o.AskURL)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkLimitsForObtainingNewCerts checks to see if name can be issued right
|
|
// now according the maximum count defined in the configuration. If a non-nil
|
|
// error is returned, do not issue a new certificate for name.
|
|
func (o *OnDemandConfig) checkLimitsForObtainingNewCerts(name string) error {
|
|
if o.MaxObtain == 0 {
|
|
return fmt.Errorf("%s: no certificates allowed to be issued on-demand", name)
|
|
}
|
|
|
|
// User can set hard limit for number of certs for the process to issue
|
|
if o.MaxObtain > 0 &&
|
|
atomic.LoadInt32(&o.obtainedCount) >= o.MaxObtain {
|
|
return fmt.Errorf("%s: maximum certificates issued (%d)", name, o.MaxObtain)
|
|
}
|
|
|
|
// Make sure name hasn't failed a challenge recently
|
|
failedIssuanceMu.RLock()
|
|
when, ok := failedIssuance[name]
|
|
failedIssuanceMu.RUnlock()
|
|
if ok {
|
|
return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
|
|
}
|
|
|
|
// Make sure, if we've issued a few certificates already, that we haven't
|
|
// issued any recently
|
|
lastIssueTimeMu.Lock()
|
|
since := time.Since(lastIssueTime)
|
|
lastIssueTimeMu.Unlock()
|
|
if atomic.LoadInt32(&o.obtainedCount) >= 10 && since < 10*time.Minute {
|
|
return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
|
|
}
|
|
|
|
// Good to go 👍
|
|
return nil
|
|
}
|
|
|
|
// failedIssuance is a set of names that we recently failed to get a
|
|
// certificate for from the ACME CA. They are removed after some time.
|
|
// When a name is in this map, do not issue a certificate for it on-demand.
|
|
var failedIssuance = make(map[string]time.Time)
|
|
var failedIssuanceMu sync.RWMutex
|
|
|
|
// lastIssueTime records when we last obtained a certificate successfully.
|
|
// If this value is recent, do not make any on-demand certificate requests.
|
|
var lastIssueTime time.Time
|
|
var lastIssueTimeMu sync.Mutex
|
|
|
|
// isLoopback returns true if the hostname of addr looks
|
|
// explicitly like a common local hostname. addr must only
|
|
// be a host or a host:port combination.
|
|
func isLoopback(addr string) bool {
|
|
host, _, err := net.SplitHostPort(strings.ToLower(addr))
|
|
if err != nil {
|
|
host = addr // happens if the addr is only a hostname
|
|
}
|
|
return host == "localhost" ||
|
|
strings.Trim(host, "[]") == "::1" ||
|
|
strings.HasPrefix(host, "127.")
|
|
}
|
|
|
|
// isInternal returns true if the IP of addr
|
|
// belongs to a private network IP range. addr
|
|
// must only be an IP or an IP:port combination.
|
|
// Loopback addresses are considered false.
|
|
func isInternal(addr string) bool {
|
|
privateNetworks := []string{
|
|
"10.0.0.0/8",
|
|
"172.16.0.0/12",
|
|
"192.168.0.0/16",
|
|
"fc00::/7",
|
|
}
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
host = addr // happens if the addr is just a hostname, missing port
|
|
// if we encounter an error, the brackets need to be stripped
|
|
// because SplitHostPort didn't do it for us
|
|
host = strings.Trim(host, "[]")
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
for _, privateNetwork := range privateNetworks {
|
|
_, ipnet, _ := net.ParseCIDR(privateNetwork)
|
|
if ipnet.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Package defaults
|
|
var (
|
|
// The endpoint of the directory for the ACME
|
|
// CA we are to use
|
|
CA = LetsEncryptProductionCA
|
|
|
|
// The email address to use when creating or
|
|
// selecting an existing ACME server account
|
|
Email string
|
|
|
|
// The synchronization implementation - all
|
|
// instances of certmagic in a cluster must
|
|
// use the same value here, otherwise some
|
|
// cert operations will not be properly
|
|
// coordinated
|
|
Sync Locker
|
|
|
|
// Set to true if agreed to the CA's
|
|
// subscriber agreement
|
|
Agreed bool
|
|
|
|
// Disable all HTTP challenges
|
|
DisableHTTPChallenge bool
|
|
|
|
// Disable all TLS-ALPN challenges
|
|
DisableTLSALPNChallenge bool
|
|
|
|
// How long before expiration to renew certificates
|
|
RenewDurationBefore = DefaultRenewDurationBefore
|
|
|
|
// How long before expiration to require a renewed
|
|
// certificate when in interactive mode, like when
|
|
// the program is first starting up (see
|
|
// mholt/caddy#1680). A wider window between
|
|
// RenewDurationBefore and this value will suppress
|
|
// errors under duress (bad) but hopefully this duration
|
|
// will give it enough time for the blockage to be
|
|
// relieved.
|
|
RenewDurationBeforeAtStartup = DefaultRenewDurationBeforeAtStartup
|
|
|
|
// An optional event callback clients can set
|
|
// to subscribe to certain things happening
|
|
// internally by this config; invocations are
|
|
// synchronous, so make them return quickly!
|
|
OnEvent func(event string, data interface{})
|
|
|
|
// The host (ONLY the host, not port) to listen
|
|
// on if necessary to start a listener to solve
|
|
// an ACME challenge
|
|
ListenHost string
|
|
|
|
// The alternate port to use for the ACME HTTP
|
|
// challenge; if non-empty, this port will be
|
|
// used instead of HTTPChallengePort to spin up
|
|
// a listener for the HTTP challenge
|
|
AltHTTPPort int
|
|
|
|
// The alternate port to use for the ACME
|
|
// TLS-ALPN challenge; the system must forward
|
|
// TLSALPNChallengePort to this port for
|
|
// challenge to succeed
|
|
AltTLSALPNPort int
|
|
|
|
// The DNS provider to use when solving the
|
|
// ACME DNS challenge
|
|
DNSProvider challenge.Provider
|
|
|
|
// The type of key to use when generating
|
|
// certificates
|
|
KeyType = certcrypto.RSA2048
|
|
|
|
// The state needed to operate on-demand TLS
|
|
OnDemand *OnDemandConfig
|
|
|
|
// Add the must staple TLS extension to the
|
|
// CSR generated by lego/acme
|
|
MustStaple bool
|
|
)
|
|
|
|
const (
|
|
// HTTPChallengePort is the officially-designated port for
|
|
// the HTTP challenge according to the ACME spec.
|
|
HTTPChallengePort = 80
|
|
|
|
// TLSALPNChallengePort is the officially-designated port for
|
|
// the TLS-ALPN challenge according to the ACME spec.
|
|
TLSALPNChallengePort = 443
|
|
)
|
|
|
|
// Some well-known CA endpoints available to use.
|
|
const (
|
|
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
|
|
)
|
|
|
|
// Port variables must remain their defaults unless you
|
|
// forward packets from the defaults to whatever these
|
|
// are set to; otherwise ACME challenges will fail.
|
|
var (
|
|
// HTTPPort is the port on which to serve HTTP
|
|
// and, by extension, the HTTP challenge (unless
|
|
// AltHTTPPort is set).
|
|
HTTPPort = 80
|
|
|
|
// HTTPSPort is the port on which to serve HTTPS
|
|
// and, by extension, the TLS-ALPN challenge
|
|
// (unless AltTLSALPNPort is set).
|
|
HTTPSPort = 443
|
|
)
|
|
|
|
// Variables for conveniently serving HTTPS
|
|
var (
|
|
httpLn, httpsLn net.Listener
|
|
lnMu sync.Mutex
|
|
httpWg sync.WaitGroup
|
|
)
|