mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
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:
parent
2782553231
commit
689591ef01
3 changed files with 72 additions and 6 deletions
|
@ -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 (
|
||||||
"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
|
||||||
|
|
||||||
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
|
||||||
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 (
|
||||||
"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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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
|
||||||
|
|
Loading…
Reference in a new issue