tls: Add option for backend to approve on-demand cert (#1939)

This adds the ask sub-directive to tls that defines the URL of a backend HTTP service to be queried during the TLS handshake to determine if an on-demand TLS certificate should be acquired for incoming hostnames. When the ask sub-directive is defined, Caddy will query the URL for permission to acquire a cert by making a HTTP GET request to the URL including the requested domain in the query string. If the backend service returns a 2xx response Caddy will acquire a cert. Any other response code (including 3xx redirects) are be considered a rejection and the certificate will not be acquired.
This commit is contained in:
Kevin Stock 2017-11-03 22:01:30 -07:00 committed by Matt Holt
parent 2782553231
commit 689591ef01
3 changed files with 72 additions and 6 deletions

View file

@ -148,6 +148,11 @@ type OnDemandState struct {
// Set from max_certs in tls config, it specifies the
// maximum number of certificates that can be issued.
MaxObtain int32
// The url to call to check if an on-demand tls certificate should
// be issued. If a request to the URL fails or returns a non 2xx
// status on-demand issuances must fail.
AskURL *url.URL
}
// ObtainCert obtains a certificate for name using c, as long

View file

@ -19,6 +19,8 @@ import (
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
@ -135,8 +137,8 @@ func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIf
name = strings.ToLower(name)
// Make sure aren't over any applicable limits
err := cfg.checkLimitsForObtainingNewCerts(name)
// Make sure the certificate should be obtained based on config
err := cfg.checkIfCertShouldBeObtained(name)
if err != nil {
return Certificate{}, err
}
@ -159,10 +161,52 @@ func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIf
return Certificate{}, fmt.Errorf("no certificate available for %s", name)
}
// checkIfCertShouldBeObtained checks to see if an on-demand tls certificate
// should be obtained for a given domain based upon the config settings. If
// a non-nil error is returned, do not issue a new certificate for name.
func (cfg *Config) checkIfCertShouldBeObtained(name string) error {
// If the "ask" URL is defined in the config, use to determine if a
// cert should obtained
if cfg.OnDemandState.AskURL != nil {
return cfg.checkURLForObtainingNewCerts(name)
}
// Otherwise use the limit defined by the "max_certs" setting
return cfg.checkLimitsForObtainingNewCerts(name)
}
func (cfg *Config) checkURLForObtainingNewCerts(name string) error {
client := http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return errors.New("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 = *cfg.OnDemandState.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", cfg.OnDemandState.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, cfg.OnDemandState.AskURL)
}
return nil
}
// checkLimitsForObtainingNewCerts checks to see if name can be issued right
// now according to mitigating factors we keep track of and preferences the
// user has set. If a non-nil error is returned, do not issue a new certificate
// for name.
// 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 (cfg *Config) checkLimitsForObtainingNewCerts(name string) error {
// User can set hard limit for number of certs for the process to issue
if cfg.OnDemandState.MaxObtain > 0 &&

View file

@ -21,6 +21,7 @@ import (
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"path/filepath"
"strconv"
@ -49,7 +50,7 @@ func setupTLS(c *caddy.Controller) error {
config.Enabled = true
for c.Next() {
var certificateFile, keyFile, loadDir, maxCerts string
var certificateFile, keyFile, loadDir, maxCerts, askURL string
args := c.RemainingArgs()
switch len(args) {
@ -164,6 +165,9 @@ func setupTLS(c *caddy.Controller) error {
case "max_certs":
c.Args(&maxCerts)
config.OnDemand = true
case "ask":
c.Args(&askURL)
config.OnDemand = true
case "dns":
args := c.RemainingArgs()
if len(args) != 1 {
@ -213,6 +217,19 @@ func setupTLS(c *caddy.Controller) error {
config.OnDemandState.MaxObtain = int32(maxCertsNum)
}
if askURL != "" {
parsedURL, err := url.Parse(askURL)
if err != nil {
return c.Err("ask must be a valid url")
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return c.Err("ask URL must use http or https")
}
config.OnDemandState.AskURL = parsedURL
}
// don't try to load certificates unless we're supposed to
if !config.Enabled || !config.Manual {
continue