Commit 216a6172 authored by Matthew Holt's avatar Matthew Holt

tls: Some bug fixes, basic rate limiting, max_certs setting

parent d25a3e95
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"path" "path"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/mholt/caddy/caddy/https" "github.com/mholt/caddy/caddy/https"
...@@ -317,6 +318,7 @@ func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { ...@@ -317,6 +318,7 @@ func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
return nil, err return nil, err
} }
cdyfile = loadedGob.Caddyfile cdyfile = loadedGob.Caddyfile
atomic.StoreInt32(https.OnDemandIssuedCount, loadedGob.OnDemandTLSCertsIssued)
} }
// Try user's loader // Try user's loader
......
...@@ -63,10 +63,12 @@ var signalParentOnce sync.Once ...@@ -63,10 +63,12 @@ var signalParentOnce sync.Once
// caddyfileGob maps bind address to index of the file descriptor // caddyfileGob maps bind address to index of the file descriptor
// in the Files array passed to the child process. It also contains // in the Files array passed to the child process. It also contains
// the caddyfile contents. Used only during graceful restarts. // the caddyfile contents and other state needed by the new process.
// Used only during graceful restarts where a new process is spawned.
type caddyfileGob struct { type caddyfileGob struct {
ListenerFds map[string]uintptr ListenerFds map[string]uintptr
Caddyfile Input Caddyfile Input
OnDemandTLSCertsIssued int32
} }
// IsRestart returns whether this process is, according // IsRestart returns whether this process is, according
......
...@@ -3,7 +3,6 @@ package https ...@@ -3,7 +3,6 @@ package https
import ( import (
"crypto/tls" "crypto/tls"
"log" "log"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
...@@ -23,21 +22,16 @@ func RequestCallback(w http.ResponseWriter, r *http.Request) bool { ...@@ -23,21 +22,16 @@ func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
scheme = "https" scheme = "https"
} }
hostname, _, err := net.SplitHostPort(r.Host) upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort)
if err != nil {
hostname = r.Host
}
upstream, err := url.Parse(scheme + "://" + hostname + ":" + AlternatePort)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
log.Printf("[ERROR] letsencrypt handler: %v", err) log.Printf("[ERROR] ACME proxy handler: %v", err)
return true return true
} }
proxy := httputil.NewSingleHostReverseProxy(upstream) proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{ proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client would use self-signed cert TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
} }
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
......
...@@ -7,7 +7,9 @@ import ( ...@@ -7,7 +7,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/mholt/caddy/server" "github.com/mholt/caddy/server"
...@@ -15,11 +17,12 @@ import ( ...@@ -15,11 +17,12 @@ import (
) )
// GetCertificate gets a certificate to satisfy clientHello as long as // GetCertificate gets a certificate to satisfy clientHello as long as
// the certificate is already cached in memory. // the certificate is already cached in memory. It will not be loaded
// from disk or obtained from the CA during the handshake.
// //
// This function is safe for use as a tls.Config.GetCertificate callback. // This function is safe for use as a tls.Config.GetCertificate callback.
func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := getCertDuringHandshake(clientHello.ServerName, false) cert, err := getCertDuringHandshake(clientHello.ServerName, false, false)
return cert.Certificate, err return cert.Certificate, err
} }
...@@ -31,36 +34,50 @@ func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) ...@@ -31,36 +34,50 @@ func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error)
// //
// This function is safe for use as a tls.Config.GetCertificate callback. // This function is safe for use as a tls.Config.GetCertificate callback.
func GetOrObtainCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { func GetOrObtainCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := getCertDuringHandshake(clientHello.ServerName, true) cert, err := getCertDuringHandshake(clientHello.ServerName, true, true)
return cert.Certificate, err return cert.Certificate, err
} }
// getCertDuringHandshake will get a certificate for name. It first tries // getCertDuringHandshake will get a certificate for name. It first tries
// the in-memory cache, then, if obtainIfNecessary is true, it goes to disk, // the in-memory cache. If no certificate for name is in the cach and if
// then asks the CA for a certificate if necessary. // loadIfNecessary == true, it goes to disk to load it into the cache and
// serve it. If it's not on disk and if obtainIfNecessary == true, the
// certificate will be obtained from the CA, cached, and served. If
// obtainIfNecessary is true, then loadIfNecessary must also be set to true.
// //
// This function is safe for concurrent use. // This function is safe for concurrent use.
func getCertDuringHandshake(name string, obtainIfNecessary bool) (Certificate, error) { func getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
// First check our in-memory cache to see if we've already loaded it // First check our in-memory cache to see if we've already loaded it
cert, ok := getCertificate(name) cert, ok := getCertificate(name)
if ok { if ok {
return cert, nil return cert, nil
} }
if obtainIfNecessary { if loadIfNecessary {
// TODO: Mitigate abuse!
var err error var err error
// Then check to see if we have one on disk // Then check to see if we have one on disk
cert, err := cacheManagedCertificate(name, true) cert, err = cacheManagedCertificate(name, true)
if err != nil { if err == nil {
return cert, err cert, err = handshakeMaintenance(name, cert)
} else if cert.Certificate != nil {
cert, err := handshakeMaintenance(name, cert)
if err != nil { if err != nil {
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err) log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
} }
return cert, err return cert, nil
}
if obtainIfNecessary {
name = strings.ToLower(name)
// Make sure aren't over any applicable limits
if onDemandMaxIssue > 0 && atomic.LoadInt32(OnDemandIssuedCount) >= onDemandMaxIssue {
return Certificate{}, fmt.Errorf("%s: maximum certificates issued (%d)", name, onDemandMaxIssue)
}
failedIssuanceMu.RLock()
when, ok := failedIssuance[name]
failedIssuanceMu.RUnlock()
if ok {
return Certificate{}, fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
} }
// Only option left is to get one from LE, but the name has to qualify first // Only option left is to get one from LE, but the name has to qualify first
...@@ -71,6 +88,7 @@ func getCertDuringHandshake(name string, obtainIfNecessary bool) (Certificate, e ...@@ -71,6 +88,7 @@ func getCertDuringHandshake(name string, obtainIfNecessary bool) (Certificate, e
// By this point, we need to obtain one from the CA. // By this point, we need to obtain one from the CA.
return obtainOnDemandCertificate(name) return obtainOnDemandCertificate(name)
} }
}
return Certificate{}, nil return Certificate{}, nil
} }
...@@ -89,7 +107,7 @@ func obtainOnDemandCertificate(name string) (Certificate, error) { ...@@ -89,7 +107,7 @@ func obtainOnDemandCertificate(name string) (Certificate, error) {
// wait for it to finish obtaining the cert and then we'll use it. // wait for it to finish obtaining the cert and then we'll use it.
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
<-wait <-wait
return getCertDuringHandshake(name, false) // passing in true might result in infinite loop if obtain failed return 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
...@@ -115,11 +133,24 @@ func obtainOnDemandCertificate(name string) (Certificate, error) { ...@@ -115,11 +133,24 @@ func obtainOnDemandCertificate(name string) (Certificate, error) {
client.Configure("") // TODO: which BindHost? client.Configure("") // TODO: which BindHost?
err = client.Obtain([]string{name}) err = client.Obtain([]string{name})
if err != nil { 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()
failedIssuance[name] = time.Now()
go func(name string) {
time.Sleep(5 * time.Minute)
failedIssuanceMu.Lock()
delete(failedIssuance, name)
failedIssuanceMu.Unlock()
}(name)
failedIssuanceMu.Unlock()
return Certificate{}, err return Certificate{}, err
} }
atomic.AddInt32(OnDemandIssuedCount, 1)
// The certificate is on disk; now just start over to load it and serve it // The certificate is on disk; now just start over to load it and serve it
return getCertDuringHandshake(name, false) // pass in false as a fail-safe from infinite-looping return getCertDuringHandshake(name, true, false)
} }
// handshakeMaintenance performs a check on cert for expiration and OCSP // handshakeMaintenance performs a check on cert for expiration and OCSP
...@@ -127,12 +158,6 @@ func obtainOnDemandCertificate(name string) (Certificate, error) { ...@@ -127,12 +158,6 @@ func obtainOnDemandCertificate(name string) (Certificate, error) {
// //
// This function is safe for use by multiple concurrent goroutines. // This function is safe for use by multiple concurrent goroutines.
func handshakeMaintenance(name string, cert Certificate) (Certificate, error) { func handshakeMaintenance(name string, cert Certificate) (Certificate, error) {
// fmt.Println("ON-DEMAND CERT?", cert.OnDemand)
// if !cert.OnDemand {
// return cert, nil
// }
fmt.Println("Checking expiration of cert; on-demand:", cert.OnDemand)
// Check cert expiration // Check cert expiration
timeLeft := cert.NotAfter.Sub(time.Now().UTC()) timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < renewDurationBefore { if timeLeft < renewDurationBefore {
...@@ -173,7 +198,7 @@ func renewDynamicCertificate(name string) (Certificate, error) { ...@@ -173,7 +198,7 @@ func renewDynamicCertificate(name string) (Certificate, error) {
// wait for it to finish, then we'll use the new one. // wait for it to finish, then we'll use the new one.
obtainCertWaitChansMu.Unlock() obtainCertWaitChansMu.Unlock()
<-wait <-wait
return getCertDuringHandshake(name, false) return getCertDuringHandshake(name, true, false)
} }
// looks like it's up to us to do all the work and renew the cert // looks like it's up to us to do all the work and renew the cert
...@@ -201,7 +226,7 @@ func renewDynamicCertificate(name string) (Certificate, error) { ...@@ -201,7 +226,7 @@ func renewDynamicCertificate(name string) (Certificate, error) {
return Certificate{}, err return Certificate{}, err
} }
return getCertDuringHandshake(name, false) return getCertDuringHandshake(name, true, false)
} }
// stapleOCSP staples OCSP information to cert for hostname name. // stapleOCSP staples OCSP information to cert for hostname name.
...@@ -235,3 +260,20 @@ func stapleOCSP(cert *Certificate, pemBundle []byte) error { ...@@ -235,3 +260,20 @@ func stapleOCSP(cert *Certificate, pemBundle []byte) error {
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname. // obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
var obtainCertWaitChans = make(map[string]chan struct{}) var obtainCertWaitChans = make(map[string]chan struct{})
var obtainCertWaitChansMu sync.Mutex var obtainCertWaitChansMu sync.Mutex
// OnDemandIssuedCount is the number of certificates that have been issued
// on-demand by this process. It is only safe to modify this count atomically.
// If it reaches max_certs, on-demand issuances will fail.
var OnDemandIssuedCount = new(int32)
// onDemandMaxIssue is set based on max_certs in tls config. It specifies the
// maximum number of certificates that can be issued.
// TODO: This applies globally, but we should probably make a server-specific
// way to keep track of these limits and counts...
var onDemandMaxIssue int32
// 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.
var failedIssuance = make(map[string]time.Time)
var failedIssuanceMu sync.RWMutex
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/mholt/caddy/caddy/setup" "github.com/mholt/caddy/caddy/setup"
...@@ -27,7 +28,7 @@ func Setup(c *setup.Controller) (middleware.Middleware, error) { ...@@ -27,7 +28,7 @@ func Setup(c *setup.Controller) (middleware.Middleware, error) {
} }
for c.Next() { for c.Next() {
var certificateFile, keyFile, loadDir string var certificateFile, keyFile, loadDir, maxCerts string
args := c.RemainingArgs() args := c.RemainingArgs()
switch len(args) { switch len(args) {
...@@ -80,6 +81,8 @@ func Setup(c *setup.Controller) (middleware.Middleware, error) { ...@@ -80,6 +81,8 @@ func Setup(c *setup.Controller) (middleware.Middleware, error) {
case "load": case "load":
c.Args(&loadDir) c.Args(&loadDir)
c.TLS.Manual = true c.TLS.Manual = true
case "max_certs":
c.Args(&maxCerts)
default: default:
return nil, c.Errf("Unknown keyword '%s'", c.Val()) return nil, c.Errf("Unknown keyword '%s'", c.Val())
} }
...@@ -90,6 +93,20 @@ func Setup(c *setup.Controller) (middleware.Middleware, error) { ...@@ -90,6 +93,20 @@ func Setup(c *setup.Controller) (middleware.Middleware, error) {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
if c.TLS.Manual && maxCerts != "" {
return nil, c.Err("Cannot limit certificate count (max_certs) for manual TLS configurations")
}
if maxCerts != "" {
maxCertsNum, err := strconv.Atoi(maxCerts)
if err != nil || maxCertsNum < 0 {
return nil, c.Err("max_certs must be a positive integer")
}
if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: This is global; should be per-server or per-vhost...
onDemandMaxIssue = int32(maxCertsNum)
}
}
// don't load certificates unless we're supposed to // don't load certificates unless we're supposed to
if !c.TLS.Enabled || !c.TLS.Manual { if !c.TLS.Enabled || !c.TLS.Manual {
continue continue
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"sync/atomic"
"github.com/mholt/caddy/caddy/https" "github.com/mholt/caddy/caddy/https"
) )
...@@ -57,6 +58,7 @@ func Restart(newCaddyfile Input) error { ...@@ -57,6 +58,7 @@ func Restart(newCaddyfile Input) error {
cdyfileGob := caddyfileGob{ cdyfileGob := caddyfileGob{
ListenerFds: make(map[string]uintptr), ListenerFds: make(map[string]uintptr),
Caddyfile: newCaddyfile, Caddyfile: newCaddyfile,
OnDemandTLSCertsIssued: atomic.LoadInt32(https.OnDemandIssuedCount),
} }
// Prepare a pipe to the fork's stdin so it can get the Caddyfile // Prepare a pipe to the fork's stdin so it can get the Caddyfile
......
...@@ -118,7 +118,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` ...@@ -118,7 +118,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
} }
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") { if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'", t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
i, j, test.password, actualRule.Password) i, j, test.password, actualRule.Password(""))
} }
expectedRes := fmt.Sprintf("%v", expectedRule.Resources) expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment