Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
caddytls: Support multiple issuers (#3862)
* caddytls: Support multiple issuers Defaults are Let's Encrypt and ZeroSSL. There are probably bugs. * Commit updated integration tests, d'oh * Update go.mod
Matt Holt committed
Nov 16, 2020 at 11:05 UTC
13781e67ab1b2553598d0dd1a7153ce3cdbd4879
13 files changed
+307
-234
caddyconfig/httpcaddyfile/builtins.go
+15
-28
@@ -88,7 +88,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
88
var certSelector caddytls.CustomCertSelectionPolicy
89
var acmeIssuer *caddytls.ACMEIssuer
90
var internalIssuer *caddytls.InternalIssuer
91
- var issuer certmagic.Issuer
91
+ var issuers []certmagic.Issuer
92
var onDemand bool
93
94
for h.Next() {
@@ -297,10 +297,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
297
if err != nil {
298
return nil, err
299
}
300
- issuer, ok = unm.(certmagic.Issuer)
300
+ issuer, ok := unm.(certmagic.Issuer)
301
if !ok {
302
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
303
}
304
+ issuers = append(issuers, issuer)
305
306
case "dns":
307
if !h.NextArg() {
@@ -371,42 +372,28 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
372
})
373
}
374
374
- // issuer
375
- if acmeIssuer != nil && internalIssuer != nil {
376
- // the logic to support this would be complex
377
- return nil, h.Err("cannot use both ACME and internal issuers in same server block")
375
+ if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) {
376
+ // some tls subdirectives are shortcuts that implicitly configure issuers, and the
377
+ // user can also configure issuers explicitly using the issuer subdirective; the
378
+ // logic to support both would likely be complex, or at least unintuitive
379
+ return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)")
380
}
379
- if issuer != nil && (acmeIssuer != nil || internalIssuer != nil) {
380
- // similarly, the logic to support this would be complex
381
- return nil, h.Err("when defining an issuer, all its config must be in its block, rather than from separate tls subdirectives")
382
- }
383
- switch {
384
- case issuer != nil:
381
+ for _, issuer := range issuers {
382
configVals = append(configVals, ConfigValue{
383
Class: "tls.cert_issuer",
384
Value: issuer,
385
})
389
-
390
- case internalIssuer != nil:
386
+ }
387
+ if acmeIssuer != nil {
388
configVals = append(configVals, ConfigValue{
389
Class: "tls.cert_issuer",
393
- Value: internalIssuer,
390
+ Value: disambiguateACMEIssuer(acmeIssuer),
391
})
395
-
396
- case acmeIssuer != nil:
397
- // fill in global defaults, if configured
398
- if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
399
- acmeIssuer.Email = email.(string)
400
- }
401
- if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
402
- acmeIssuer.CA = acmeCA.(string)
403
- }
404
- if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
405
- acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
406
- }
392
+ }
393
+ if internalIssuer != nil {
394
configVals = append(configVals, ConfigValue{
395
Class: "tls.cert_issuer",
409
- Value: disambiguateACMEIssuer(acmeIssuer),
396
+ Value: internalIssuer,
397
})
398
}
399
caddyconfig/httpcaddyfile/tlsapp.go
+125
-87
@@ -110,47 +110,43 @@ func (st ServerType) buildTLSApp(
110
111
// certificate issuers
112
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
113
+ var issuers []certmagic.Issuer
114
for _, issuerVal := range issuerVals {
114
- issuer := issuerVal.Value.(certmagic.Issuer)
115
- if ap == catchAllAP && !reflect.DeepEqual(ap.Issuer, issuer) {
116
- return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuer, issuer)
117
- }
118
- ap.Issuer = issuer
115
+ ap.Issuers = append(ap.Issuers, issuerVal.Value.(certmagic.Issuer))
116
+ }
117
+ if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
118
+ return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
119
}
120
}
121
122
// custom bind host
123
for _, cfgVal := range sblock.pile["bind"] {
124
- // if an issuer was already configured and it is NOT an ACME
125
- // issuer, skip, since we intend to adjust only ACME issuers
126
- var acmeIssuer *caddytls.ACMEIssuer
127
- if ap.Issuer != nil {
128
- // ensure we include any issuer that embeds/wraps an underlying ACME issuer
129
- type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
130
- if acmeWrapper, ok := ap.Issuer.(acmeCapable); ok {
124
+ for _, iss := range ap.Issuers {
125
+ // if an issuer was already configured and it is NOT an ACME issuer,
126
+ // skip, since we intend to adjust only ACME issuers; ensure we
127
+ // include any issuer that embeds/wraps an underlying ACME issuer
128
+ var acmeIssuer *caddytls.ACMEIssuer
129
+ if acmeWrapper, ok := iss.(acmeCapable); ok {
130
acmeIssuer = acmeWrapper.GetACMEIssuer()
132
- } else {
133
- break
131
}
135
- }
132
+ if acmeIssuer == nil {
133
+ continue
134
+ }
135
137
- // proceed to configure the ACME issuer's bind host, without
138
- // overwriting any existing settings
139
- if acmeIssuer == nil {
140
- acmeIssuer = new(caddytls.ACMEIssuer)
141
- }
142
- if acmeIssuer.Challenges == nil {
143
- acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
144
- }
145
- if acmeIssuer.Challenges.BindHost == "" {
146
- // only binding to one host is supported
147
- var bindHost string
148
- if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
149
- bindHost = bindHosts[0]
136
+ // proceed to configure the ACME issuer's bind host, without
137
+ // overwriting any existing settings
138
+ if acmeIssuer.Challenges == nil {
139
+ acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
140
+ }
141
+ if acmeIssuer.Challenges.BindHost == "" {
142
+ // only binding to one host is supported
143
+ var bindHost string
144
+ if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
145
+ bindHost = bindHosts[0]
146
+ }
147
+ acmeIssuer.Challenges.BindHost = bindHost
148
}
151
- acmeIssuer.Challenges.BindHost = bindHost
149
}
153
- ap.Issuer = acmeIssuer // we'll encode it later
150
}
151
152
// first make sure this block is allowed to create an automation policy;
@@ -188,7 +184,7 @@ func (st ServerType) buildTLSApp(
184
// that the internal names can use the internal issuer and
185
// the other names can use the default/public/ACME issuer
186
var ap2 *caddytls.AutomationPolicy
191
- if ap.Issuer == nil {
187
+ if len(ap.Issuers) == 0 {
188
var internal, external []string
189
for _, s := range ap.Subjects {
190
if !certmagic.SubjectQualifiesForCert(s) {
@@ -212,7 +208,7 @@ func (st ServerType) buildTLSApp(
208
apCopy := *ap
209
ap2 = &apCopy
210
ap2.Subjects = internal
215
- ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)
211
+ ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
212
}
213
}
214
if tlsApp.Automation == nil {
@@ -277,7 +273,7 @@ func (st ServerType) buildTLSApp(
273
// get internal certificates by default rather than ACME
274
var al caddytls.AutomateLoader
275
internalAP := &caddytls.AutomationPolicy{
280
- IssuerRaw: json.RawMessage(`{"module":"internal"}`),
276
+ IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
277
}
278
for h := range hostsSharedWithHostlessKey {
279
al = append(al, h)
@@ -295,14 +291,48 @@ func (st ServerType) buildTLSApp(
291
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
292
}
293
294
+ // if there are any global options set for issuers (ACME ones in particular), make sure they
295
+ // take effect in every automation policy that does not have any issuers
296
+ if tlsApp.Automation != nil {
297
+ globalEmail := options["email"]
298
+ globalACMECA := options["acme_ca"]
299
+ globalACMECARoot := options["acme_ca_root"]
300
+ globalACMEDNS := options["acme_dns"]
301
+ globalACMEEAB := options["acme_eab"]
302
+ hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil
303
+ if hasGlobalACMEDefaults {
304
+ for _, ap := range tlsApp.Automation.Policies {
305
+ if len(ap.Issuers) == 0 {
306
+ acme, zerosslACME := new(caddytls.ACMEIssuer), new(caddytls.ACMEIssuer)
307
+ zerossl := &caddytls.ZeroSSLIssuer{ACMEIssuer: zerosslACME}
308
+ ap.Issuers = []certmagic.Issuer{acme, zerossl} // TODO: keep this in sync with Caddy's other issuer defaults elsewhere, like in caddytls/automation.go (DefaultIssuers).
309
+
310
+ // if a non-ZeroSSL endpoint is specified, we assume we can't use the ZeroSSL issuer successfully
311
+ if globalACMECA != nil && !strings.Contains(globalACMECA.(string), "zerossl") {
312
+ ap.Issuers = []certmagic.Issuer{acme}
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
// finalize and verify policies; do cleanup
320
if tlsApp.Automation != nil {
300
- // encode any issuer values we created, so they will be rendered in the output
301
- for _, ap := range tlsApp.Automation.Policies {
302
- if ap.Issuer != nil && ap.IssuerRaw == nil {
303
- // encode issuer now that it's all set up
304
- issuerName := ap.Issuer.(caddy.Module).CaddyModule().ID.Name()
305
- ap.IssuerRaw = caddyconfig.JSONModuleObject(ap.Issuer, "module", issuerName, &warnings)
321
+ for i, ap := range tlsApp.Automation.Policies {
322
+ // ensure all issuers have global defaults filled in
323
+ for j, issuer := range ap.Issuers {
324
+ err := fillInGlobalACMEDefaults(issuer, options)
325
+ if err != nil {
326
+ return nil, warnings, fmt.Errorf("filling in global issuer defaults for AP %d, issuer %d: %v", i, j, err)
327
+ }
328
+ }
329
+
330
+ // encode all issuer values we created, so they will be rendered in the output
331
+ if len(ap.Issuers) > 0 && ap.IssuersRaw == nil {
332
+ for _, iss := range ap.Issuers {
333
+ issuerName := iss.(caddy.Module).CaddyModule().ID.Name()
334
+ ap.IssuersRaw = append(ap.IssuersRaw, caddyconfig.JSONModuleObject(iss, "module", issuerName, &warnings))
335
+ }
336
}
337
}
338
@@ -334,6 +364,51 @@ func (st ServerType) buildTLSApp(
364
return tlsApp, warnings, nil
365
}
366
367
+type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
368
+
369
+func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
370
+ acmeWrapper, ok := issuer.(acmeCapable)
371
+ if !ok {
372
+ return nil
373
+ }
374
+ acmeIssuer := acmeWrapper.GetACMEIssuer()
375
+ if acmeIssuer == nil {
376
+ return nil
377
+ }
378
+
379
+ globalEmail := options["email"]
380
+ globalACMECA := options["acme_ca"]
381
+ globalACMECARoot := options["acme_ca_root"]
382
+ globalACMEDNS := options["acme_dns"]
383
+ globalACMEEAB := options["acme_eab"]
384
+
385
+ if globalEmail != nil && acmeIssuer.Email == "" {
386
+ acmeIssuer.Email = globalEmail.(string)
387
+ }
388
+ if globalACMECA != nil && acmeIssuer.CA == "" {
389
+ acmeIssuer.CA = globalACMECA.(string)
390
+ }
391
+ if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
392
+ acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
393
+ }
394
+ if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
395
+ provName := globalACMEDNS.(string)
396
+ dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
397
+ if err != nil {
398
+ return fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
399
+ }
400
+ acmeIssuer.Challenges = &caddytls.ChallengesConfig{
401
+ DNS: &caddytls.DNSChallengeConfig{
402
+ ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, nil),
403
+ },
404
+ }
405
+ }
406
+ if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
407
+ acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
408
+ }
409
+ return nil
410
+}
411
+
412
// newBaseAutomationPolicy returns a new TLS automation policy that gets
413
// its values from the global options map. It should be used as the base
414
// for any other automation policies. A nil policy (and no error) will be
@@ -341,17 +416,10 @@ func (st ServerType) buildTLSApp(
416
// true, a non-nil value will always be returned (unless there is an error).
417
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
418
issuer, hasIssuer := options["cert_issuer"]
344
-
345
- acmeCA, hasACMECA := options["acme_ca"]
346
- acmeCARoot, hasACMECARoot := options["acme_ca_root"]
347
- acmeDNS, hasACMEDNS := options["acme_dns"]
348
- acmeEAB, hasACMEEAB := options["acme_eab"]
349
-
350
- email, hasEmail := options["email"]
351
- localCerts, hasLocalCerts := options["local_certs"]
419
+ _, hasLocalCerts := options["local_certs"]
420
keyType, hasKeyType := options["key_type"]
421
354
- hasGlobalAutomationOpts := hasIssuer || hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType
422
+ hasGlobalAutomationOpts := hasIssuer || hasLocalCerts || hasKeyType
423
424
// if there are no global options related to automation policies
425
// set, then we can just return right away
@@ -363,48 +431,18 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
431
}
432
433
ap := new(caddytls.AutomationPolicy)
366
- if keyType != nil {
434
+ if hasKeyType {
435
ap.KeyType = keyType.(string)
436
}
437
438
+ if hasIssuer && hasLocalCerts {
439
+ 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")
440
+ }
441
+
442
if hasIssuer {
371
- if hasACMECA || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts {
372
- return nil, fmt.Errorf("global options are ambiguous: cert_issuer is confusing when combined with acme_*, email, or local_certs options")
373
- }
374
- ap.Issuer = issuer.(certmagic.Issuer)
375
- } else if localCerts != nil {
376
- // internal issuer enabled trumps any ACME configurations; useful in testing
377
- ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later
378
- } else {
379
- if acmeCA == nil {
380
- acmeCA = ""
381
- }
382
- if email == nil {
383
- email = ""
384
- }
385
- mgr := &caddytls.ACMEIssuer{
386
- CA: acmeCA.(string),
387
- Email: email.(string),
388
- }
389
- if acmeDNS != nil {
390
- provName := acmeDNS.(string)
391
- dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
392
- if err != nil {
393
- return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
394
- }
395
- mgr.Challenges = &caddytls.ChallengesConfig{
396
- DNS: &caddytls.DNSChallengeConfig{
397
- ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings),
398
- },
399
- }
400
- }
401
- if acmeCARoot != nil {
402
- mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
403
- }
404
- if acmeEAB != nil {
405
- mgr.ExternalAccount = acmeEAB.(*acme.EAB)
406
- }
407
- ap.Issuer = disambiguateACMEIssuer(mgr) // we'll encode it later
443
+ ap.Issuers = []certmagic.Issuer{issuer.(certmagic.Issuer)}
444
+ } else if hasLocalCerts {
445
+ ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
446
}
447
448
return ap, nil
@@ -463,7 +501,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
501
// otherwise the one without any subjects (a catch-all) would be
502
// eaten up by the one with subjects; and if both have subjects, we
503
// need to combine their lists
466
- if bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) &&
504
+ if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
505
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
506
aps[i].MustStaple == aps[j].MustStaple &&
507
aps[i].KeyType == aps[j].KeyType &&
caddytest/integration/caddyfile_adapt/global_options.txt
+5
-3
@@ -54,9 +54,11 @@
54
"automation": {
55
"policies": [
56
{
57
- "issuer": {
58
- "module": "internal"
59
- },
57
+ "issuers": [
58
+ {
59
+ "module": "internal"
60
+ }
61
+ ],
62
"key_type": "ed25519"
63
}
64
],
caddytest/integration/caddyfile_adapt/global_options_acme.txt
+14
-12
@@ -57,18 +57,20 @@
57
"automation": {
58
"policies": [
59
{
60
- "issuer": {
61
- "ca": "https://example.com",
62
- "email": "test@example.com",
63
- "external_account": {
64
- "key_id": "4K2scIVbBpNd-78scadB2g",
65
- "mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
66
- },
67
- "module": "acme",
68
- "trusted_roots_pem_files": [
69
- "/path/to/ca.crt"
70
- ]
71
- },
60
+ "issuers": [
61
+ {
62
+ "ca": "https://example.com",
63
+ "email": "test@example.com",
64
+ "external_account": {
65
+ "key_id": "4K2scIVbBpNd-78scadB2g",
66
+ "mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
67
+ },
68
+ "module": "acme",
69
+ "trusted_roots_pem_files": [
70
+ "/path/to/ca.crt"
71
+ ]
72
+ }
73
+ ],
74
"key_type": "ed25519"
75
}
76
],
caddytest/integration/caddyfile_adapt/global_options_admin.txt
+5
-3
@@ -62,9 +62,11 @@
62
"automation": {
63
"policies": [
64
{
65
- "issuer": {
66
- "module": "internal"
67
- },
65
+ "issuers": [
66
+ {
67
+ "module": "internal"
68
+ }
69
+ ],
70
"key_type": "ed25519"
71
}
72
],
caddytest/integration/caddyfile_adapt/tls_automation_policies.txt
+15
-9
@@ -54,24 +54,30 @@ foo.tld, www.foo.tld {
54
"foo.tld",
55
"www.foo.tld"
56
],
57
- "issuer": {
58
- "module": "internal"
59
- }
57
+ "issuers": [
58
+ {
59
+ "module": "internal"
60
+ }
61
+ ]
62
},
63
{
64
"subjects": [
65
"*.*.tld",
66
"*.tld"
67
],
66
- "issuer": {
67
- "module": "internal"
68
- },
68
+ "issuers": [
69
+ {
70
+ "module": "internal"
71
+ }
72
+ ],
73
"on_demand": true
74
},
75
{
72
- "issuer": {
73
- "module": "internal"
74
- }
76
+ "issuers": [
77
+ {
78
+ "module": "internal"
79
+ }
80
+ ]
81
}
82
]
83
}
go.mod
+1
-1
@@ -6,7 +6,7 @@ require (
6
github.com/Masterminds/sprig/v3 v3.1.0
7
github.com/alecthomas/chroma v0.8.0
8
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
9
- github.com/caddyserver/certmagic v0.12.1-0.20201112220015-12df2be5a021
9
+ github.com/caddyserver/certmagic v0.12.1-0.20201116175341-0f8a9f688760
10
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
11
github.com/go-chi/chi v4.1.2+incompatible
12
github.com/google/cel-go v0.5.1
go.sum
+2
-2
@@ -85,8 +85,8 @@ github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTK
85
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
86
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
87
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
88
-github.com/caddyserver/certmagic v0.12.1-0.20201112220015-12df2be5a021 h1:VY4JahIicW/GT84KoIm+TSKwMHj08NyPJhsGypTWJn4=
89
-github.com/caddyserver/certmagic v0.12.1-0.20201112220015-12df2be5a021/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U=
88
+github.com/caddyserver/certmagic v0.12.1-0.20201116175341-0f8a9f688760 h1:h7KGtOb9TAfZp2/KwPd9iyqiLVZMWbpx5Mu0Her2iRw=
89
+github.com/caddyserver/certmagic v0.12.1-0.20201116175341-0f8a9f688760/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U=
90
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
91
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
92
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
modules/caddyhttp/autohttps.go
+40
-21
@@ -241,7 +241,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
241
// we now have a list of all the unique names for which we need certs;
242
// turn the set into a slice so that phase 2 can use it
243
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
244
- var internal, external []string
244
+ var internal []string
245
uniqueDomainsLoop:
246
for d := range uniqueDomainsForCerts {
247
// whether or not there is already an automation policy for this
@@ -264,15 +264,13 @@ uniqueDomainsLoop:
264
265
// if no automation policy exists for the name yet, we
266
// will associate it with an implicit one
267
- if certmagic.SubjectQualifiesForPublicCert(d) {
268
- external = append(external, d)
269
- } else {
267
+ if !certmagic.SubjectQualifiesForPublicCert(d) {
268
internal = append(internal, d)
269
}
270
}
271
272
// ensure there is an automation policy to handle these certs
275
- err := app.createAutomationPolicies(ctx, external, internal)
273
+ err := app.createAutomationPolicies(ctx, internal)
274
if err != nil {
275
return err
276
}
@@ -430,7 +428,7 @@ redirServersLoop:
428
// automation policy exists, it will be shallow-copied and used as the
429
// base for the new ones (this is important for preserving behavior the
430
// user intends to be "defaults").
433
-func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, internalNames []string) error {
431
+func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []string) error {
432
// before we begin, loop through the existing automation policies
433
// and, for any ACMEIssuers we find, make sure they're filled in
434
// with default values that might be specified in our HTTP app; also
@@ -447,16 +445,23 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
445
// set up default issuer -- honestly, this is only
446
// really necessary because the HTTP app is opinionated
447
// and has settings which could be inferred as new
450
- // defaults for the ACMEIssuer in the TLS app
451
- if ap.Issuer == nil {
452
- ap.Issuer = new(caddytls.ACMEIssuer)
453
- }
454
- if acmeIssuer, ok := ap.Issuer.(acmeCapable); ok {
455
- err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer())
448
+ // defaults for the ACMEIssuer in the TLS app (such as
449
+ // what the HTTP and HTTPS ports are)
450
+ if ap.Issuers == nil {
451
+ var err error
452
+ ap.Issuers, err = caddytls.DefaultIssuers(ctx)
453
if err != nil {
454
return err
455
}
456
}
457
+ for _, iss := range ap.Issuers {
458
+ if acmeIssuer, ok := iss.(acmeCapable); ok {
459
+ err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer())
460
+ if err != nil {
461
+ return err
462
+ }
463
+ }
464
+ }
465
466
// while we're here, is this the catch-all/base policy?
467
if !foundBasePolicy && len(ap.Subjects) == 0 {
@@ -471,11 +476,14 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
476
}
477
478
// if the basePolicy has an existing ACMEIssuer (particularly to
474
- // include any type that embeds/wraps an ACMEIssuer), let's use it,
475
- // otherwise we'll make one
479
+ // include any type that embeds/wraps an ACMEIssuer), let's use it
480
+ // (I guess we just use the first one?), otherwise we'll make one
481
var baseACMEIssuer *caddytls.ACMEIssuer
477
- if acmeWrapper, ok := basePolicy.Issuer.(acmeCapable); ok {
478
- baseACMEIssuer = acmeWrapper.GetACMEIssuer()
482
+ for _, iss := range basePolicy.Issuers {
483
+ if acmeWrapper, ok := iss.(acmeCapable); ok {
484
+ baseACMEIssuer = acmeWrapper.GetACMEIssuer()
485
+ break
486
+ }
487
}
488
if baseACMEIssuer == nil {
489
// note that this happens if basePolicy.Issuer is nil
@@ -485,7 +493,7 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
493
494
// if there was a base policy to begin with, we already
495
// filled in its issuer's defaults; if there wasn't, we
488
- // stil need to do that
496
+ // still need to do that
497
if !foundBasePolicy {
498
err := app.fillInACMEIssuer(baseACMEIssuer)
499
if err != nil {
@@ -494,8 +502,20 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
502
}
503
504
// never overwrite any other issuer that might already be configured
497
- if basePolicy.Issuer == nil {
498
- basePolicy.Issuer = baseACMEIssuer
505
+ if basePolicy.Issuers == nil {
506
+ var err error
507
+ basePolicy.Issuers, err = caddytls.DefaultIssuers(ctx)
508
+ if err != nil {
509
+ return err
510
+ }
511
+ for _, iss := range basePolicy.Issuers {
512
+ if acmeIssuer, ok := iss.(acmeCapable); ok {
513
+ err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer())
514
+ if err != nil {
515
+ return err
516
+ }
517
+ }
518
+ }
519
}
520
521
if !foundBasePolicy {
@@ -549,8 +569,7 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna
569
// of names that would normally use the production API;
570
// anyway, that gets into the weeds a bit...
571
newPolicy.Subjects = internalNames
552
- newPolicy.Issuer = internalIssuer
553
-
572
+ newPolicy.Issuers = []certmagic.Issuer{internalIssuer}
573
err := app.tlsApp.AddAutomationPolicy(newPolicy)
574
if err != nil {
575
return err
modules/caddytls/acmeissuer.go
+9
@@ -97,6 +97,15 @@ func (ACMEIssuer) CaddyModule() caddy.ModuleInfo {
97
func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
98
iss.logger = ctx.Logger(iss)
99
100
+ // expand email address, if non-empty
101
+ if iss.Email != "" {
102
+ email, err := caddy.NewReplacer().ReplaceOrErr(iss.Email, true, true)
103
+ if err != nil {
104
+ return fmt.Errorf("expanding email address '%s': %v", iss.Email, err)
105
+ }
106
+ iss.Email = email
107
+ }
108
+
109
// DNS providers
110
if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.ProviderRaw != nil {
111
val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw")
modules/caddytls/automation.go
+50
-34
@@ -23,7 +23,6 @@ import (
23
"github.com/caddyserver/caddy/v2"
24
"github.com/caddyserver/certmagic"
25
"github.com/mholt/acmez"
26
- "go.uber.org/zap"
26
)
27
28
// AutomationConfig governs the automated management of TLS certificates.
@@ -72,8 +71,13 @@ type AutomationPolicy struct {
71
// Which subjects (hostnames or IP addresses) this policy applies to.
72
Subjects []string `json:"subjects,omitempty"`
73
75
- // The module that will issue certificates. Default: internal if all
76
- // subjects do not qualify for public certificates; othewise acme.
74
+ // The modules that may issue certificates. Default: internal if all
75
+ // subjects do not qualify for public certificates; othewise acme and
76
+ // zerossl.
77
+ IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
78
+
79
+ // DEPRECATED: Use `issuers` instead (November 2020). This field will
80
+ // be removed in the future.
81
IssuerRaw json.RawMessage `json:"issuer,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
82
83
// If true, certificates will be requested with MustStaple. Not all
@@ -103,10 +107,10 @@ type AutomationPolicy struct {
107
// load.
108
OnDemand bool `json:"on_demand,omitempty"`
109
106
- // Issuer stores the decoded issuer parameters. This is only
107
- // used to populate an underlying certmagic.Config's Issuer
110
+ // Issuers stores the decoded issuer parameters. This is only
111
+ // used to populate an underlying certmagic.Config's Issuers
112
// field; it is not referenced thereafter.
109
- Issuer certmagic.Issuer `json:"-"`
113
+ Issuers []certmagic.Issuer `json:"-"`
114
115
magic *certmagic.Config
116
storage certmagic.Storage
@@ -150,34 +154,30 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
154
}
155
}
156
153
- // if this automation policy has no Issuer defined, and
154
- // none of the subjects qualify for a public certificate,
155
- // set the issuer to internal so that these names can all
156
- // get certificates; critically, we can only do this if an
157
- // issuer is not explicitly configured (IssuerRaw, vs. just
158
- // Issuer) AND if the list of subjects is non-empty
159
- if ap.IssuerRaw == nil && len(ap.Subjects) > 0 {
160
- var anyPublic bool
161
- for _, s := range ap.Subjects {
162
- if certmagic.SubjectQualifiesForPublicCert(s) {
163
- anyPublic = true
164
- break
165
- }
157
+ // TODO: IssuerRaw field deprecated as of November 2020 - remove this shim after deprecation is complete
158
+ if ap.IssuerRaw != nil {
159
+ tlsApp.logger.Warn("the 'issuer' field is deprecated and will be removed in the future; use 'issuers' instead; your issuer has been appended automatically for now")
160
+ ap.IssuersRaw = append(ap.IssuersRaw, ap.IssuerRaw)
161
+ }
162
+
163
+ // load and provision any explicitly-configured issuer modules
164
+ if ap.IssuersRaw != nil {
165
+ val, err := tlsApp.ctx.LoadModule(ap, "IssuersRaw")
166
+ if err != nil {
167
+ return fmt.Errorf("loading TLS automation management module: %s", err)
168
}
167
- if !anyPublic {
168
- tlsApp.logger.Info("setting internal issuer for automation policy that has only internal subjects but no issuer configured",
169
- zap.Strings("subjects", ap.Subjects))
170
- ap.IssuerRaw = json.RawMessage(`{"module":"internal"}`)
169
+ for _, issVal := range val.([]interface{}) {
170
+ ap.Issuers = append(ap.Issuers, issVal.(certmagic.Issuer))
171
}
172
}
173
174
- // load and provision any explicitly-configured issuer module
175
- if ap.IssuerRaw != nil {
176
- val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw")
174
+ issuers := ap.Issuers
175
+ if len(issuers) == 0 {
176
+ var err error
177
+ issuers, err = DefaultIssuers(tlsApp.ctx)
178
if err != nil {
178
- return fmt.Errorf("loading TLS automation management module: %s", err)
179
+ return err
180
}
180
- ap.Issuer = val.(certmagic.Issuer)
181
}
182
183
keyType := ap.KeyType
@@ -206,12 +206,9 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
206
KeySource: keySource,
207
OnDemand: ond,
208
Storage: storage,
209
- Issuer: ap.Issuer, // if nil, certmagic.New() will create one
209
+ Issuers: issuers,
210
Logger: tlsApp.logger,
211
}
212
- if rev, ok := ap.Issuer.(certmagic.Revoker); ok {
213
- template.Revoker = rev
214
- }
212
ap.magic = certmagic.New(tlsApp.certCache, template)
213
214
// sometimes issuers may need the parent certmagic.Config in
@@ -219,13 +216,32 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
216
// access to the correct storage and cache so it can solve
217
// ACME challenges -- it's an annoying, inelegant circular
218
// dependency that I don't know how to resolve nicely!)
222
- if annoying, ok := ap.Issuer.(ConfigSetter); ok {
223
- annoying.SetConfig(ap.magic)
219
+ for _, issuer := range ap.magic.Issuers {
220
+ if annoying, ok := issuer.(ConfigSetter); ok {
221
+ annoying.SetConfig(ap.magic)
222
+ }
223
}
224
225
return nil
226
}
227
228
+// DefaultIssuers returns empty but provisioned default Issuers.
229
+// This function is experimental and has no compatibility promises.
230
+func DefaultIssuers(ctx caddy.Context) ([]certmagic.Issuer, error) {
231
+ acme := new(ACMEIssuer)
232
+ err := acme.Provision(ctx)
233
+ if err != nil {
234
+ return nil, err
235
+ }
236
+ zerossl := new(ZeroSSLIssuer)
237
+ err = zerossl.Provision(ctx)
238
+ if err != nil {
239
+ return nil, err
240
+ }
241
+ // TODO: eventually, insert ZeroSSL into first position in the slice -- see also httpcaddyfile/tlsapp.go for where similar defaults are configured
242
+ return []certmagic.Issuer{acme, zerossl}, nil
243
+}
244
+
245
// ChallengesConfig configures the ACME challenges.
246
type ChallengesConfig struct {
247
// HTTP configures the ACME HTTP challenge. This
modules/caddytls/tls.go
+11
-9
@@ -137,7 +137,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
137
continue
138
}
139
t.Automation.defaultInternalAutomationPolicy = &AutomationPolicy{
140
- IssuerRaw: json.RawMessage(`{"module":"internal"}`),
140
+ IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
141
}
142
err = t.Automation.defaultInternalAutomationPolicy.Provision(t)
143
if err != nil {
@@ -303,20 +303,22 @@ func (t *TLS) Manage(names []string) error {
303
304
// HandleHTTPChallenge ensures that the HTTP challenge is handled for the
305
// certificate named by r.Host, if it is an HTTP challenge request. It
306
-// requires that the automation policy for r.Host has an issue of type
307
-// *certmagic.ACMEManager.
306
+// requires that the automation policy for r.Host has an issuer of type
307
+// *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()).
308
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
309
if !certmagic.LooksLikeHTTPChallenge(r) {
310
return false
311
}
312
+ // try all the issuers until we find the one that initiated the challenge
313
ap := t.getAutomationPolicyForName(r.Host)
313
- if ap.magic.Issuer == nil {
314
- return false
315
- }
314
type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
317
- if am, ok := ap.magic.Issuer.(acmeCapable); ok {
318
- iss := am.GetACMEIssuer()
319
- return certmagic.NewACMEManager(iss.magic, iss.template).HandleHTTPChallenge(w, r)
315
+ for _, iss := range ap.magic.Issuers {
316
+ if am, ok := iss.(acmeCapable); ok {
317
+ iss := am.GetACMEIssuer()
318
+ if certmagic.NewACMEManager(iss.magic, iss.template).HandleHTTPChallenge(w, r) {
319
+ return true
320
+ }
321
+ }
322
}
323
return false
324
}
modules/caddytls/zerosslissuer.go
+15
-25
@@ -59,16 +59,13 @@ func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo {
59
// Provision sets up iss.
60
func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error {
61
iss.logger = ctx.Logger(iss)
62
-
62
if iss.ACMEIssuer == nil {
63
iss.ACMEIssuer = new(ACMEIssuer)
64
}
66
- err := iss.ACMEIssuer.Provision(ctx)
67
- if err != nil {
68
- return err
65
+ if iss.ACMEIssuer.CA == "" {
66
+ iss.ACMEIssuer.CA = certmagic.ZeroSSLProductionCA
67
}
70
-
71
- return nil
68
+ return iss.ACMEIssuer.Provision(ctx)
69
}
70
71
func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, am *certmagic.ACMEManager, _ acme.Account) error {
@@ -86,26 +83,22 @@ func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context) (*acme.EAB
83
84
// there are two ways to generate EAB credentials: authenticated with
85
// their API key, or unauthenticated with their email address
89
- switch {
90
- case iss.APIKey != "":
86
+ if iss.APIKey != "" {
87
apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "")
88
if apiKey == "" {
89
return nil, fmt.Errorf("missing API key: '%v'", iss.APIKey)
90
}
91
qs := url.Values{"access_key": []string{apiKey}}
92
endpoint = fmt.Sprintf("%s/eab-credentials?%s", zerosslAPIBase, qs.Encode())
97
-
98
- case iss.Email != "":
99
- email := caddy.NewReplacer().ReplaceAll(iss.Email, "")
93
+ } else {
94
+ email := iss.Email
95
if email == "" {
101
- return nil, fmt.Errorf("missing email: '%v'", iss.Email)
96
+ iss.logger.Warn("missing email address for ZeroSSL; it is strongly recommended to set one for next time")
97
+ email = "caddy@zerossl.com" // special email address that preserves backwards-compat, but which black-holes dashboard features, oh well
98
}
99
endpoint = zerosslAPIBase + "/eab-credentials-email"
100
form := url.Values{"email": []string{email}}
101
body = strings.NewReader(form.Encode())
106
-
107
- default:
108
- return nil, fmt.Errorf("must configure either an API key or email address to use ZeroSSL without explicit EAB")
102
}
103
104
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
@@ -161,9 +154,6 @@ func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context) (*acme.EAB
154
func (iss *ZeroSSLIssuer) initialize() {
155
iss.mu.Lock()
156
defer iss.mu.Unlock()
164
- if iss.template.CA == "" {
165
- iss.template.CA = zerosslACMEDirectory
166
- }
157
if iss.template.NewAccountFunc == nil {
158
iss.template.NewAccountFunc = iss.newAccountCallback
159
}
@@ -195,15 +185,18 @@ func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.Certificate
185
186
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
187
//
198
-// ... zerossl <api_key> {
188
+// ... zerossl [<api_key>] {
189
// ...
190
// }
191
//
192
// Any of the subdirectives for the ACME issuer can be used in the block.
193
func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
194
for d.Next() {
205
- if !d.AllArgs(&iss.APIKey) {
206
- return d.ArgErr()
195
+ if d.NextArg() {
196
+ iss.APIKey = d.Val()
197
+ if d.NextArg() {
198
+ return d.ArgErr()
199
+ }
200
}
201
202
if iss.ACMEIssuer == nil {
@@ -217,10 +210,7 @@ func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
210
return nil
211
}
212
220
-const (
221
- zerosslACMEDirectory = "https://acme.zerossl.com/v2/DV90"
222
- zerosslAPIBase = "https://api.zerossl.com/acme"
223
-)
213
+const zerosslAPIBase = "https://api.zerossl.com/acme"
214
215
// Interface guards
216
var (