mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-02 22:27:10 +01:00
tls: Some bug fixes, basic rate limiting, max_certs setting
This commit is contained in:
parent
d25a3e95e4
commit
216a617249
7 changed files with 105 additions and 46 deletions
|
@ -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) {
|
||||||
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
|
||||||
|
|
||||||
// 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
|
||||||
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 {
|
||||||
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 (
|
||||||
"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 (
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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)
|
||||||
//
|
//
|
||||||
// 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
|
||||||
// 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) {
|
||||||
// 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) {
|
||||||
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) {
|
||||||
//
|
//
|
||||||
// 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) {
|
||||||
// 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) {
|
||||||
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 {
|
||||||
// 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 (
|
||||||
"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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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) {
|
||||||
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 (
|
||||||
"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 {
|
||||||
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`
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|
Loading…
Reference in a new issue