mirror of
https://github.com/caddyserver/caddy.git
synced 2026-04-27 11:19:51 -04:00
httpcaddyfile: inherit global ACME issuer settings in tls shortcuts (#7617)
This commit is contained in:
parent
fdbef2a6ef
commit
c1918ff1ad
@ -550,26 +550,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
}
|
||||
|
||||
case acmeIssuer != nil:
|
||||
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
||||
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
||||
|
||||
// if an ACME CA endpoint was set, the user expects to use that specific one,
|
||||
// not any others that may be defaults, so replace all defaults with that ACME CA
|
||||
if acmeIssuer.CA != "" {
|
||||
defaultIssuers = []certmagic.Issuer{acmeIssuer}
|
||||
}
|
||||
|
||||
// implicit ACME issuers (from various subdirectives) should inherit from
|
||||
// any globally-configured ACME issuer templates, then apply the local
|
||||
// shortcut settings as overrides.
|
||||
defaultIssuers := implicitACMEIssuers(h, acmeIssuer)
|
||||
for _, issuer := range defaultIssuers {
|
||||
// apply settings from the implicitly-configured ACMEIssuer to any
|
||||
// default ACMEIssuers, but preserve each default issuer's CA endpoint,
|
||||
// because, for example, if you configure the DNS challenge, it should
|
||||
// apply to any of the default ACMEIssuers, but you don't want to trample
|
||||
// out their unique CA endpoints
|
||||
if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
|
||||
acmeCopy := *acmeIssuer
|
||||
acmeCopy.CA = iss.CA
|
||||
issuer = &acmeCopy
|
||||
}
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_issuer",
|
||||
Value: issuer,
|
||||
|
||||
@ -3,7 +3,9 @@ package httpcaddyfile
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
||||
@ -166,3 +168,126 @@ func TestGlobalResolversOption(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) {
|
||||
adapter := caddyfile.Adapter{
|
||||
ServerType: ServerType{},
|
||||
}
|
||||
|
||||
input := `{
|
||||
cert_issuer acme {
|
||||
disable_tlsalpn_challenge
|
||||
}
|
||||
}
|
||||
report.company.intern {
|
||||
tls {
|
||||
ca https://deglacme01.company.intern/acme/acme/directory
|
||||
ca_root /etc/certs/company_root2.crt
|
||||
}
|
||||
respond "ok"
|
||||
}`
|
||||
|
||||
out, _, err := adapter.Adapt([]byte(input), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("adapting caddyfile: %v", err)
|
||||
}
|
||||
|
||||
var config struct {
|
||||
Apps struct {
|
||||
TLS *caddytls.TLS `json:"tls"`
|
||||
} `json:"apps"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &config); err != nil {
|
||||
t.Fatalf("unmarshaling adapted config: %v", err)
|
||||
}
|
||||
if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil {
|
||||
t.Fatal("expected tls automation config")
|
||||
}
|
||||
|
||||
var subjectPolicy *caddytls.AutomationPolicy
|
||||
for _, ap := range config.Apps.TLS.Automation.Policies {
|
||||
if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" {
|
||||
subjectPolicy = ap
|
||||
break
|
||||
}
|
||||
}
|
||||
if subjectPolicy == nil {
|
||||
t.Fatal("expected subject-specific automation policy")
|
||||
}
|
||||
if len(subjectPolicy.IssuersRaw) != 1 {
|
||||
t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw))
|
||||
}
|
||||
|
||||
var issuer caddytls.ACMEIssuer
|
||||
if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil {
|
||||
t.Fatalf("unmarshaling issuer: %v", err)
|
||||
}
|
||||
if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" {
|
||||
t.Fatalf("expected custom ACME CA, got %q", issuer.CA)
|
||||
}
|
||||
if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" {
|
||||
t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles)
|
||||
}
|
||||
if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled {
|
||||
t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeACMEIssuers(t *testing.T) {
|
||||
base := &caddytls.ACMEIssuer{
|
||||
Email: "ops@example.com",
|
||||
Challenges: &caddytls.ChallengesConfig{
|
||||
HTTP: &caddytls.HTTPChallengeConfig{
|
||||
AlternatePort: 8080,
|
||||
},
|
||||
TLSALPN: &caddytls.TLSALPNChallengeConfig{
|
||||
Disabled: true,
|
||||
AlternatePort: 8443,
|
||||
},
|
||||
DNS: &caddytls.DNSChallengeConfig{
|
||||
Resolvers: []string{"1.1.1.1"},
|
||||
OverrideDomain: "_acme-challenge.example.net",
|
||||
},
|
||||
},
|
||||
TrustedRootsPEMFiles: []string{"global.pem"},
|
||||
}
|
||||
overrides := &caddytls.ACMEIssuer{
|
||||
CA: "https://deglacme01.company.intern/acme/acme/directory",
|
||||
Challenges: &caddytls.ChallengesConfig{
|
||||
HTTP: &caddytls.HTTPChallengeConfig{
|
||||
Disabled: true,
|
||||
},
|
||||
DNS: &caddytls.DNSChallengeConfig{
|
||||
PropagationTimeout: caddy.Duration(time.Minute),
|
||||
},
|
||||
},
|
||||
TrustedRootsPEMFiles: []string{"site.pem"},
|
||||
}
|
||||
|
||||
merged := mergeACMEIssuers(base, overrides)
|
||||
if merged.CA != overrides.CA {
|
||||
t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA)
|
||||
}
|
||||
if merged.Email != base.Email {
|
||||
t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email)
|
||||
}
|
||||
if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" {
|
||||
t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles)
|
||||
}
|
||||
if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 {
|
||||
t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges)
|
||||
}
|
||||
if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 {
|
||||
t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges)
|
||||
}
|
||||
if merged.Challenges.DNS == nil || merged.Challenges.DNS.PropagationTimeout != caddy.Duration(time.Minute) || len(merged.Challenges.DNS.Resolvers) != 1 || merged.Challenges.DNS.Resolvers[0] != "1.1.1.1" || merged.Challenges.DNS.OverrideDomain != "_acme-challenge.example.net" {
|
||||
t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges)
|
||||
}
|
||||
|
||||
if base.CA != "" {
|
||||
t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA)
|
||||
}
|
||||
if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" {
|
||||
t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles)
|
||||
}
|
||||
}
|
||||
|
||||
@ -612,6 +612,289 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// implicitACMEIssuers returns the issuers to use for ACME-related tls
|
||||
// shortcuts such as ca, ca_root, and dns. If any global cert_issuer options
|
||||
// configure ACME issuers, those become the templates for the local shortcut
|
||||
// configuration; otherwise, default ACME issuers are used.
|
||||
func implicitACMEIssuers(h Helper, acmeIssuer *caddytls.ACMEIssuer) []certmagic.Issuer {
|
||||
globalIssuers, _ := h.Option("cert_issuer").([]certmagic.Issuer)
|
||||
|
||||
var implicitIssuers []certmagic.Issuer
|
||||
for _, issuer := range globalIssuers {
|
||||
acmeWrapper, ok := issuer.(acmeCapable)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
baseIssuer := acmeWrapper.GetACMEIssuer()
|
||||
if baseIssuer == nil {
|
||||
continue
|
||||
}
|
||||
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
|
||||
}
|
||||
if len(implicitIssuers) > 0 {
|
||||
return implicitIssuers
|
||||
}
|
||||
|
||||
// If an ACME CA endpoint was set locally, the user expects to use only that
|
||||
// CA rather than the usual default fallback issuers.
|
||||
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
||||
if acmeIssuer.CA != "" {
|
||||
defaultIssuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
||||
}
|
||||
|
||||
implicitIssuers = make([]certmagic.Issuer, 0, len(defaultIssuers))
|
||||
for _, issuer := range defaultIssuers {
|
||||
acmeWrapper, ok := issuer.(acmeCapable)
|
||||
if !ok {
|
||||
implicitIssuers = append(implicitIssuers, issuer)
|
||||
continue
|
||||
}
|
||||
baseIssuer := acmeWrapper.GetACMEIssuer()
|
||||
if baseIssuer == nil {
|
||||
implicitIssuers = append(implicitIssuers, issuer)
|
||||
continue
|
||||
}
|
||||
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
|
||||
}
|
||||
return implicitIssuers
|
||||
}
|
||||
|
||||
func mergeACMEIssuers(base, overrides *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
|
||||
if base == nil {
|
||||
return cloneACMEIssuer(overrides)
|
||||
}
|
||||
|
||||
merged := cloneACMEIssuer(base)
|
||||
if overrides == nil {
|
||||
return merged
|
||||
}
|
||||
|
||||
if overrides.CA != "" {
|
||||
merged.CA = overrides.CA
|
||||
}
|
||||
if overrides.TestCA != "" {
|
||||
merged.TestCA = overrides.TestCA
|
||||
}
|
||||
if overrides.Email != "" {
|
||||
merged.Email = overrides.Email
|
||||
}
|
||||
if overrides.Profile != "" {
|
||||
merged.Profile = overrides.Profile
|
||||
}
|
||||
if overrides.AccountKey != "" {
|
||||
merged.AccountKey = overrides.AccountKey
|
||||
}
|
||||
if overrides.ExternalAccount != nil {
|
||||
merged.ExternalAccount = cloneACMEEAB(overrides.ExternalAccount)
|
||||
}
|
||||
if overrides.ACMETimeout != 0 {
|
||||
merged.ACMETimeout = overrides.ACMETimeout
|
||||
}
|
||||
if len(overrides.TrustedRootsPEMFiles) > 0 {
|
||||
merged.TrustedRootsPEMFiles = appendUniqueStrings(merged.TrustedRootsPEMFiles, overrides.TrustedRootsPEMFiles...)
|
||||
}
|
||||
if overrides.PreferredChains != nil {
|
||||
merged.PreferredChains = cloneChainPreference(overrides.PreferredChains)
|
||||
}
|
||||
if overrides.CertificateLifetime != 0 {
|
||||
merged.CertificateLifetime = overrides.CertificateLifetime
|
||||
}
|
||||
if len(overrides.NetworkProxyRaw) > 0 {
|
||||
merged.NetworkProxyRaw = slices.Clone(overrides.NetworkProxyRaw)
|
||||
}
|
||||
merged.Challenges = mergeChallengesConfig(merged.Challenges, overrides.Challenges)
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func mergeChallengesConfig(base, overrides *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
|
||||
if base == nil {
|
||||
return cloneChallengesConfig(overrides)
|
||||
}
|
||||
merged := cloneChallengesConfig(base)
|
||||
if overrides == nil {
|
||||
return merged
|
||||
}
|
||||
|
||||
merged.HTTP = mergeHTTPChallengeConfig(merged.HTTP, overrides.HTTP)
|
||||
merged.TLSALPN = mergeTLSALPNChallengeConfig(merged.TLSALPN, overrides.TLSALPN)
|
||||
merged.DNS = mergeDNSChallengeConfig(merged.DNS, overrides.DNS)
|
||||
if overrides.BindHost != "" {
|
||||
merged.BindHost = overrides.BindHost
|
||||
}
|
||||
if overrides.Distributed != nil {
|
||||
value := *overrides.Distributed
|
||||
merged.Distributed = &value
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func mergeHTTPChallengeConfig(base, overrides *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
|
||||
if base == nil {
|
||||
return cloneHTTPChallengeConfig(overrides)
|
||||
}
|
||||
merged := cloneHTTPChallengeConfig(base)
|
||||
if overrides == nil {
|
||||
return merged
|
||||
}
|
||||
|
||||
if overrides.Disabled {
|
||||
merged.Disabled = true
|
||||
}
|
||||
if overrides.AlternatePort != 0 {
|
||||
merged.AlternatePort = overrides.AlternatePort
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func mergeTLSALPNChallengeConfig(base, overrides *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
|
||||
if base == nil {
|
||||
return cloneTLSALPNChallengeConfig(overrides)
|
||||
}
|
||||
merged := cloneTLSALPNChallengeConfig(base)
|
||||
if overrides == nil {
|
||||
return merged
|
||||
}
|
||||
|
||||
if overrides.Disabled {
|
||||
merged.Disabled = true
|
||||
}
|
||||
if overrides.AlternatePort != 0 {
|
||||
merged.AlternatePort = overrides.AlternatePort
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func mergeDNSChallengeConfig(base, overrides *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
|
||||
if base == nil {
|
||||
return cloneDNSChallengeConfig(overrides)
|
||||
}
|
||||
merged := cloneDNSChallengeConfig(base)
|
||||
if overrides == nil {
|
||||
return merged
|
||||
}
|
||||
|
||||
if len(overrides.ProviderRaw) > 0 {
|
||||
merged.ProviderRaw = slices.Clone(overrides.ProviderRaw)
|
||||
}
|
||||
if overrides.PropagationDelay != 0 {
|
||||
merged.PropagationDelay = overrides.PropagationDelay
|
||||
}
|
||||
if overrides.PropagationTimeout != 0 {
|
||||
merged.PropagationTimeout = overrides.PropagationTimeout
|
||||
}
|
||||
if overrides.Resolvers != nil {
|
||||
merged.Resolvers = slices.Clone(overrides.Resolvers)
|
||||
}
|
||||
if overrides.OverrideDomain != "" {
|
||||
merged.OverrideDomain = overrides.OverrideDomain
|
||||
}
|
||||
if overrides.TTL != 0 {
|
||||
merged.TTL = overrides.TTL
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func cloneACMEIssuer(iss *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
|
||||
if iss == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *iss
|
||||
cloned.Challenges = cloneChallengesConfig(iss.Challenges)
|
||||
cloned.ExternalAccount = cloneACMEEAB(iss.ExternalAccount)
|
||||
cloned.TrustedRootsPEMFiles = slices.Clone(iss.TrustedRootsPEMFiles)
|
||||
cloned.PreferredChains = cloneChainPreference(iss.PreferredChains)
|
||||
cloned.NetworkProxyRaw = slices.Clone(iss.NetworkProxyRaw)
|
||||
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneChallengesConfig(cfg *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *cfg
|
||||
cloned.HTTP = cloneHTTPChallengeConfig(cfg.HTTP)
|
||||
cloned.TLSALPN = cloneTLSALPNChallengeConfig(cfg.TLSALPN)
|
||||
cloned.DNS = cloneDNSChallengeConfig(cfg.DNS)
|
||||
if cfg.Distributed != nil {
|
||||
value := *cfg.Distributed
|
||||
cloned.Distributed = &value
|
||||
}
|
||||
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneHTTPChallengeConfig(cfg *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *cfg
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneTLSALPNChallengeConfig(cfg *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *cfg
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneDNSChallengeConfig(cfg *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *cfg
|
||||
cloned.ProviderRaw = slices.Clone(cfg.ProviderRaw)
|
||||
cloned.Resolvers = slices.Clone(cfg.Resolvers)
|
||||
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneACMEEAB(eab *acme.EAB) *acme.EAB {
|
||||
if eab == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *eab
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneChainPreference(pref *caddytls.ChainPreference) *caddytls.ChainPreference {
|
||||
if pref == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := *pref
|
||||
cloned.RootCommonName = slices.Clone(pref.RootCommonName)
|
||||
cloned.AnyCommonName = slices.Clone(pref.AnyCommonName)
|
||||
if pref.Smallest != nil {
|
||||
value := *pref.Smallest
|
||||
cloned.Smallest = &value
|
||||
}
|
||||
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func appendUniqueStrings(existing []string, additions ...string) []string {
|
||||
for _, value := range additions {
|
||||
if !slices.Contains(existing, value) {
|
||||
existing = append(existing, value)
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
||||
// its values from the global options map. It should be used as the base
|
||||
// for any other automation policies. A nil policy (and no error) will be
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user