Skip to content

Commit c1918ff

Browse files
authored
httpcaddyfile: inherit global ACME issuer settings in tls shortcuts (#7617)
1 parent fdbef2a commit c1918ff

3 files changed

Lines changed: 412 additions & 19 deletions

File tree

caddyconfig/httpcaddyfile/builtins.go

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -550,26 +550,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
550550
}
551551

552552
case acmeIssuer != nil:
553-
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
554-
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
555-
556-
// if an ACME CA endpoint was set, the user expects to use that specific one,
557-
// not any others that may be defaults, so replace all defaults with that ACME CA
558-
if acmeIssuer.CA != "" {
559-
defaultIssuers = []certmagic.Issuer{acmeIssuer}
560-
}
561-
553+
// implicit ACME issuers (from various subdirectives) should inherit from
554+
// any globally-configured ACME issuer templates, then apply the local
555+
// shortcut settings as overrides.
556+
defaultIssuers := implicitACMEIssuers(h, acmeIssuer)
562557
for _, issuer := range defaultIssuers {
563-
// apply settings from the implicitly-configured ACMEIssuer to any
564-
// default ACMEIssuers, but preserve each default issuer's CA endpoint,
565-
// because, for example, if you configure the DNS challenge, it should
566-
// apply to any of the default ACMEIssuers, but you don't want to trample
567-
// out their unique CA endpoints
568-
if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
569-
acmeCopy := *acmeIssuer
570-
acmeCopy.CA = iss.CA
571-
issuer = &acmeCopy
572-
}
573558
configVals = append(configVals, ConfigValue{
574559
Class: "tls.cert_issuer",
575560
Value: issuer,

caddyconfig/httpcaddyfile/options_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package httpcaddyfile
33
import (
44
"encoding/json"
55
"testing"
6+
"time"
67

8+
"github.com/caddyserver/caddy/v2"
79
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
810
"github.com/caddyserver/caddy/v2/modules/caddytls"
911
_ "github.com/caddyserver/caddy/v2/modules/logging"
@@ -166,3 +168,126 @@ func TestGlobalResolversOption(t *testing.T) {
166168
})
167169
}
168170
}
171+
172+
func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) {
173+
adapter := caddyfile.Adapter{
174+
ServerType: ServerType{},
175+
}
176+
177+
input := `{
178+
cert_issuer acme {
179+
disable_tlsalpn_challenge
180+
}
181+
}
182+
report.company.intern {
183+
tls {
184+
ca https://deglacme01.company.intern/acme/acme/directory
185+
ca_root /etc/certs/company_root2.crt
186+
}
187+
respond "ok"
188+
}`
189+
190+
out, _, err := adapter.Adapt([]byte(input), nil)
191+
if err != nil {
192+
t.Fatalf("adapting caddyfile: %v", err)
193+
}
194+
195+
var config struct {
196+
Apps struct {
197+
TLS *caddytls.TLS `json:"tls"`
198+
} `json:"apps"`
199+
}
200+
if err := json.Unmarshal(out, &config); err != nil {
201+
t.Fatalf("unmarshaling adapted config: %v", err)
202+
}
203+
if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil {
204+
t.Fatal("expected tls automation config")
205+
}
206+
207+
var subjectPolicy *caddytls.AutomationPolicy
208+
for _, ap := range config.Apps.TLS.Automation.Policies {
209+
if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" {
210+
subjectPolicy = ap
211+
break
212+
}
213+
}
214+
if subjectPolicy == nil {
215+
t.Fatal("expected subject-specific automation policy")
216+
}
217+
if len(subjectPolicy.IssuersRaw) != 1 {
218+
t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw))
219+
}
220+
221+
var issuer caddytls.ACMEIssuer
222+
if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil {
223+
t.Fatalf("unmarshaling issuer: %v", err)
224+
}
225+
if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" {
226+
t.Fatalf("expected custom ACME CA, got %q", issuer.CA)
227+
}
228+
if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" {
229+
t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles)
230+
}
231+
if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled {
232+
t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges)
233+
}
234+
}
235+
236+
func TestMergeACMEIssuers(t *testing.T) {
237+
base := &caddytls.ACMEIssuer{
238+
Email: "ops@example.com",
239+
Challenges: &caddytls.ChallengesConfig{
240+
HTTP: &caddytls.HTTPChallengeConfig{
241+
AlternatePort: 8080,
242+
},
243+
TLSALPN: &caddytls.TLSALPNChallengeConfig{
244+
Disabled: true,
245+
AlternatePort: 8443,
246+
},
247+
DNS: &caddytls.DNSChallengeConfig{
248+
Resolvers: []string{"1.1.1.1"},
249+
OverrideDomain: "_acme-challenge.example.net",
250+
},
251+
},
252+
TrustedRootsPEMFiles: []string{"global.pem"},
253+
}
254+
overrides := &caddytls.ACMEIssuer{
255+
CA: "https://deglacme01.company.intern/acme/acme/directory",
256+
Challenges: &caddytls.ChallengesConfig{
257+
HTTP: &caddytls.HTTPChallengeConfig{
258+
Disabled: true,
259+
},
260+
DNS: &caddytls.DNSChallengeConfig{
261+
PropagationTimeout: caddy.Duration(time.Minute),
262+
},
263+
},
264+
TrustedRootsPEMFiles: []string{"site.pem"},
265+
}
266+
267+
merged := mergeACMEIssuers(base, overrides)
268+
if merged.CA != overrides.CA {
269+
t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA)
270+
}
271+
if merged.Email != base.Email {
272+
t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email)
273+
}
274+
if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" {
275+
t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles)
276+
}
277+
if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 {
278+
t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges)
279+
}
280+
if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 {
281+
t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges)
282+
}
283+
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" {
284+
t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges)
285+
}
286+
287+
if base.CA != "" {
288+
t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA)
289+
}
290+
if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" {
291+
t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles)
292+
}
293+
}

0 commit comments

Comments
 (0)