Commit 689591ef authored by Kevin Stock's avatar Kevin Stock Committed by Matt Holt

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.
parent 27825532
...@@ -148,6 +148,11 @@ type OnDemandState struct { ...@@ -148,6 +148,11 @@ type OnDemandState struct {
// Set from max_certs in tls config, it specifies the // Set from max_certs in tls config, it specifies the
// maximum number of certificates that can be issued. // maximum number of certificates that can be issued.
MaxObtain int32 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 // ObtainCert obtains a certificate for name using c, as long
......
...@@ -19,6 +19,8 @@ import ( ...@@ -19,6 +19,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net/http"
"net/url"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
...@@ -135,8 +137,8 @@ func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIf ...@@ -135,8 +137,8 @@ func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIf
name = strings.ToLower(name) name = strings.ToLower(name)
// Make sure aren't over any applicable limits // Make sure the certificate should be obtained based on config
err := cfg.checkLimitsForObtainingNewCerts(name) err := cfg.checkIfCertShouldBeObtained(name)
if err != nil { if err != nil {
return Certificate{}, err return Certificate{}, err
} }
...@@ -159,10 +161,52 @@ func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIf ...@@ -159,10 +161,52 @@ func (cfg *Config) getCertDuringHandshake(name string, loadIfNecessary, obtainIf
return Certificate{}, fmt.Errorf("no certificate available for %s", name) 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 // checkLimitsForObtainingNewCerts checks to see if name can be issued right
// now according to mitigating factors we keep track of and preferences the // now according the maximum count defined in the configuration. If a non-nil
// user has set. If a non-nil error is returned, do not issue a new certificate // error is returned, do not issue a new certificate for name.
// for name.
func (cfg *Config) checkLimitsForObtainingNewCerts(name string) error { func (cfg *Config) checkLimitsForObtainingNewCerts(name string) error {
// User can set hard limit for number of certs for the process to issue // User can set hard limit for number of certs for the process to issue
if cfg.OnDemandState.MaxObtain > 0 && if cfg.OnDemandState.MaxObtain > 0 &&
......
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
...@@ -49,7 +50,7 @@ func setupTLS(c *caddy.Controller) error { ...@@ -49,7 +50,7 @@ func setupTLS(c *caddy.Controller) error {
config.Enabled = true config.Enabled = true
for c.Next() { for c.Next() {
var certificateFile, keyFile, loadDir, maxCerts string var certificateFile, keyFile, loadDir, maxCerts, askURL string
args := c.RemainingArgs() args := c.RemainingArgs()
switch len(args) { switch len(args) {
...@@ -164,6 +165,9 @@ func setupTLS(c *caddy.Controller) error { ...@@ -164,6 +165,9 @@ func setupTLS(c *caddy.Controller) error {
case "max_certs": case "max_certs":
c.Args(&maxCerts) c.Args(&maxCerts)
config.OnDemand = true config.OnDemand = true
case "ask":
c.Args(&askURL)
config.OnDemand = true
case "dns": case "dns":
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) != 1 { if len(args) != 1 {
...@@ -213,6 +217,19 @@ func setupTLS(c *caddy.Controller) error { ...@@ -213,6 +217,19 @@ func setupTLS(c *caddy.Controller) error {
config.OnDemandState.MaxObtain = int32(maxCertsNum) 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 // don't try to load certificates unless we're supposed to
if !config.Enabled || !config.Manual { if !config.Enabled || !config.Manual {
continue continue
......
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