diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 336c6999f..2262864ac 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -64,6 +64,7 @@ func init() { RegisterGlobalOption("preferred_chains", parseOptPreferredChains) RegisterGlobalOption("persist_config", parseOptPersistConfig) RegisterGlobalOption("dns", parseOptDNS) + RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers) RegisterGlobalOption("ech", parseOptECH) } @@ -305,6 +306,15 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) { return val, nil } +func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + resolvers := d.RemainingArgs() + if len(resolvers) == 0 { + return nil, d.ArgErr() + } + return resolvers, nil +} + func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() // consume option name diff --git a/caddyconfig/httpcaddyfile/options_test.go b/caddyconfig/httpcaddyfile/options_test.go index bc9e88134..524187f30 100644 --- a/caddyconfig/httpcaddyfile/options_test.go +++ b/caddyconfig/httpcaddyfile/options_test.go @@ -1,9 +1,11 @@ package httpcaddyfile import ( + "encoding/json" "testing" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddytls" _ "github.com/caddyserver/caddy/v2/modules/logging" ) @@ -62,3 +64,105 @@ func TestGlobalLogOptionSyntax(t *testing.T) { } } } + +func TestGlobalResolversOption(t *testing.T) { + tests := []struct { + name string + input string + expectResolvers []string + expectError bool + }{ + { + name: "single resolver", + input: `{ + tls_resolvers 1.1.1.1 + } + example.com { + }`, + expectResolvers: []string{"1.1.1.1"}, + expectError: false, + }, + { + name: "two resolvers", + input: `{ + tls_resolvers 1.1.1.1 8.8.8.8 + } + example.com { + }`, + expectResolvers: []string{"1.1.1.1", "8.8.8.8"}, + expectError: false, + }, + { + name: "multiple resolvers", + input: `{ + tls_resolvers 1.1.1.1 8.8.8.8 9.9.9.9 + } + example.com { + }`, + expectResolvers: []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"}, + expectError: false, + }, + { + name: "no resolvers specified", + input: `{ + } + example.com { + }`, + expectResolvers: nil, + expectError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + adapter := caddyfile.Adapter{ + ServerType: ServerType{}, + } + + out, _, err := adapter.Adapt([]byte(tc.input), nil) + + if (err != nil) != tc.expectError { + t.Errorf("error expectation failed. Expected error: %v, got: %v", tc.expectError, err) + return + } + + if tc.expectError { + return + } + + // Parse the output JSON to check resolvers + var config struct { + Apps struct { + TLS *caddytls.TLS `json:"tls"` + } `json:"apps"` + } + + if err := json.Unmarshal(out, &config); err != nil { + t.Errorf("failed to unmarshal output: %v", err) + return + } + + // Check if resolvers match expected + if config.Apps.TLS == nil { + if tc.expectResolvers != nil { + t.Errorf("Expected TLS config with resolvers %v, but TLS config is nil", tc.expectResolvers) + } + return + } + + actualResolvers := config.Apps.TLS.Resolvers + if len(tc.expectResolvers) == 0 && len(actualResolvers) == 0 { + return // Both empty, ok + } + if len(actualResolvers) != len(tc.expectResolvers) { + t.Errorf("Expected %d resolvers, got %d. Expected: %v, got: %v", len(tc.expectResolvers), len(actualResolvers), tc.expectResolvers, actualResolvers) + return + } + for j, expected := range tc.expectResolvers { + if actualResolvers[j] != expected { + t.Errorf("Resolver %d mismatch. Expected: %s, got: %s", j, expected, actualResolvers[j]) + } + } + }) + } +} diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 30948f84f..6682aedf7 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -362,6 +362,11 @@ func (st ServerType) buildTLSApp( tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil) } + // set up "global" (to the TLS app) DNS resolvers config + if globalResolvers, ok := options["tls_resolvers"]; ok && globalResolvers != nil { + tlsApp.Resolvers = globalResolvers.([]string) + } + // set up ECH from Caddyfile options if ech, ok := options["ech"].(*caddytls.ECH); ok { tlsApp.EncryptedClientHello = ech @@ -624,6 +629,15 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 { acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration) } + // apply global resolvers if DNS challenge is configured and resolvers are not already set + globalResolvers := options["tls_resolvers"] + if globalResolvers != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil { + // Check if DNS challenge is actually configured + hasDNSChallenge := globalACMEDNSok || acmeIssuer.Challenges.DNS.ProviderRaw != nil + if hasDNSChallenge && len(acmeIssuer.Challenges.DNS.Resolvers) == 0 { + acmeIssuer.Challenges.DNS.Resolvers = globalResolvers.([]string) + } + } return nil } diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers.caddyfiletest new file mode 100644 index 000000000..7043b5da3 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers.caddyfiletest @@ -0,0 +1,77 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 + acme_dns +} + +example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + }, + { + "ca": "https://acme.zerossl.com/v2/DV90", + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_http_challenge.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_http_challenge.caddyfiletest new file mode 100644 index 000000000..d375dc711 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_http_challenge.caddyfiletest @@ -0,0 +1,38 @@ +{ + tls_resolvers 1.1.1.1 8.8.8.8 +} + +example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_dns_inherit.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_dns_inherit.caddyfiletest new file mode 100644 index 000000000..20385f84b --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_dns_inherit.caddyfiletest @@ -0,0 +1,72 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 +} + +example.com { + tls { + dns mock + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "example.com" + ], + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_override.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_override.caddyfiletest new file mode 100644 index 000000000..27f7d09d3 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_local_override.caddyfiletest @@ -0,0 +1,98 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 + acme_dns +} + +example.com { + tls { + resolvers 9.9.9.9 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "example.com" + ], + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "9.9.9.9" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + }, + { + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + }, + { + "ca": "https://acme.zerossl.com/v2/DV90", + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/global_options_resolvers_mixed.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_resolvers_mixed.caddyfiletest new file mode 100644 index 000000000..3a4b5571c --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_resolvers_mixed.caddyfiletest @@ -0,0 +1,112 @@ +{ + email test@example.com + dns mock + tls_resolvers 1.1.1.1 8.8.8.8 + acme_dns +} + +site1.example.com { +} + +site2.example.com { + tls { + resolvers 9.9.9.9 8.8.4.4 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "site1.example.com" + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "site2.example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "site2.example.com" + ], + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "9.9.9.9", + "8.8.4.4" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + }, + { + "issuers": [ + { + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + }, + { + "ca": "https://acme.zerossl.com/v2/DV90", + "challenges": { + "dns": { + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + }, + "email": "test@example.com", + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + }, + "resolvers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + } +} diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go index aeb4eab8e..cceadc401 100644 --- a/modules/caddypki/acmeserver/acmeserver.go +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -40,6 +40,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddypki" + "github.com/caddyserver/caddy/v2/modules/caddytls" ) func init() { @@ -287,7 +288,19 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) { // makeClient creates an ACME client which will use a custom // resolver instead of net.DefaultResolver. func (ash Handler) makeClient() (acme.Client, error) { - for _, v := range ash.Resolvers { + // If no local resolvers are configured, check for global resolvers from TLS app + resolversToUse := ash.Resolvers + if len(resolversToUse) == 0 { + tlsAppIface, err := ash.ctx.App("tls") + if err == nil { + tlsApp := tlsAppIface.(*caddytls.TLS) + if len(tlsApp.Resolvers) > 0 { + resolversToUse = tlsApp.Resolvers + } + } + } + + for _, v := range resolversToUse { addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53) if err != nil { return nil, err diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 7b49c0208..c64753a85 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -125,6 +125,13 @@ type TLS struct { DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"` dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) + // The default DNS resolvers to use for TLS-related DNS operations, specifically + // for ACME DNS challenges and ACME server DNS validations. + // If not specified, the system default resolvers will be used. + // + // EXPERIMENTAL: Subject to change. + Resolvers []string `json:"resolvers,omitempty"` + certificateLoaders []CertificateLoader automateNames map[string]struct{} ctx caddy.Context