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
)