diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index a7bb3b1de0d..66b38a73105 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -124,6 +124,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var certSelector caddytls.CustomCertSelectionPolicy var acmeIssuer *caddytls.ACMEIssuer var keyType string + var keyFile string var internalIssuer *caddytls.InternalIssuer var issuers []certmagic.Issuer var certManagers []certmagic.Manager @@ -272,6 +273,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } keyType = arg[0] + case "key_file": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + keyFile = arg[0] + case "eab": arg := h.RemainingArgs() if len(arg) != 2 { @@ -591,6 +599,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) { }) } + if keyFile != "" { + configVals = append(configVals, ConfigValue{ + Class: "tls.key_file", + Value: keyFile, + }) + } + // on-demand TLS if onDemand { configVals = append(configVals, ConfigValue{ diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index f985cff9e5a..882babdd636 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -55,6 +55,7 @@ func init() { RegisterGlobalOption("on_demand_tls", parseOptOnDemand) RegisterGlobalOption("local_certs", parseOptTrue) RegisterGlobalOption("key_type", parseOptSingleString) + RegisterGlobalOption("key_file", parseOptSingleString) RegisterGlobalOption("auto_https", parseOptAutoHTTPS) RegisterGlobalOption("metrics", parseMetricsOptions) RegisterGlobalOption("servers", parseServerOptions) diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index d14bd17fbed..59dd93decf9 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -143,6 +143,10 @@ func (st ServerType) buildTLSApp( ap.KeyType = keyTypeVals[0].Value.(string) } + if keyFileVals, ok := sblock.pile["tls.key_file"]; ok { + ap.KeyFile = keyFileVals[0].Value.(string) + } + if renewalWindowRatioVals, ok := sblock.pile["tls.renewal_window_ratio"]; ok { ap.RenewalWindowRatio = renewalWindowRatioVals[0].Value.(float64) } else if globalRenewalWindowRatio, ok := options["renewal_window_ratio"]; ok { @@ -611,9 +615,10 @@ func newBaseAutomationPolicy( issuers, hasIssuers := options["cert_issuer"] _, hasLocalCerts := options["local_certs"] keyType, hasKeyType := options["key_type"] + keyFile, hasKeyFile := options["key_file"] ocspStapling, hasOCSPStapling := options["ocsp_stapling"] renewalWindowRatio, hasRenewalWindowRatio := options["renewal_window_ratio"] - hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling || hasRenewalWindowRatio + hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasKeyFile || hasOCSPStapling || hasRenewalWindowRatio globalACMECA := options["acme_ca"] globalACMECARoot := options["acme_ca_root"] @@ -636,6 +641,10 @@ func newBaseAutomationPolicy( ap.KeyType = keyType.(string) } + if hasKeyFile { + ap.KeyFile = keyFile.(string) + } + if hasIssuers && hasLocalCerts { return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer") } @@ -727,6 +736,7 @@ outer: bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && aps[i].MustStaple == aps[j].MustStaple && aps[i].KeyType == aps[j].KeyType && + aps[i].KeyFile == aps[j].KeyFile && aps[i].OnDemand == aps[j].OnDemand && aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys && aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio { diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index e69b5ad2f8f..d1b4f8d9918 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -132,6 +132,10 @@ type AutomationPolicy struct { // Supported values: `ed25519`, `p256`, `p384`, `rsa2048`, `rsa4096`. KeyType string `json:"key_type,omitempty"` + // Path to a custom private key file to use for TLS management. + // If specified, Caddy will not generate a new key but will use this one. + KeyFile string `json:"key_file,omitempty"` + // Optionally configure a separate storage module associated with this // manager, instead of using Caddy's global/default-configured storage. StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` @@ -243,19 +247,32 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { } } - keyType := ap.KeyType - if keyType != "" { - var err error - keyType, err = caddy.NewReplacer().ReplaceOrErr(ap.KeyType, true, true) + var keySource certmagic.KeyGenerator + + if ap.KeyFile != "" { + keyFilePath, err := caddy.NewReplacer().ReplaceOrErr(ap.KeyFile, true, true) if err != nil { - return fmt.Errorf("invalid key type %s: %s", ap.KeyType, err) + return fmt.Errorf("invalid key file path %s: %v", ap.KeyFile, err) } - if _, ok := supportedCertKeyTypes[keyType]; !ok { - return fmt.Errorf("unrecognized key type: %s", keyType) + + keySource = certmagic.FileKeyGenerator{ + KeyFilename: keyFilePath, + } + } else { + keyType := ap.KeyType + if keyType != "" { + var err error + keyType, err = caddy.NewReplacer().ReplaceOrErr(ap.KeyType, true, true) + if err != nil { + return fmt.Errorf("invalid key type %s: %s", ap.KeyType, err) + } + if _, ok := supportedCertKeyTypes[keyType]; !ok { + return fmt.Errorf("unrecognized key type: %s", keyType) + } + } + keySource = certmagic.StandardKeyGenerator{ + KeyType: supportedCertKeyTypes[keyType], } - } - keySource := certmagic.StandardKeyGenerator{ - KeyType: supportedCertKeyTypes[keyType], } storage := ap.storage