@marcel91200 / caddy Mirror · read-only / commits / 81413cae
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 14:30 UTC

caddytls: Upgrade ACMEz to v2; support ZeroSSL API; various fixes (#6229)

* WIP: acmez v2, CertMagic, and ZeroSSL issuer upgrades * caddytls: ZeroSSLIssuer now uses ZeroSSL API instead of ACME * Fix go.mod * caddytls: Fix automation related to managers (fix #6060) * Fix typo (appease linter) * Fix HTTP validation with ZeroSSL API

Matt Holt committed Apr 13, 2024 at 21:31 UTC 81413caea251a3ef9e3641d7b1b6e867572a2b1b
27 files changed +444 -298
admin.go
-1
@@ -474,7 +474,6 @@ func manageIdentity(ctx Context, cfg *Config) error {
474 // import the caddytls package -- but it works
475 if cfg.Admin.Identity.IssuersRaw == nil {
476 cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
477 - json.RawMessage(`{"module": "zerossl"}`),
477 json.RawMessage(`{"module": "acme"}`),
478 }
479 }
caddyconfig/httpcaddyfile/builtins.go
+16 -12
@@ -24,7 +24,7 @@ import (
24 "time"
25
26 "github.com/caddyserver/certmagic"
27 - "github.com/mholt/acmez/acme"
27 + "github.com/mholt/acmez/v2/acme"
28 "go.uber.org/zap/zapcore"
29
30 "github.com/caddyserver/caddy/v2"
@@ -107,7 +107,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
107 var onDemand bool
108 var reusePrivateKeys bool
109
110 - // file certificate loader
110 firstLine := h.RemainingArgs()
111 switch len(firstLine) {
112 case 0:
@@ -117,13 +116,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
116 } else if !strings.Contains(firstLine[0], "@") {
117 return nil, h.Err("single argument must either be 'internal' or an email address")
118 } else {
120 - if acmeIssuer == nil {
121 - acmeIssuer = new(caddytls.ACMEIssuer)
119 + acmeIssuer = &caddytls.ACMEIssuer{
120 + Email: firstLine[0],
121 }
123 - acmeIssuer.Email = firstLine[0]
122 }
123
124 case 2:
125 + // file certificate loader
126 certFilename := firstLine[0]
127 keyFilename := firstLine[1]
128
@@ -488,19 +487,24 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
487
488 case acmeIssuer != nil:
489 // implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
491 - defaultIssuers := caddytls.DefaultIssuers()
490 + defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
491
493 - // if a CA endpoint was set, override multiple implicit issuers since it's a specific one
492 + // if an ACME CA endpoint was set, the user expects to use that specific one,
493 + // not any others that may be defaults, so replace all defaults with that ACME CA
494 if acmeIssuer.CA != "" {
495 defaultIssuers = []certmagic.Issuer{acmeIssuer}
496 }
497
498 for _, issuer := range defaultIssuers {
499 - switch iss := issuer.(type) {
500 - case *caddytls.ACMEIssuer:
501 - issuer = acmeIssuer
502 - case *caddytls.ZeroSSLIssuer:
503 - iss.ACMEIssuer = acmeIssuer
499 + // apply settings from the implicitly-configured ACMEIssuer to any
500 + // default ACMEIssuers, but preserve each default issuer's CA endpoint,
501 + // because, for example, if you configure the DNS challenge, it should
502 + // apply to any of the default ACMEIssuers, but you don't want to trample
503 + // out their unique CA endpoints
504 + if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
505 + acmeCopy := *acmeIssuer
506 + acmeCopy.CA = iss.CA
507 + issuer = &acmeCopy
508 }
509 configVals = append(configVals, ConfigValue{
510 Class: "tls.cert_issuer",
caddyconfig/httpcaddyfile/options.go
+3 -3
@@ -18,7 +18,7 @@ import (
18 "strconv"
19
20 "github.com/caddyserver/certmagic"
21 - "github.com/mholt/acmez/acme"
21 + "github.com/mholt/acmez/v2/acme"
22
23 "github.com/caddyserver/caddy/v2"
24 "github.com/caddyserver/caddy/v2/caddyconfig"
@@ -212,9 +212,9 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
212 if err != nil {
213 return nil, err
214 }
215 - prov, ok := unm.(certmagic.ACMEDNSProvider)
215 + prov, ok := unm.(certmagic.DNSProvider)
216 if !ok {
217 - return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
217 + return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm)
218 }
219 return prov, nil
220 }
caddyconfig/httpcaddyfile/tlsapp.go
+22 -9
@@ -24,7 +24,7 @@ import (
24 "strings"
25
26 "github.com/caddyserver/certmagic"
27 - "github.com/mholt/acmez/acme"
27 + "github.com/mholt/acmez/v2/acme"
28
29 "github.com/caddyserver/caddy/v2"
30 "github.com/caddyserver/caddy/v2/caddyconfig"
@@ -224,7 +224,7 @@ func (st ServerType) buildTLSApp(
224 var internal, external []string
225 for _, s := range ap.SubjectsRaw {
226 // do not create Issuers for Tailscale domains; they will be given a Manager instead
227 - if strings.HasSuffix(strings.ToLower(s), ".ts.net") {
227 + if isTailscaleDomain(s) {
228 continue
229 }
230 if !certmagic.SubjectQualifiesForCert(s) {
@@ -378,15 +378,12 @@ func (st ServerType) buildTLSApp(
378 if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
379 // for public names, create default issuers which will later be filled in with configured global defaults
380 // (internal names will implicitly use the internal issuer at auto-https time)
381 - ap.Issuers = caddytls.DefaultIssuers()
381 + emailStr, _ := globalEmail.(string)
382 + ap.Issuers = caddytls.DefaultIssuers(emailStr)
383
384 // if a specific endpoint is configured, can't use multiple default issuers
385 if globalACMECA != nil {
385 - if strings.Contains(globalACMECA.(string), "zerossl") {
386 - ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
387 - } else {
388 - ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
389 - }
386 + ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
387 }
388 }
389 }
@@ -666,17 +663,33 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
663 // subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
664 // that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
665 // if the automation policy has OnDemand enabled (i.e. this function is more lenient).
666 +//
667 +// IP subjects are considered as non-qualifying for public certs. Technically, there are
668 +// now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function
669 +// is used solely for implicit automation (defaults), where it gets really complicated to
670 +// keep track of which issuers support IP certificates in which circumstances. Currently,
671 +// issuers that support IP certificates are very few, and all require some sort of config
672 +// from the user anyway (such as an account credential). Since we cannot implicitly and
673 +// automatically get public IP certs without configuration from the user, we treat IPs as
674 +// not qualifying for public certificates. Users should expressly configure an issuer
675 +// that supports IP certs for that purpose.
676 func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
677 return !certmagic.SubjectIsIP(subj) &&
678 !certmagic.SubjectIsInternal(subj) &&
679 (strings.Count(subj, "*.") < 2 || ap.OnDemand)
680 }
681
682 +// automationPolicyHasAllPublicNames returns true if all the names on the policy
683 +// do NOT qualify for public certs OR are tailscale domains.
684 func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
685 for _, subj := range ap.SubjectsRaw {
677 - if !subjectQualifiesForPublicCert(ap, subj) {
686 + if !subjectQualifiesForPublicCert(ap, subj) || isTailscaleDomain(subj) {
687 return false
688 }
689 }
690 return true
691 }
692 +
693 +func isTailscaleDomain(name string) bool {
694 + return strings.HasSuffix(strings.ToLower(name), ".ts.net")
695 +}
caddytest/integration/acme_test.go
+4 -4
@@ -13,8 +13,8 @@ import (
13
14 "github.com/caddyserver/caddy/v2"
15 "github.com/caddyserver/caddy/v2/caddytest"
16 - "github.com/mholt/acmez"
17 - "github.com/mholt/acmez/acme"
16 + "github.com/mholt/acmez/v2"
17 + "github.com/mholt/acmez/v2/acme"
18 smallstepacme "github.com/smallstep/certificates/acme"
19 "go.uber.org/zap"
20 )
@@ -77,7 +77,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
77 return
78 }
79
80 - certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
80 + certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
81 if err != nil {
82 t.Errorf("obtaining certificate: %v", err)
83 return
@@ -146,7 +146,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
146 return
147 }
148
149 - certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
149 + certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
150 if len(certs) > 0 {
151 t.Errorf("expected '0' certificates, but received '%d'", len(certs))
152 }
caddytest/integration/acmeserver_test.go
+5 -10
@@ -9,8 +9,8 @@ import (
9 "testing"
10
11 "github.com/caddyserver/caddy/v2/caddytest"
12 - "github.com/mholt/acmez"
13 - "github.com/mholt/acmez/acme"
12 + "github.com/mholt/acmez/v2"
13 + "github.com/mholt/acmez/v2/acme"
14 "go.uber.org/zap"
15 )
16
@@ -105,12 +105,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
105 return
106 }
107 {
108 - certs, err := client.ObtainCertificate(
109 - ctx,
110 - account,
111 - certPrivateKey,
112 - []string{"localhost"},
113 - )
108 + certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
109 if err != nil {
110 t.Errorf("obtaining certificate for allowed domain: %v", err)
111 return
@@ -126,7 +121,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
121 }
122 }
123 {
129 - _, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
124 + _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
125 if err == nil {
126 t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
127 } else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
@@ -199,7 +194,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
194 return
195 }
196 {
202 - _, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"deny.localhost"})
197 + _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
198 if err == nil {
199 t.Errorf("obtaining certificate for 'deny.localhost' domain")
200 } else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest
-6
@@ -40,12 +40,6 @@ example.com
40 "preferred_chains": {
41 "smallest": true
42 }
43 - },
44 - {
45 - "module": "zerossl",
46 - "preferred_chains": {
47 - "smallest": true
48 - }
43 }
44 ]
45 }
caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest
+2 -1
@@ -70,8 +70,9 @@ c.example.com {
70 "module": "acme"
71 },
72 {
73 + "ca": "https://acme.zerossl.com/v2/DV90",
74 "email": "abc@example.com",
74 - "module": "zerossl"
75 + "module": "acme"
76 }
77 ]
78 },
caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest
+2 -1
@@ -131,8 +131,9 @@ abc.de {
131 "module": "acme"
132 },
133 {
134 + "ca": "https://acme.zerossl.com/v2/DV90",
135 "email": "my.email@example.com",
135 - "module": "zerossl"
136 + "module": "acme"
137 }
138 ]
139 }
caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest
+2 -1
@@ -86,8 +86,9 @@ http://localhost:8081 {
86 "module": "acme"
87 },
88 {
89 + "ca": "https://acme.zerossl.com/v2/DV90",
90 "email": "abc@example.com",
90 - "module": "zerossl"
91 + "module": "acme"
92 }
93 ]
94 }
caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest
+2 -1
@@ -54,8 +54,9 @@ example.com {
54 "module": "acme"
55 },
56 {
57 + "ca": "https://acme.zerossl.com/v2/DV90",
58 "email": "foo@bar",
58 - "module": "zerossl"
59 + "module": "acme"
60 }
61 ]
62 }
caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest
-8
@@ -58,14 +58,6 @@ tls {
58 }
59 },
60 "module": "acme"
61 - },
62 - {
63 - "challenges": {
64 - "dns": {
65 - "ttl": 310000000000
66 - }
67 - },
68 - "module": "zerossl"
61 }
62 ]
63 }
caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest
+4 -5
@@ -5,7 +5,7 @@ tls {
5 issuer acme {
6 dns_ttl 5m10s
7 }
8 - issuer zerossl {
8 + issuer zerossl api_key {
9 dns_ttl 10m20s
10 }
11 }
@@ -65,10 +65,9 @@ tls {
65 "module": "acme"
66 },
67 {
68 - "challenges": {
69 - "dns": {
70 - "ttl": 620000000000
71 - }
68 + "api_key": "api_key",
69 + "cname_validation": {
70 + "ttl": 620000000000
71 },
72 "module": "zerossl"
73 }
caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest
+5 -6
@@ -6,7 +6,7 @@ tls {
6 propagation_delay 5m10s
7 propagation_timeout 10m20s
8 }
9 - issuer zerossl {
9 + issuer zerossl api_key {
10 propagation_delay 5m30s
11 propagation_timeout -1
12 }
@@ -68,11 +68,10 @@ tls {
68 "module": "acme"
69 },
70 {
71 - "challenges": {
72 - "dns": {
73 - "propagation_delay": 330000000000,
74 - "propagation_timeout": -1
75 - }
71 + "api_key": "api_key",
72 + "cname_validation": {
73 + "propagation_delay": 330000000000,
74 + "propagation_timeout": -1
75 },
76 "module": "zerossl"
77 }
caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest
-9
@@ -60,15 +60,6 @@ tls {
60 }
61 },
62 "module": "acme"
63 - },
64 - {
65 - "challenges": {
66 - "dns": {
67 - "propagation_delay": 310000000000,
68 - "propagation_timeout": 620000000000
69 - }
70 - },
71 - "module": "zerossl"
63 }
64 ]
65 }
cmd/caddy/setcap.sh
+6 -3
@@ -1,13 +1,16 @@
1 #!/bin/sh
2
3 -# USAGE: go run -exec ./setcap.sh main.go <args...>
3 +# USAGE:
4 +# go run -exec ./setcap.sh main.go <args...>
5 #
6 # (Example: `go run -exec ./setcap.sh main.go run --config caddy.json`)
7 #
8 # For some reason this does not work on my Arch system, so if you find that's
8 -# the case, you can instead do: go build && ./setcap.sh ./caddy <args...>
9 -# but this will leave the ./caddy binary laying around.
9 +# the case, you can instead do:
10 +#
11 +# go build && ./setcap.sh ./caddy <args...>
12 #
13 +# but this will leave the ./caddy binary laying around.
14 #
15
16 sudo setcap cap_net_bind_service=+ep "$1"
go.mod
+7 -8
@@ -1,22 +1,21 @@
1 module github.com/caddyserver/caddy/v2
2
3 -go 1.21
4 -
5 -toolchain go1.21.4
3 +go 1.22.0
4
5 require (
6 github.com/BurntSushi/toml v1.3.2
7 github.com/Masterminds/sprig/v3 v3.2.3
8 github.com/alecthomas/chroma/v2 v2.13.0
9 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
12 - github.com/caddyserver/certmagic v0.20.0
10 + github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570
11 + github.com/caddyserver/zerossl v0.1.2
12 github.com/dustin/go-humanize v1.0.1
13 github.com/go-chi/chi/v5 v5.0.12
14 github.com/google/cel-go v0.20.0
15 github.com/google/uuid v1.6.0
16 github.com/klauspost/compress v1.17.0
18 - github.com/klauspost/cpuid/v2 v2.2.5
19 - github.com/mholt/acmez v1.2.0
17 + github.com/klauspost/cpuid/v2 v2.2.7
18 + github.com/mholt/acmez/v2 v2.0.0-beta.2
19 github.com/prometheus/client_golang v1.19.0
20 github.com/quic-go/quic-go v0.42.0
21 github.com/smallstep/certificates v0.25.3-rc5
@@ -112,12 +111,12 @@ require (
111 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
112 github.com/jackc/pgtype v1.14.0 // indirect
113 github.com/jackc/pgx/v4 v4.18.3 // indirect
115 - github.com/libdns/libdns v0.2.1 // indirect
114 + github.com/libdns/libdns v0.2.2 // indirect
115 github.com/manifoldco/promptui v0.9.0 // indirect
116 github.com/mattn/go-colorable v0.1.13 // indirect
117 github.com/mattn/go-isatty v0.0.20 // indirect
118 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
120 - github.com/miekg/dns v1.1.55 // indirect
119 + github.com/miekg/dns v1.1.58 // indirect
120 github.com/mitchellh/copystructure v1.2.0 // indirect
121 github.com/mitchellh/go-ps v1.0.0 // indirect
122 github.com/mitchellh/reflectwalk v1.0.2 // indirect
go.sum
+12 -10
@@ -68,8 +68,10 @@ github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
68 github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
69 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
70 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
71 -github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
72 -github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
71 +github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570 h1:SsAXjoQx2wOmLl6mEwJEwh7wwys2hb/l/mhtmxA3wts=
72 +github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570/go.mod h1:e1NhB1rF5KZnAuAX6oSyhE7sg1Ru5bWgggw5RtauhEY=
73 +github.com/caddyserver/zerossl v0.1.2 h1:tlEu1VzWGoqcCpivs9liKAKhfpJWYJkHEMmlxRbVAxE=
74 +github.com/caddyserver/zerossl v0.1.2/go.mod h1:wtiJEHbdvunr40ZzhXlnIkOB8Xj4eKtBKizCcZitJiQ=
75 github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
76 github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
77 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -256,8 +258,8 @@ github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
258 github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
259 github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
260 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
259 -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
260 -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
261 +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
262 +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
263 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
264 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
265 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -275,8 +277,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
277 github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
278 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
279 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
278 -github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
279 -github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
280 +github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
281 +github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
282 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
283 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
284 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
@@ -292,10 +294,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
294 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
295 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
296 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
295 -github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
296 -github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
297 -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
298 -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
297 +github.com/mholt/acmez/v2 v2.0.0-beta.2 h1:GIgGILx8AWN0ePyTd+bjs2WDgNiIWm0nBwDLWp59aHc=
298 +github.com/mholt/acmez/v2 v2.0.0-beta.2/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
299 +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
300 +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
301 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
302 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
303 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
modules/caddyhttp/autohttps.go
+10
@@ -287,6 +287,16 @@ uniqueDomainsLoop:
287 for _, ap := range app.tlsApp.Automation.Policies {
288 for _, apHost := range ap.Subjects() {
289 if apHost == d {
290 + // if the automation policy has all internal subjects but no issuers,
291 + // it will default to CertMagic's issuers which are public CAs; use
292 + // our internal issuer instead
293 + if len(ap.Issuers) == 0 && ap.AllInternalSubjects() {
294 + iss := new(caddytls.InternalIssuer)
295 + if err := iss.Provision(ctx); err != nil {
296 + return err
297 + }
298 + ap.Issuers = append(ap.Issuers, iss)
299 + }
300 continue uniqueDomainsLoop
301 }
302 }
modules/caddyhttp/reverseproxy/fastcgi/client_test.go
-2
@@ -213,8 +213,6 @@ func DisabledTest(t *testing.T) {
213 // TODO: test chunked reader
214 globalt = t
215
216 - rand.Seed(time.Now().UTC().UnixNano())
217 -
216 // server
217 go func() {
218 listener, err := net.Listen("tcp", ipPort)
modules/caddytls/acmeissuer.go
+86 -8
@@ -17,14 +17,19 @@ package caddytls
17 import (
18 "context"
19 "crypto/x509"
20 + "encoding/json"
21 "fmt"
22 + "net/http"
23 + "net/url"
24 "os"
25 "strconv"
26 + "strings"
27 "time"
28
29 "github.com/caddyserver/certmagic"
26 - "github.com/mholt/acmez"
27 - "github.com/mholt/acmez/acme"
30 + "github.com/caddyserver/zerossl"
31 + "github.com/mholt/acmez/v2"
32 + "github.com/mholt/acmez/v2/acme"
33 "go.uber.org/zap"
34
35 "github.com/caddyserver/caddy/v2"
@@ -142,12 +147,14 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
147 iss.Challenges.DNS.solver = deprecatedProvider
148 } else {
149 iss.Challenges.DNS.solver = &certmagic.DNS01Solver{
145 - DNSProvider: val.(certmagic.ACMEDNSProvider),
146 - TTL: time.Duration(iss.Challenges.DNS.TTL),
147 - PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
148 - PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
149 - Resolvers: iss.Challenges.DNS.Resolvers,
150 - OverrideDomain: iss.Challenges.DNS.OverrideDomain,
150 + DNSManager: certmagic.DNSManager{
151 + DNSProvider: val.(certmagic.DNSProvider),
152 + TTL: time.Duration(iss.Challenges.DNS.TTL),
153 + PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
154 + PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
155 + Resolvers: iss.Challenges.DNS.Resolvers,
156 + OverrideDomain: iss.Challenges.DNS.OverrideDomain,
157 + },
158 }
159 }
160 }
@@ -210,6 +217,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) {
217 }
218 }
219
220 + // ZeroSSL requires EAB, but we can generate that automatically (requires an email address be configured)
221 + if strings.HasPrefix(iss.CA, "https://acme.zerossl.com/") {
222 + template.NewAccountFunc = func(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
223 + if acmeIss.ExternalAccount != nil {
224 + return acct, nil
225 + }
226 + var err error
227 + acmeIss.ExternalAccount, acct, err = iss.generateZeroSSLEABCredentials(ctx, acct)
228 + return acct, err
229 + }
230 + }
231 +
232 return template, nil
233 }
234
@@ -248,6 +267,65 @@ func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateRes
267 // to be accessed and manipulated.
268 func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss }
269
270 +// generateZeroSSLEABCredentials generates ZeroSSL EAB credentials for the primary contact email
271 +// on the issuer. It should only be usedif the CA endpoint is ZeroSSL. An email address is required.
272 +func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) {
273 + if strings.TrimSpace(iss.Email) == "" {
274 + return nil, acme.Account{}, fmt.Errorf("your email address is required to use ZeroSSL's ACME endpoint")
275 + }
276 +
277 + if len(acct.Contact) == 0 {
278 + // we borrow the email from config or the default email, so ensure it's saved with the account
279 + acct.Contact = []string{"mailto:" + iss.Email}
280 + }
281 +
282 + endpoint := zerossl.BaseURL + "/acme/eab-credentials-email"
283 + form := url.Values{"email": []string{iss.Email}}
284 + body := strings.NewReader(form.Encode())
285 +
286 + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
287 + if err != nil {
288 + return nil, acct, fmt.Errorf("forming request: %v", err)
289 + }
290 + req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
291 + req.Header.Set("User-Agent", certmagic.UserAgent)
292 +
293 + resp, err := http.DefaultClient.Do(req)
294 + if err != nil {
295 + return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err)
296 + }
297 + defer resp.Body.Close()
298 +
299 + var result struct {
300 + Success bool `json:"success"`
301 + Error struct {
302 + Code int `json:"code"`
303 + Type string `json:"type"`
304 + } `json:"error"`
305 + EABKID string `json:"eab_kid"`
306 + EABHMACKey string `json:"eab_hmac_key"`
307 + }
308 + err = json.NewDecoder(resp.Body).Decode(&result)
309 + if err != nil {
310 + return nil, acct, fmt.Errorf("decoding API response: %v", err)
311 + }
312 + if result.Error.Code != 0 {
313 + // do this check first because ZeroSSL's API returns 200 on errors
314 + return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)",
315 + resp.StatusCode, result.Error.Type, result.Error.Code)
316 + }
317 + if resp.StatusCode != http.StatusOK {
318 + return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
319 + }
320 +
321 + iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID))
322 +
323 + return &acme.EAB{
324 + KeyID: result.EABKID,
325 + MACKey: result.EABHMACKey,
326 + }, acct, nil
327 +}
328 +
329 // UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
330 //
331 // ... acme [<directory_url>] {
modules/caddytls/automation.go
+38 -9
@@ -24,7 +24,7 @@ import (
24 "strings"
25
26 "github.com/caddyserver/certmagic"
27 - "github.com/mholt/acmez"
27 + "github.com/mholt/acmez/v2"
28 "go.uber.org/zap"
29
30 "github.com/caddyserver/caddy/v2"
@@ -201,6 +201,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
201 // store them on the policy before putting it on the config
202
203 // load and provision any cert manager modules
204 + hadExplicitManagers := len(ap.ManagersRaw) > 0
205 if ap.ManagersRaw != nil {
206 vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw")
207 if err != nil {
@@ -256,12 +257,25 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
257 if ap.OnDemand || len(ap.Managers) > 0 {
258 // permission module is now required after a number of negligence cases that allowed abuse;
259 // but it may still be optional for explicit subjects (bounded, non-wildcard), for the
259 - // internal issuer since it doesn't cause public PKI pressure on ACME servers
260 - if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) {
261 - return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
260 + // internal issuer since it doesn't cause public PKI pressure on ACME servers; subtly, it
261 + // is useful to allow on-demand TLS to be enabled so Managers can be used, but to still
262 + // prevent issuance from Issuers (when Managers don't provide a certificate) if there's no
263 + // permission module configured
264 + noProtections := ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil)
265 + failClosed := noProtections && hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured
266 + if noProtections {
267 + if !hadExplicitManagers {
268 + // no managers, no explicitly-configured permission module, this is a config error
269 + return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
270 + }
271 + // allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers
272 + tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified")
273 }
274 ond = &certmagic.OnDemandConfig{
275 DecisionFunc: func(ctx context.Context, name string) error {
276 + if failClosed {
277 + return fmt.Errorf("no permission module configured; certificates not allowed except from external Managers")
278 + }
279 if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil {
280 return nil
281 }
@@ -344,6 +358,16 @@ func (ap *AutomationPolicy) Subjects() []string {
358 return ap.subjects
359 }
360
361 +// AllInternalSubjects returns true if all the subjects on this policy are internal.
362 +func (ap *AutomationPolicy) AllInternalSubjects() bool {
363 + for _, subj := range ap.subjects {
364 + if !certmagic.SubjectIsInternal(subj) {
365 + return false
366 + }
367 + }
368 + return true
369 +}
370 +
371 func (ap *AutomationPolicy) onlyInternalIssuer() bool {
372 if len(ap.Issuers) != 1 {
373 return false
@@ -370,17 +394,21 @@ func (ap *AutomationPolicy) isWildcardOrDefault() bool {
394
395 // DefaultIssuers returns empty Issuers (not provisioned) to be used as defaults.
396 // This function is experimental and has no compatibility promises.
373 -func DefaultIssuers() []certmagic.Issuer {
374 - return []certmagic.Issuer{
375 - new(ACMEIssuer),
376 - &ZeroSSLIssuer{ACMEIssuer: new(ACMEIssuer)},
397 +func DefaultIssuers(userEmail string) []certmagic.Issuer {
398 + issuers := []certmagic.Issuer{new(ACMEIssuer)}
399 + if strings.TrimSpace(userEmail) != "" {
400 + issuers = append(issuers, &ACMEIssuer{
401 + CA: certmagic.ZeroSSLProductionCA,
402 + Email: userEmail,
403 + })
404 }
405 + return issuers
406 }
407
408 // DefaultIssuersProvisioned returns empty but provisioned default Issuers from
409 // DefaultIssuers(). This function is experimental and has no compatibility promises.
410 func DefaultIssuersProvisioned(ctx caddy.Context) ([]certmagic.Issuer, error) {
383 - issuers := DefaultIssuers()
411 + issuers := DefaultIssuers("")
412 for i, iss := range issuers {
413 if prov, ok := iss.(caddy.Provisioner); ok {
414 err := prov.Provision(ctx)
@@ -453,6 +481,7 @@ type TLSALPNChallengeConfig struct {
481 type DNSChallengeConfig struct {
482 // The DNS provider module to use which will manage
483 // the DNS records relevant to the ACME challenge.
484 + // Required.
485 ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
486
487 // The TTL of the TXT record used for the DNS challenge.
modules/caddytls/capools.go
+1 -2
@@ -579,8 +579,7 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
579 customTransport.TLSClientConfig = tlsConfig
580 }
581
582 - var httpClient *http.Client
583 - *httpClient = *http.DefaultClient
582 + httpClient := *http.DefaultClient
583 httpClient.Transport = customTransport
584
585 for _, uri := range hcp.Endpoints {
modules/caddytls/certmanagers.go
+9
@@ -96,6 +96,11 @@ type HTTPCertGetter struct {
96 // To be valid, the response must be HTTP 200 with a PEM body
97 // consisting of blocks for the certificate chain and the private
98 // key.
99 + //
100 + // To indicate that this manager is not managing a certificate for
101 + // the described handshake, the endpoint should return HTTP 204
102 + // (No Content). Error statuses will indicate that the manager is
103 + // capable of providing a certificate but was unable to.
104 URL string `json:"url,omitempty"`
105
106 ctx context.Context
@@ -147,6 +152,10 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH
152 return nil, err
153 }
154 defer resp.Body.Close()
155 + if resp.StatusCode == http.StatusNoContent {
156 + // endpoint is not managing certs for this handshake
157 + return nil, nil
158 + }
159 if resp.StatusCode != http.StatusOK {
160 return nil, fmt.Errorf("got HTTP %d", resp.StatusCode)
161 }
modules/caddytls/connpolicy.go
+1 -1
@@ -26,7 +26,7 @@ import (
26 "path/filepath"
27 "strings"
28
29 - "github.com/mholt/acmez"
29 + "github.com/mholt/acmez/v2"
30 "go.uber.org/zap"
31
32 "github.com/caddyserver/caddy/v2"
modules/caddytls/tls.go
+35 -20
@@ -176,8 +176,9 @@ func (t *TLS) Provision(ctx caddy.Context) error {
176 t.Automation.OnDemand.permission = val.(OnDemandPermission)
177 }
178
179 - // on-demand rate limiting
179 + // on-demand rate limiting (TODO: deprecated, and should be removed later; rate limiting is ineffective now that permission modules are required)
180 if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
181 + t.logger.Warn("DEPRECATED: on_demand.rate_limit will be removed in a future release; use permission modules or external certificate managers instead")
182 onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst)
183 onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
184 } else {
@@ -413,35 +414,49 @@ func (t *TLS) Manage(names []string) error {
414 return nil
415 }
416
416 -// HandleHTTPChallenge ensures that the HTTP challenge is handled for the
417 -// certificate named by r.Host, if it is an HTTP challenge request. It
418 -// requires that the automation policy for r.Host has an issuer of type
419 -// *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()).
417 +// HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP
418 +// validation request is handled for the certificate named by r.Host, if it
419 +// is an HTTP challenge request. It requires that the automation policy for
420 +// r.Host has an issuer that implements GetACMEIssuer() or is a *ZeroSSLIssuer.
421 func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
422 + acmeChallenge := certmagic.LooksLikeHTTPChallenge(r)
423 + zerosslValidation := certmagic.LooksLikeZeroSSLHTTPValidation(r)
424 +
425 // no-op if it's not an ACME challenge request
422 - if !certmagic.LooksLikeHTTPChallenge(r) {
426 + if !acmeChallenge && !zerosslValidation {
427 return false
428 }
429
430 // try all the issuers until we find the one that initiated the challenge
431 ap := t.getAutomationPolicyForName(r.Host)
428 - type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
429 - for _, iss := range ap.magic.Issuers {
430 - if am, ok := iss.(acmeCapable); ok {
431 - iss := am.GetACMEIssuer()
432 - if iss.issuer.HandleHTTPChallenge(w, r) {
433 - return true
432 +
433 + if acmeChallenge {
434 + type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
435 +
436 + for _, iss := range ap.magic.Issuers {
437 + if acmeIssuer, ok := iss.(acmeCapable); ok {
438 + if acmeIssuer.GetACMEIssuer().issuer.HandleHTTPChallenge(w, r) {
439 + return true
440 + }
441 }
442 }
436 - }
443
438 - // it's possible another server in this process initiated the challenge;
439 - // users have requested that Caddy only handle HTTP challenges it initiated,
440 - // so that users can proxy the others through to their backends; but we
441 - // might not have an automation policy for all identifiers that are trying
442 - // to get certificates (e.g. the admin endpoint), so we do this manual check
443 - if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok {
444 - return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge)
444 + // it's possible another server in this process initiated the challenge;
445 + // users have requested that Caddy only handle HTTP challenges it initiated,
446 + // so that users can proxy the others through to their backends; but we
447 + // might not have an automation policy for all identifiers that are trying
448 + // to get certificates (e.g. the admin endpoint), so we do this manual check
449 + if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok {
450 + return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge)
451 + }
452 + } else if zerosslValidation {
453 + for _, iss := range ap.magic.Issuers {
454 + if ziss, ok := iss.(*ZeroSSLIssuer); ok {
455 + if ziss.issuer.HandleZeroSSLHTTPValidation(w, r) {
456 + return true
457 + }
458 + }
459 + }
460 }
461
462 return false
modules/caddytls/zerosslissuer.go
+172 -158
@@ -17,19 +17,15 @@ package caddytls
17 import (
18 "context"
19 "crypto/x509"
20 - "encoding/json"
20 "fmt"
22 - "io"
23 - "net/http"
24 - "net/url"
25 - "strings"
26 - "sync"
21 + "strconv"
22 + "time"
23
24 "github.com/caddyserver/certmagic"
29 - "github.com/mholt/acmez/acme"
25 "go.uber.org/zap"
26
27 "github.com/caddyserver/caddy/v2"
28 + "github.com/caddyserver/caddy/v2/caddyconfig"
29 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
30 )
31
@@ -37,24 +33,36 @@ func init() {
33 caddy.RegisterModule(new(ZeroSSLIssuer))
34 }
35
40 -// ZeroSSLIssuer makes an ACME issuer for getting certificates
41 -// from ZeroSSL by automatically generating EAB credentials.
42 -// Please be sure to set a valid email address in your config
43 -// so you can access/manage your domains in your ZeroSSL account.
44 -//
45 -// This issuer is only needed for automatic generation of EAB
46 -// credentials. If manually configuring/reusing EAB credentials,
47 -// the standard ACMEIssuer may be used if desired.
36 +// ZeroSSLIssuer uses the ZeroSSL API to get certificates.
37 +// Note that this is distinct from ZeroSSL's ACME endpoint.
38 +// To use ZeroSSL's ACME endpoint, use the ACMEIssuer
39 +// configured with ZeroSSL's ACME directory endpoint.
40 type ZeroSSLIssuer struct {
49 - *ACMEIssuer
50 -
41 // The API key (or "access key") for using the ZeroSSL API.
52 - // This is optional, but can be used if you have an API key
53 - // already and don't want to supply your email address.
42 + // REQUIRED.
43 APIKey string `json:"api_key,omitempty"`
44
56 - mu sync.Mutex
57 - logger *zap.Logger
45 + // How many days the certificate should be valid for.
46 + // Only certain values are accepted; see ZeroSSL docs.
47 + ValidityDays int `json:"validity_days,omitempty"`
48 +
49 + // The host to bind to when opening a listener for
50 + // verifying domain names (or IPs).
51 + ListenHost string `json:"listen_host,omitempty"`
52 +
53 + // If HTTP is forwarded from port 80, specify the
54 + // forwarded port here.
55 + AlternateHTTPPort int `json:"alternate_http_port,omitempty"`
56 +
57 + // Use CNAME validation instead of HTTP. ZeroSSL's
58 + // API uses CNAME records for DNS validation, similar
59 + // to how Let's Encrypt uses TXT records for the
60 + // DNS challenge.
61 + CNAMEValidation *DNSChallengeConfig `json:"cname_validation,omitempty"`
62 +
63 + logger *zap.Logger
64 + storage certmagic.Storage
65 + issuer *certmagic.ZeroSSLIssuer
66 }
67
68 // CaddyModule returns the Caddy module information.
@@ -65,178 +73,184 @@ func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo {
73 }
74 }
75
68 -// Provision sets up iss.
76 +// Provision sets up the issuer.
77 func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error {
78 iss.logger = ctx.Logger()
71 - if iss.ACMEIssuer == nil {
72 - iss.ACMEIssuer = new(ACMEIssuer)
73 - }
74 - if iss.ACMEIssuer.CA == "" {
75 - iss.ACMEIssuer.CA = certmagic.ZeroSSLProductionCA
76 - }
77 - return iss.ACMEIssuer.Provision(ctx)
78 -}
79 + iss.storage = ctx.Storage()
80 + repl := caddy.NewReplacer()
81
80 -// newAccountCallback generates EAB if not already provided. It also sets a valid default contact on the account if not set.
81 -func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
82 - if acmeIss.ExternalAccount != nil {
83 - return acct, nil
84 - }
85 - var err error
86 - acmeIss.ExternalAccount, acct, err = iss.generateEABCredentials(ctx, acct)
87 - return acct, err
88 -}
89 -
90 -// generateEABCredentials generates EAB credentials using the API key if provided,
91 -// otherwise using the primary contact email on the issuer. If an email is not set
92 -// on the issuer, a default generic email is used.
93 -func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) {
94 - var endpoint string
95 - var body io.Reader
96 -
97 - // there are two ways to generate EAB credentials: authenticated with
98 - // their API key, or unauthenticated with their email address
99 - if iss.APIKey != "" {
100 - apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "")
101 - if apiKey == "" {
102 - return nil, acct, fmt.Errorf("missing API key: '%v'", iss.APIKey)
103 - }
104 - qs := url.Values{"access_key": []string{apiKey}}
105 - endpoint = fmt.Sprintf("%s/eab-credentials?%s", zerosslAPIBase, qs.Encode())
106 - } else {
107 - email := iss.Email
108 - if email == "" {
109 - iss.logger.Warn("missing email address for ZeroSSL; it is strongly recommended to set one for next time")
110 - email = "caddy@zerossl.com" // special email address that preserves backwards-compat, but which black-holes dashboard features, oh well
82 + var dnsManager *certmagic.DNSManager
83 + if iss.CNAMEValidation != nil && len(iss.CNAMEValidation.ProviderRaw) > 0 {
84 + val, err := ctx.LoadModule(iss.CNAMEValidation, "ProviderRaw")
85 + if err != nil {
86 + return fmt.Errorf("loading DNS provider module: %v", err)
87 }
112 - if len(acct.Contact) == 0 {
113 - // we borrow the email from config or the default email, so ensure it's saved with the account
114 - acct.Contact = []string{"mailto:" + email}
88 + dnsManager = &certmagic.DNSManager{
89 + DNSProvider: val.(certmagic.DNSProvider),
90 + TTL: time.Duration(iss.CNAMEValidation.TTL),
91 + PropagationDelay: time.Duration(iss.CNAMEValidation.PropagationDelay),
92 + PropagationTimeout: time.Duration(iss.CNAMEValidation.PropagationTimeout),
93 + Resolvers: iss.CNAMEValidation.Resolvers,
94 + OverrideDomain: iss.CNAMEValidation.OverrideDomain,
95 + Logger: iss.logger.Named("cname"),
96 }
116 - endpoint = zerosslAPIBase + "/eab-credentials-email"
117 - form := url.Values{"email": []string{email}}
118 - body = strings.NewReader(form.Encode())
119 - }
120 -
121 - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
122 - if err != nil {
123 - return nil, acct, fmt.Errorf("forming request: %v", err)
124 - }
125 - if body != nil {
126 - req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
127 - }
128 - req.Header.Set("User-Agent", certmagic.UserAgent)
129 -
130 - resp, err := http.DefaultClient.Do(req)
131 - if err != nil {
132 - return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err)
133 - }
134 - defer resp.Body.Close()
135 -
136 - var result struct {
137 - Success bool `json:"success"`
138 - Error struct {
139 - Code int `json:"code"`
140 - Type string `json:"type"`
141 - } `json:"error"`
142 - EABKID string `json:"eab_kid"`
143 - EABHMACKey string `json:"eab_hmac_key"`
144 - }
145 - err = json.NewDecoder(resp.Body).Decode(&result)
146 - if err != nil {
147 - return nil, acct, fmt.Errorf("decoding API response: %v", err)
148 - }
149 - if result.Error.Code != 0 {
150 - return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)",
151 - resp.StatusCode, result.Error.Type, result.Error.Code)
97 }
153 - if resp.StatusCode != http.StatusOK {
154 - return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
155 - }
156 -
157 - iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID))
158 -
159 - return &acme.EAB{
160 - KeyID: result.EABKID,
161 - MACKey: result.EABHMACKey,
162 - }, acct, nil
163 -}
98
165 -// initialize modifies the template for the underlying ACMEIssuer
166 -// values by setting the CA endpoint to the ZeroSSL directory and
167 -// setting the NewAccountFunc callback to one which allows us to
168 -// generate EAB credentials only if a new account is being made.
169 -// Since it modifies the stored template, its effect should only
170 -// be needed once, but it is fine to call it repeatedly.
171 -func (iss *ZeroSSLIssuer) initialize() {
172 - iss.mu.Lock()
173 - defer iss.mu.Unlock()
174 - if iss.ACMEIssuer.issuer.NewAccountFunc == nil {
175 - iss.ACMEIssuer.issuer.NewAccountFunc = iss.newAccountCallback
99 + iss.issuer = &certmagic.ZeroSSLIssuer{
100 + APIKey: repl.ReplaceAll(iss.APIKey, ""),
101 + ValidityDays: iss.ValidityDays,
102 + ListenHost: iss.ListenHost,
103 + AltHTTPPort: iss.AlternateHTTPPort,
104 + Storage: iss.storage,
105 + CNAMEValidation: dnsManager,
106 + Logger: iss.logger,
107 }
177 -}
108
179 -// PreCheck implements the certmagic.PreChecker interface.
180 -func (iss *ZeroSSLIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
181 - iss.initialize()
182 - return iss.ACMEIssuer.PreCheck(ctx, names, interactive)
109 + return nil
110 }
111
112 // Issue obtains a certificate for the given csr.
113 func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
187 - iss.initialize()
188 - return iss.ACMEIssuer.Issue(ctx, csr)
114 + return iss.issuer.Issue(ctx, csr)
115 }
116
117 // IssuerKey returns the unique issuer key for the configured CA endpoint.
118 func (iss *ZeroSSLIssuer) IssuerKey() string {
193 - iss.initialize()
194 - return iss.ACMEIssuer.IssuerKey()
119 + return iss.issuer.IssuerKey()
120 }
121
122 // Revoke revokes the given certificate.
123 func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error {
199 - iss.initialize()
200 - return iss.ACMEIssuer.Revoke(ctx, cert, reason)
124 + return iss.issuer.Revoke(ctx, cert, reason)
125 }
126
127 // UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
128 //
205 -// ... zerossl [<api_key>] {
206 -// ...
129 +// ... zerossl <api_key> {
130 +// validity_days <days>
131 +// alt_http_port <port>
132 +// dns <provider_name> ...
133 +// propagation_delay <duration>
134 +// propagation_timeout <duration>
135 +// resolvers <list...>
136 +// dns_ttl <duration>
137 // }
208 -//
209 -// Any of the subdirectives for the ACME issuer can be used in the block.
138 func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
139 d.Next() // consume issuer name
140 +
141 + // API key is required
142 + if !d.NextArg() {
143 + return d.ArgErr()
144 + }
145 + iss.APIKey = d.Val()
146 if d.NextArg() {
213 - iss.APIKey = d.Val()
214 - if d.NextArg() {
215 - return d.ArgErr()
216 - }
147 + return d.ArgErr()
148 }
149
219 - if iss.ACMEIssuer == nil {
220 - iss.ACMEIssuer = new(ACMEIssuer)
221 - }
222 - err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment())
223 - if err != nil {
224 - return err
150 + for nesting := d.Nesting(); d.NextBlock(nesting); {
151 + switch d.Val() {
152 + case "validity_days":
153 + if iss.ValidityDays != 0 {
154 + return d.Errf("validity days is already specified: %d", iss.ValidityDays)
155 + }
156 + days, err := strconv.Atoi(d.Val())
157 + if err != nil {
158 + return d.Errf("invalid number of days %s: %v", d.Val(), err)
159 + }
160 + iss.ValidityDays = days
161 +
162 + case "alt_http_port":
163 + if !d.NextArg() {
164 + return d.ArgErr()
165 + }
166 + port, err := strconv.Atoi(d.Val())
167 + if err != nil {
168 + return d.Errf("invalid port %s: %v", d.Val(), err)
169 + }
170 + iss.AlternateHTTPPort = port
171 +
172 + case "dns":
173 + if !d.NextArg() {
174 + return d.ArgErr()
175 + }
176 + provName := d.Val()
177 + if iss.CNAMEValidation == nil {
178 + iss.CNAMEValidation = new(DNSChallengeConfig)
179 + }
180 + unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName)
181 + if err != nil {
182 + return err
183 + }
184 + iss.CNAMEValidation.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil)
185 +
186 + case "propagation_delay":
187 + if !d.NextArg() {
188 + return d.ArgErr()
189 + }
190 + delayStr := d.Val()
191 + delay, err := caddy.ParseDuration(delayStr)
192 + if err != nil {
193 + return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err)
194 + }
195 + if iss.CNAMEValidation == nil {
196 + iss.CNAMEValidation = new(DNSChallengeConfig)
197 + }
198 + iss.CNAMEValidation.PropagationDelay = caddy.Duration(delay)
199 +
200 + case "propagation_timeout":
201 + if !d.NextArg() {
202 + return d.ArgErr()
203 + }
204 + timeoutStr := d.Val()
205 + var timeout time.Duration
206 + if timeoutStr == "-1" {
207 + timeout = time.Duration(-1)
208 + } else {
209 + var err error
210 + timeout, err = caddy.ParseDuration(timeoutStr)
211 + if err != nil {
212 + return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err)
213 + }
214 + }
215 + if iss.CNAMEValidation == nil {
216 + iss.CNAMEValidation = new(DNSChallengeConfig)
217 + }
218 + iss.CNAMEValidation.PropagationTimeout = caddy.Duration(timeout)
219 +
220 + case "resolvers":
221 + if iss.CNAMEValidation == nil {
222 + iss.CNAMEValidation = new(DNSChallengeConfig)
223 + }
224 + iss.CNAMEValidation.Resolvers = d.RemainingArgs()
225 + if len(iss.CNAMEValidation.Resolvers) == 0 {
226 + return d.ArgErr()
227 + }
228 +
229 + case "dns_ttl":
230 + if !d.NextArg() {
231 + return d.ArgErr()
232 + }
233 + ttlStr := d.Val()
234 + ttl, err := caddy.ParseDuration(ttlStr)
235 + if err != nil {
236 + return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err)
237 + }
238 + if iss.CNAMEValidation == nil {
239 + iss.CNAMEValidation = new(DNSChallengeConfig)
240 + }
241 + iss.CNAMEValidation.TTL = caddy.Duration(ttl)
242 +
243 + default:
244 + return d.Errf("unrecognized zerossl issuer property: %s", d.Val())
245 + }
246 }
247 +
248 return nil
249 }
250
229 -const zerosslAPIBase = "https://api.zerossl.com/acme"
230 -
251 // Interface guards
252 var (
233 - _ certmagic.PreChecker = (*ZeroSSLIssuer)(nil)
234 - _ certmagic.Issuer = (*ZeroSSLIssuer)(nil)
235 - _ certmagic.Revoker = (*ZeroSSLIssuer)(nil)
236 - _ caddy.Provisioner = (*ZeroSSLIssuer)(nil)
237 - _ ConfigSetter = (*ZeroSSLIssuer)(nil)
238 -
239 - // a type which properly embeds an ACMEIssuer should implement
240 - // this interface so it can be treated as an ACMEIssuer
241 - _ interface{ GetACMEIssuer() *ACMEIssuer } = (*ZeroSSLIssuer)(nil)
253 + _ certmagic.Issuer = (*ZeroSSLIssuer)(nil)
254 + _ certmagic.Revoker = (*ZeroSSLIssuer)(nil)
255 + _ caddy.Provisioner = (*ZeroSSLIssuer)(nil)
256 )