summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/caddyhttp/autohttps.go99
-rw-r--r--modules/caddypki/ca.go12
-rw-r--r--modules/caddypki/certificates.go5
-rw-r--r--modules/caddypki/crypto.go74
-rw-r--r--modules/caddytls/automation.go44
-rw-r--r--modules/caddytls/certmanagers.go208
-rw-r--r--modules/caddytls/folderloader.go35
7 files changed, 339 insertions, 138 deletions
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go
index 6c37d70..3e38d1b 100644
--- a/modules/caddyhttp/autohttps.go
+++ b/modules/caddyhttp/autohttps.go
@@ -31,13 +31,20 @@ import (
// HTTPS is enabled automatically and by default when
// qualifying hostnames are available from the config.
type AutoHTTPSConfig struct {
- // If true, automatic HTTPS will be entirely disabled.
+ // If true, automatic HTTPS will be entirely disabled,
+ // including certificate management and redirects.
Disabled bool `json:"disable,omitempty"`
// If true, only automatic HTTP->HTTPS redirects will
- // be disabled.
+ // be disabled, but other auto-HTTPS features will
+ // remain enabled.
DisableRedir bool `json:"disable_redirects,omitempty"`
+ // If true, automatic certificate management will be
+ // disabled, but other auto-HTTPS features will
+ // remain enabled.
+ DisableCerts bool `json:"disable_certificates,omitempty"`
+
// Hosts/domain names listed here will not be included
// in automatic HTTPS (they will not have certificates
// loaded nor redirects applied).
@@ -104,12 +111,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
srv.AutoHTTPS = new(AutoHTTPSConfig)
}
if srv.AutoHTTPS.Disabled {
+ app.logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName))
continue
}
// skip if all listeners use the HTTP port
if !srv.listenersUseAnyPortOtherThan(app.httpPort()) {
- app.logger.Info("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server",
+ app.logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server",
zap.String("server_name", srvName),
zap.Int("http_port", app.httpPort()),
)
@@ -166,30 +174,35 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// for all the hostnames we found, filter them so we have
// a deduplicated list of names for which to obtain certs
- for d := range serverDomainSet {
- if certmagic.SubjectQualifiesForCert(d) &&
- !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
- // if a certificate for this name is already loaded,
- // don't obtain another one for it, unless we are
- // supposed to ignore loaded certificates
- if !srv.AutoHTTPS.IgnoreLoadedCerts &&
- len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
- app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
- zap.String("domain", d),
- zap.String("server_name", srvName),
- )
- continue
- }
+ // (only if cert management not disabled for this server)
+ if srv.AutoHTTPS.DisableCerts {
+ app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName))
+ } else {
+ for d := range serverDomainSet {
+ if certmagic.SubjectQualifiesForCert(d) &&
+ !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
+ // if a certificate for this name is already loaded,
+ // don't obtain another one for it, unless we are
+ // supposed to ignore loaded certificates
+ if !srv.AutoHTTPS.IgnoreLoadedCerts &&
+ len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
+ app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
+ zap.String("domain", d),
+ zap.String("server_name", srvName),
+ )
+ continue
+ }
- // most clients don't accept wildcards like *.tld... we
- // can handle that, but as a courtesy, warn the user
- if strings.Contains(d, "*") &&
- strings.Count(strings.Trim(d, "."), ".") == 1 {
- app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)",
- zap.String("domain", d))
- }
+ // most clients don't accept wildcards like *.tld... we
+ // can handle that, but as a courtesy, warn the user
+ if strings.Contains(d, "*") &&
+ strings.Count(strings.Trim(d, "."), ".") == 1 {
+ app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)",
+ zap.String("domain", d))
+ }
- uniqueDomainsForCerts[d] = struct{}{}
+ uniqueDomainsForCerts[d] = struct{}{}
+ }
}
}
@@ -200,12 +213,11 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// nothing left to do if auto redirects are disabled
if srv.AutoHTTPS.DisableRedir {
+ app.logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName))
continue
}
- app.logger.Info("enabling automatic HTTP->HTTPS redirects",
- zap.String("server_name", srvName),
- )
+ app.logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName))
// create HTTP->HTTPS redirects
for _, addr := range srv.Listen {
@@ -459,6 +471,16 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
}
}
+ // if no external managers were configured, enable
+ // implicit Tailscale support for convenience
+ if ap.Managers == nil {
+ ts, err := implicitTailscale(ctx)
+ if err != nil {
+ return err
+ }
+ ap.Managers = []certmagic.CertificateManager{ts}
+ }
+
// while we're here, is this the catch-all/base policy?
if !foundBasePolicy && len(ap.Subjects) == 0 {
basePolicy = ap
@@ -467,8 +489,14 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
}
if basePolicy == nil {
- // no base policy found, we will make one!
- basePolicy = new(caddytls.AutomationPolicy)
+ // no base policy found, we will make one! (with implicit Tailscale integration)
+ ts, err := implicitTailscale(ctx)
+ if err != nil {
+ return err
+ }
+ basePolicy = &caddytls.AutomationPolicy{
+ Managers: []certmagic.CertificateManager{ts},
+ }
}
// if the basePolicy has an existing ACMEIssuer (particularly to
@@ -482,8 +510,8 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
}
}
if baseACMEIssuer == nil {
- // note that this happens if basePolicy.Issuer is nil
- // OR if it is not nil but is not an ACMEIssuer
+ // note that this happens if basePolicy.Issuers is empty
+ // OR if it is not empty but does not have not an ACMEIssuer
baseACMEIssuer = new(caddytls.ACMEIssuer)
}
@@ -653,4 +681,11 @@ func (app *App) automaticHTTPSPhase2() error {
return nil
}
+// implicitTailscale returns a new and provisioned Tailscale module configured to be optional.
+func implicitTailscale(ctx caddy.Context) (caddytls.Tailscale, error) {
+ ts := caddytls.Tailscale{Optional: true}
+ err := ts.Provision(ctx)
+ return ts, err
+}
+
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go
index e3102fb..7fefee6 100644
--- a/modules/caddypki/ca.go
+++ b/modules/caddypki/ca.go
@@ -239,7 +239,7 @@ func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, e
if err != nil {
return nil, nil, fmt.Errorf("loading root key: %v", err)
}
- rootKey, err = pemDecodePrivateKey(rootKeyPEM)
+ rootKey, err = certmagic.PEMDecodePrivateKey(rootKeyPEM)
if err != nil {
return nil, nil, fmt.Errorf("decoding root key: %v", err)
}
@@ -263,7 +263,7 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err err
if err != nil {
return nil, nil, fmt.Errorf("saving root certificate: %v", err)
}
- rootKeyPEM, err := pemEncodePrivateKey(rootKey)
+ rootKeyPEM, err := certmagic.PEMEncodePrivateKey(rootKey)
if err != nil {
return nil, nil, fmt.Errorf("encoding root key: %v", err)
}
@@ -275,7 +275,7 @@ func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err err
return rootCert, rootKey, nil
}
-func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) {
+func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.PrivateKey) (interCert *x509.Certificate, interKey crypto.PrivateKey, err error) {
interCertPEM, err := ca.storage.Load(ca.storageKeyIntermediateCert())
if err != nil {
if _, ok := err.(certmagic.ErrNotExist); !ok {
@@ -301,7 +301,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface
if err != nil {
return nil, nil, fmt.Errorf("loading intermediate key: %v", err)
}
- interKey, err = pemDecodePrivateKey(interKeyPEM)
+ interKey, err = certmagic.PEMDecodePrivateKey(interKeyPEM)
if err != nil {
return nil, nil, fmt.Errorf("decoding intermediate key: %v", err)
}
@@ -310,7 +310,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface
return interCert, interKey, nil
}
-func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) {
+func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.PrivateKey) (interCert *x509.Certificate, interKey crypto.PrivateKey, err error) {
repl := ca.newReplacer()
interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey)
@@ -325,7 +325,7 @@ func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (i
if err != nil {
return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err)
}
- interKeyPEM, err := pemEncodePrivateKey(interKey)
+ interKeyPEM, err := certmagic.PEMEncodePrivateKey(interKey)
if err != nil {
return nil, nil, fmt.Errorf("encoding intermediate key: %v", err)
}
diff --git a/modules/caddypki/certificates.go b/modules/caddypki/certificates.go
index a55c165..bd260da 100644
--- a/modules/caddypki/certificates.go
+++ b/modules/caddypki/certificates.go
@@ -15,6 +15,7 @@
package caddypki
import (
+ "crypto"
"crypto/x509"
"time"
@@ -30,7 +31,7 @@ func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey inte
return newCert(rootProfile)
}
-func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey interface{}) (cert *x509.Certificate, privateKey interface{}, err error) {
+func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey crypto.PrivateKey) (cert *x509.Certificate, privateKey crypto.PrivateKey, err error) {
interProfile, err := x509util.NewIntermediateProfile(commonName, rootCrt, rootKey)
if err != nil {
return
@@ -39,7 +40,7 @@ func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey
return newCert(interProfile)
}
-func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey interface{}, err error) {
+func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey crypto.PrivateKey, err error) {
certBytes, err := profile.CreateCertificate()
if err != nil {
return
diff --git a/modules/caddypki/crypto.go b/modules/caddypki/crypto.go
index dbc6f38..386ce62 100644
--- a/modules/caddypki/crypto.go
+++ b/modules/caddypki/crypto.go
@@ -17,14 +17,12 @@ package caddypki
import (
"bytes"
"crypto"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
- "strings"
+
+ "github.com/caddyserver/certmagic"
)
func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
@@ -45,70 +43,6 @@ func pemEncodeCert(der []byte) ([]byte, error) {
return pemEncode("CERTIFICATE", der)
}
-// pemEncodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes.
-// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported.
-func pemEncodePrivateKey(key crypto.PrivateKey) ([]byte, error) {
- var pemType string
- var keyBytes []byte
- switch key := key.(type) {
- case *ecdsa.PrivateKey:
- var err error
- pemType = "EC"
- keyBytes, err = x509.MarshalECPrivateKey(key)
- if err != nil {
- return nil, err
- }
- case *rsa.PrivateKey:
- pemType = "RSA"
- keyBytes = x509.MarshalPKCS1PrivateKey(key)
- case *ed25519.PrivateKey:
- var err error
- pemType = "ED25519"
- keyBytes, err = x509.MarshalPKCS8PrivateKey(key)
- if err != nil {
- return nil, err
- }
- default:
- return nil, fmt.Errorf("unsupported key type: %T", key)
- }
- return pemEncode(pemType+" PRIVATE KEY", keyBytes)
-}
-
-// pemDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
-// Borrowed from Go standard library, to handle various private key and PEM block types.
-// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
-// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238)
-// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported.
-func pemDecodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) {
- keyBlockDER, _ := pem.Decode(keyPEMBytes)
- if keyBlockDER == nil {
- return nil, fmt.Errorf("no PEM data found")
- }
-
- if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") {
- return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type)
- }
-
- if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil {
- return key, nil
- }
-
- if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil {
- switch key := key.(type) {
- case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
- return key, nil
- default:
- return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key)
- }
- }
-
- if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil {
- return key, nil
- }
-
- return nil, fmt.Errorf("unknown private key type")
-}
-
func pemEncode(blockType string, b []byte) ([]byte, error) {
var buf bytes.Buffer
err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: b})
@@ -137,7 +71,7 @@ type KeyPair struct {
}
// Load loads the certificate and key.
-func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) {
+func (kp KeyPair) Load() (*x509.Certificate, crypto.Signer, error) {
switch kp.Format {
case "", "pem_file":
certData, err := os.ReadFile(kp.Certificate)
@@ -153,7 +87,7 @@ func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) {
if err != nil {
return nil, nil, err
}
- key, err := pemDecodePrivateKey(keyData)
+ key, err := certmagic.PEMDecodePrivateKey(keyData)
if err != nil {
return nil, nil, err
}
diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go
index 2a701bf..95b1772 100644
--- a/modules/caddytls/automation.go
+++ b/modules/caddytls/automation.go
@@ -89,6 +89,14 @@ type AutomationPolicy struct {
// zerossl.
IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
+ // Modules that can get a custom certificate to use for any
+ // given TLS handshake at handshake-time. Custom certificates
+ // can be useful if another entity is managing certificates
+ // and Caddy need only get it and serve it.
+ //
+ // TODO: This is an EXPERIMENTAL feature. It is subject to change or removal.
+ ManagersRaw []json.RawMessage `json:"get_certificate,omitempty" caddy:"namespace=tls.get_certificate inline_key=via"`
+
// If true, certificates will be requested with MustStaple. Not all
// CAs support this, and there are potentially serious consequences
// of enabling this feature without proper threat modeling.
@@ -113,7 +121,7 @@ type AutomationPolicy struct {
// If true, certificates will be managed "on demand"; that is, during
// TLS handshakes or when needed, as opposed to at startup or config
- // load.
+ // load. This enables On-Demand TLS for this policy.
OnDemand bool `json:"on_demand,omitempty"`
// Disables OCSP stapling. Disabling OCSP stapling puts clients at
@@ -129,10 +137,12 @@ type AutomationPolicy struct {
// EXPERIMENTAL. Subject to change.
OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"`
- // Issuers stores the decoded issuer parameters. This is only
- // used to populate an underlying certmagic.Config's Issuers
- // field; it is not referenced thereafter.
- Issuers []certmagic.Issuer `json:"-"`
+ // Issuers and Managers store the decoded issuer and manager modules;
+ // they are only used to populate an underlying certmagic.Config's
+ // fields during provisioning so that the modules can survive a
+ // re-provisioning.
+ Issuers []certmagic.Issuer `json:"-"`
+ Managers []certmagic.CertificateManager `json:"-"`
magic *certmagic.Config
storage certmagic.Storage
@@ -153,6 +163,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
ap.storage = cmStorage
}
+ // on-demand TLS
var ond *certmagic.OnDemandConfig
if ap.OnDemand {
ond = &certmagic.OnDemandConfig{
@@ -176,6 +187,22 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
}
}
+ // we don't store loaded modules directly in the certmagic config since
+ // policy provisioning may happen more than once (during auto-HTTPS) and
+ // loading a module clears its config bytes; thus, load the module and
+ // store them on the policy before putting it on the config
+
+ // load and provision any cert manager modules
+ if ap.ManagersRaw != nil {
+ vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw")
+ if err != nil {
+ return fmt.Errorf("loading external certificate manager modules: %v", err)
+ }
+ for _, getCertVal := range vals.([]interface{}) {
+ ap.Managers = append(ap.Managers, getCertVal.(certmagic.CertificateManager))
+ }
+ }
+
// load and provision any explicitly-configured issuer modules
if ap.IssuersRaw != nil {
val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw")
@@ -225,9 +252,10 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
DisableStapling: ap.DisableOCSPStapling,
ResponderOverrides: ap.OCSPOverrides,
},
- Storage: storage,
- Issuers: issuers,
- Logger: tlsApp.logger,
+ Storage: storage,
+ Issuers: issuers,
+ Managers: ap.Managers,
+ Logger: tlsApp.logger,
}
ap.magic = certmagic.New(tlsApp.certCache, template)
diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go
new file mode 100644
index 0000000..653e9f5
--- /dev/null
+++ b/modules/caddytls/certmanagers.go
@@ -0,0 +1,208 @@
+package caddytls
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/certmagic"
+ "github.com/tailscale/tscert"
+ "go.uber.org/zap"
+)
+
+func init() {
+ caddy.RegisterModule(Tailscale{})
+ caddy.RegisterModule(HTTPCertGetter{})
+}
+
+// Tailscale is a module that can get certificates from the local Tailscale process.
+type Tailscale struct {
+ // If true, this module will operate in "best-effort" mode and
+ // ignore "soft" errors; i.e. try Tailscale, and if it doesn't connect
+ // or return a certificate, oh well. Failure to connect to Tailscale
+ // results in a no-op instead of an error. Intended for the use case
+ // where this module is added implicitly for convenience, even if
+ // Tailscale isn't necessarily running.
+ Optional bool `json:"optional,omitempty"`
+
+ logger *zap.Logger
+}
+
+// CaddyModule returns the Caddy module information.
+func (Tailscale) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "tls.get_certificate.tailscale",
+ New: func() caddy.Module { return new(Tailscale) },
+ }
+}
+
+func (ts *Tailscale) Provision(ctx caddy.Context) error {
+ ts.logger = ctx.Logger(ts)
+ return nil
+}
+
+func (ts Tailscale) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ canGetCert, err := ts.canHazCertificate(ctx, hello)
+ if err == nil && !canGetCert {
+ return nil, nil // pass-thru: Tailscale can't offer a cert for this name
+ }
+ if err != nil {
+ ts.logger.Warn("could not get status; will try to get certificate anyway", zap.Error(err))
+ }
+ return tscert.GetCertificate(hello)
+}
+
+// canHazCertificate returns true if Tailscale reports it can get a certificate for the given ClientHello.
+func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (bool, error) {
+ if ts.Optional && !strings.HasSuffix(strings.ToLower(hello.ServerName), tailscaleDomainAliasEnding) {
+ return false, nil
+ }
+ status, err := tscert.GetStatus(ctx)
+ if err != nil {
+ if ts.Optional {
+ return false, nil // ignore error if we don't expect/require it to work anyway
+ }
+ return false, err
+ }
+ for _, domain := range status.CertDomains {
+ if certmagic.MatchWildcard(hello.ServerName, domain) {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// UnmarshalCaddyfile deserializes Caddyfile tokens into ts.
+//
+// ... tailscale
+//
+func (Tailscale) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+ }
+ return nil
+}
+
+// tailscaleDomainAliasEnding is the ending for all Tailscale custom domains.
+const tailscaleDomainAliasEnding = ".ts.net"
+
+// HTTPCertGetter can get a certificate via HTTP(S) request.
+type HTTPCertGetter struct {
+ // The URL from which to download the certificate. Required.
+ //
+ // The URL will be augmented with query string parameters taken
+ // from the TLS handshake:
+ //
+ // - server_name: The SNI value
+ // - signature_schemes: Comma-separated list of hex IDs of signatures
+ // - cipher_suites: Comma-separated list of hex IDs of cipher suites
+ //
+ // To be valid, the response must be HTTP 200 with a PEM body
+ // consisting of blocks for the certificate chain and the private
+ // key.
+ URL string `json:"url,omitempty"`
+
+ ctx context.Context
+}
+
+// CaddyModule returns the Caddy module information.
+func (hcg HTTPCertGetter) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "tls.get_certificate.http",
+ New: func() caddy.Module { return new(HTTPCertGetter) },
+ }
+}
+
+func (hcg *HTTPCertGetter) Provision(ctx caddy.Context) error {
+ hcg.ctx = ctx
+ if hcg.URL == "" {
+ return fmt.Errorf("URL is required")
+ }
+ return nil
+}
+
+func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ sigs := make([]string, len(hello.SignatureSchemes))
+ for i, sig := range hello.SignatureSchemes {
+ sigs[i] = fmt.Sprintf("%x", uint16(sig)) // you won't believe what %x uses if the val is a Stringer
+ }
+ suites := make([]string, len(hello.CipherSuites))
+ for i, cs := range hello.CipherSuites {
+ suites[i] = fmt.Sprintf("%x", cs)
+ }
+
+ parsed, err := url.Parse(hcg.URL)
+ if err != nil {
+ return nil, err
+ }
+ qs := parsed.Query()
+ qs.Set("server_name", hello.ServerName)
+ qs.Set("signature_schemes", strings.Join(sigs, ","))
+ qs.Set("cipher_suites", strings.Join(suites, ","))
+ parsed.RawQuery = qs.Encode()
+
+ req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("got HTTP %d", resp.StatusCode)
+ }
+
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("error reading response body: %v", err)
+ }
+
+ cert, err := tlsCertFromCertAndKeyPEMBundle(bodyBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ return &cert, nil
+}
+
+// UnmarshalCaddyfile deserializes Caddyfile tokens into ts.
+//
+// ... http <url>
+//
+func (hcg *HTTPCertGetter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ hcg.URL = d.Val()
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ return d.Err("block not allowed here")
+ }
+ }
+ return nil
+}
+
+// Interface guards
+var (
+ _ certmagic.CertificateManager = (*Tailscale)(nil)
+ _ caddy.Provisioner = (*Tailscale)(nil)
+ _ caddyfile.Unmarshaler = (*Tailscale)(nil)
+
+ _ certmagic.CertificateManager = (*HTTPCertGetter)(nil)
+ _ caddy.Provisioner = (*HTTPCertGetter)(nil)
+ _ caddyfile.Unmarshaler = (*HTTPCertGetter)(nil)
+)
diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go
index 0ff0629..33b31a5 100644
--- a/modules/caddytls/folderloader.go
+++ b/modules/caddytls/folderloader.go
@@ -61,10 +61,14 @@ func (fl FolderLoader) LoadCertificates() ([]Certificate, error) {
return nil
}
- cert, err := x509CertFromCertAndKeyPEMFile(fpath)
+ bundle, err := os.ReadFile(fpath)
if err != nil {
return err
}
+ cert, err := tlsCertFromCertAndKeyPEMBundle(bundle)
+ if err != nil {
+ return fmt.Errorf("%s: %w", fpath, err)
+ }
certs = append(certs, Certificate{Certificate: cert})
@@ -77,12 +81,7 @@ func (fl FolderLoader) LoadCertificates() ([]Certificate, error) {
return certs, nil
}
-func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) {
- bundle, err := os.ReadFile(fpath)
- if err != nil {
- return tls.Certificate{}, err
- }
-
+func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) {
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
var foundKey bool // use only the first key in the file
@@ -96,8 +95,7 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) {
if derBlock.Type == "CERTIFICATE" {
// Re-encode certificate as PEM, appending to certificate chain
- err = pem.Encode(certBuilder, derBlock)
- if err != nil {
+ if err := pem.Encode(certBuilder, derBlock); err != nil {
return tls.Certificate{}, err
}
} else if derBlock.Type == "EC PARAMETERS" {
@@ -105,18 +103,16 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) {
// parameters and key (parameter block should come first)
if !foundKey {
// Encode parameters
- err = pem.Encode(keyBuilder, derBlock)
- if err != nil {
+ if err := pem.Encode(keyBuilder, derBlock); err != nil {
return tls.Certificate{}, err
}
// Key must immediately follow
derBlock, bundle = pem.Decode(bundle)
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
- return tls.Certificate{}, fmt.Errorf("%s: expected elliptic private key to immediately follow EC parameters", fpath)
+ return tls.Certificate{}, fmt.Errorf("expected elliptic private key to immediately follow EC parameters")
}
- err = pem.Encode(keyBuilder, derBlock)
- if err != nil {
+ if err := pem.Encode(keyBuilder, derBlock); err != nil {
return tls.Certificate{}, err
}
foundKey = true
@@ -124,28 +120,27 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) {
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
// RSA key
if !foundKey {
- err = pem.Encode(keyBuilder, derBlock)
- if err != nil {
+ if err := pem.Encode(keyBuilder, derBlock); err != nil {
return tls.Certificate{}, err
}
foundKey = true
}
} else {
- return tls.Certificate{}, fmt.Errorf("%s: unrecognized PEM block type: %s", fpath, derBlock.Type)
+ return tls.Certificate{}, fmt.Errorf("unrecognized PEM block type: %s", derBlock.Type)
}
}
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
if len(certPEMBytes) == 0 {
- return tls.Certificate{}, fmt.Errorf("%s: failed to parse PEM data", fpath)
+ return tls.Certificate{}, fmt.Errorf("failed to parse PEM data")
}
if len(keyPEMBytes) == 0 {
- return tls.Certificate{}, fmt.Errorf("%s: no private key block found", fpath)
+ return tls.Certificate{}, fmt.Errorf("no private key block found")
}
cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes)
if err != nil {
- return tls.Certificate{}, fmt.Errorf("%s: making X509 key pair: %v", fpath, err)
+ return tls.Certificate{}, fmt.Errorf("making X509 key pair: %v", err)
}
return cert, nil