mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
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
This commit is contained in:
parent
dc9dd2e4b3
commit
81413caea2
27 changed files with 444 additions and 298 deletions
1
admin.go
1
admin.go
|
@ -474,7 +474,6 @@ func manageIdentity(ctx Context, cfg *Config) error {
|
||||||
// import the caddytls package -- but it works
|
// import the caddytls package -- but it works
|
||||||
if cfg.Admin.Identity.IssuersRaw == nil {
|
if cfg.Admin.Identity.IssuersRaw == nil {
|
||||||
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
|
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
|
||||||
json.RawMessage(`{"module": "zerossl"}`),
|
|
||||||
json.RawMessage(`{"module": "acme"}`),
|
json.RawMessage(`{"module": "acme"}`),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez/acme"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
@ -107,7 +107,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
var onDemand bool
|
var onDemand bool
|
||||||
var reusePrivateKeys bool
|
var reusePrivateKeys bool
|
||||||
|
|
||||||
// file certificate loader
|
|
||||||
firstLine := h.RemainingArgs()
|
firstLine := h.RemainingArgs()
|
||||||
switch len(firstLine) {
|
switch len(firstLine) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -117,13 +116,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
} else if !strings.Contains(firstLine[0], "@") {
|
} else if !strings.Contains(firstLine[0], "@") {
|
||||||
return nil, h.Err("single argument must either be 'internal' or an email address")
|
return nil, h.Err("single argument must either be 'internal' or an email address")
|
||||||
} else {
|
} else {
|
||||||
if acmeIssuer == nil {
|
acmeIssuer = &caddytls.ACMEIssuer{
|
||||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
Email: firstLine[0],
|
||||||
}
|
}
|
||||||
acmeIssuer.Email = firstLine[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
|
// file certificate loader
|
||||||
certFilename := firstLine[0]
|
certFilename := firstLine[0]
|
||||||
keyFilename := firstLine[1]
|
keyFilename := firstLine[1]
|
||||||
|
|
||||||
|
@ -488,19 +487,24 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
|
|
||||||
case acmeIssuer != nil:
|
case acmeIssuer != nil:
|
||||||
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
||||||
defaultIssuers := caddytls.DefaultIssuers()
|
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
||||||
|
|
||||||
// if a CA endpoint was set, override multiple implicit issuers since it's a specific one
|
// if an ACME CA endpoint was set, the user expects to use that specific one,
|
||||||
|
// not any others that may be defaults, so replace all defaults with that ACME CA
|
||||||
if acmeIssuer.CA != "" {
|
if acmeIssuer.CA != "" {
|
||||||
defaultIssuers = []certmagic.Issuer{acmeIssuer}
|
defaultIssuers = []certmagic.Issuer{acmeIssuer}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, issuer := range defaultIssuers {
|
for _, issuer := range defaultIssuers {
|
||||||
switch iss := issuer.(type) {
|
// apply settings from the implicitly-configured ACMEIssuer to any
|
||||||
case *caddytls.ACMEIssuer:
|
// default ACMEIssuers, but preserve each default issuer's CA endpoint,
|
||||||
issuer = acmeIssuer
|
// because, for example, if you configure the DNS challenge, it should
|
||||||
case *caddytls.ZeroSSLIssuer:
|
// apply to any of the default ACMEIssuers, but you don't want to trample
|
||||||
iss.ACMEIssuer = acmeIssuer
|
// out their unique CA endpoints
|
||||||
|
if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
|
||||||
|
acmeCopy := *acmeIssuer
|
||||||
|
acmeCopy.CA = iss.CA
|
||||||
|
issuer = &acmeCopy
|
||||||
}
|
}
|
||||||
configVals = append(configVals, ConfigValue{
|
configVals = append(configVals, ConfigValue{
|
||||||
Class: "tls.cert_issuer",
|
Class: "tls.cert_issuer",
|
||||||
|
|
|
@ -18,7 +18,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez/acme"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
@ -212,9 +212,9 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
prov, ok := unm.(certmagic.ACMEDNSProvider)
|
prov, ok := unm.(certmagic.DNSProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
|
return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm)
|
||||||
}
|
}
|
||||||
return prov, nil
|
return prov, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez/acme"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
@ -224,7 +224,7 @@ func (st ServerType) buildTLSApp(
|
||||||
var internal, external []string
|
var internal, external []string
|
||||||
for _, s := range ap.SubjectsRaw {
|
for _, s := range ap.SubjectsRaw {
|
||||||
// do not create Issuers for Tailscale domains; they will be given a Manager instead
|
// do not create Issuers for Tailscale domains; they will be given a Manager instead
|
||||||
if strings.HasSuffix(strings.ToLower(s), ".ts.net") {
|
if isTailscaleDomain(s) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !certmagic.SubjectQualifiesForCert(s) {
|
if !certmagic.SubjectQualifiesForCert(s) {
|
||||||
|
@ -378,15 +378,12 @@ func (st ServerType) buildTLSApp(
|
||||||
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
||||||
// for public names, create default issuers which will later be filled in with configured global defaults
|
// for public names, create default issuers which will later be filled in with configured global defaults
|
||||||
// (internal names will implicitly use the internal issuer at auto-https time)
|
// (internal names will implicitly use the internal issuer at auto-https time)
|
||||||
ap.Issuers = caddytls.DefaultIssuers()
|
emailStr, _ := globalEmail.(string)
|
||||||
|
ap.Issuers = caddytls.DefaultIssuers(emailStr)
|
||||||
|
|
||||||
// if a specific endpoint is configured, can't use multiple default issuers
|
// if a specific endpoint is configured, can't use multiple default issuers
|
||||||
if globalACMECA != nil {
|
if globalACMECA != nil {
|
||||||
if strings.Contains(globalACMECA.(string), "zerossl") {
|
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
||||||
ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
|
|
||||||
} else {
|
|
||||||
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -666,17 +663,33 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
|
||||||
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
|
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
|
||||||
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
|
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
|
||||||
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
|
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
|
||||||
|
//
|
||||||
|
// IP subjects are considered as non-qualifying for public certs. Technically, there are
|
||||||
|
// now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function
|
||||||
|
// is used solely for implicit automation (defaults), where it gets really complicated to
|
||||||
|
// keep track of which issuers support IP certificates in which circumstances. Currently,
|
||||||
|
// issuers that support IP certificates are very few, and all require some sort of config
|
||||||
|
// from the user anyway (such as an account credential). Since we cannot implicitly and
|
||||||
|
// automatically get public IP certs without configuration from the user, we treat IPs as
|
||||||
|
// not qualifying for public certificates. Users should expressly configure an issuer
|
||||||
|
// that supports IP certs for that purpose.
|
||||||
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
|
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
|
||||||
return !certmagic.SubjectIsIP(subj) &&
|
return !certmagic.SubjectIsIP(subj) &&
|
||||||
!certmagic.SubjectIsInternal(subj) &&
|
!certmagic.SubjectIsInternal(subj) &&
|
||||||
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automationPolicyHasAllPublicNames returns true if all the names on the policy
|
||||||
|
// do NOT qualify for public certs OR are tailscale domains.
|
||||||
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
||||||
for _, subj := range ap.SubjectsRaw {
|
for _, subj := range ap.SubjectsRaw {
|
||||||
if !subjectQualifiesForPublicCert(ap, subj) {
|
if !subjectQualifiesForPublicCert(ap, subj) || isTailscaleDomain(subj) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isTailscaleDomain(name string) bool {
|
||||||
|
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
|
||||||
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
"github.com/mholt/acmez"
|
"github.com/mholt/acmez/v2"
|
||||||
"github.com/mholt/acmez/acme"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
smallstepacme "github.com/smallstep/certificates/acme"
|
smallstepacme "github.com/smallstep/certificates/acme"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
@ -77,7 +77,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
|
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("obtaining certificate: %v", err)
|
t.Errorf("obtaining certificate: %v", err)
|
||||||
return
|
return
|
||||||
|
@ -146,7 +146,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
|
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
|
||||||
if len(certs) > 0 {
|
if len(certs) > 0 {
|
||||||
t.Errorf("expected '0' certificates, but received '%d'", len(certs))
|
t.Errorf("expected '0' certificates, but received '%d'", len(certs))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
"github.com/mholt/acmez"
|
"github.com/mholt/acmez/v2"
|
||||||
"github.com/mholt/acmez/acme"
|
"github.com/mholt/acmez/v2/acme"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,12 +105,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
certs, err := client.ObtainCertificate(
|
certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
|
||||||
ctx,
|
|
||||||
account,
|
|
||||||
certPrivateKey,
|
|
||||||
[]string{"localhost"},
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("obtaining certificate for allowed domain: %v", err)
|
t.Errorf("obtaining certificate for allowed domain: %v", err)
|
||||||
return
|
return
|
||||||
|
@ -126,7 +121,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
|
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
|
||||||
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
|
@ -199,7 +194,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
_, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
t.Errorf("obtaining certificate for 'deny.localhost' domain")
|
||||||
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
|
||||||
|
|
|
@ -40,12 +40,6 @@ example.com
|
||||||
"preferred_chains": {
|
"preferred_chains": {
|
||||||
"smallest": true
|
"smallest": true
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"module": "zerossl",
|
|
||||||
"preferred_chains": {
|
|
||||||
"smallest": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,8 +70,9 @@ c.example.com {
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||||
"email": "abc@example.com",
|
"email": "abc@example.com",
|
||||||
"module": "zerossl"
|
"module": "acme"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -131,8 +131,9 @@ abc.de {
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||||
"email": "my.email@example.com",
|
"email": "my.email@example.com",
|
||||||
"module": "zerossl"
|
"module": "acme"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,8 +86,9 @@ http://localhost:8081 {
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||||
"email": "abc@example.com",
|
"email": "abc@example.com",
|
||||||
"module": "zerossl"
|
"module": "acme"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,9 @@ example.com {
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||||
"email": "foo@bar",
|
"email": "foo@bar",
|
||||||
"module": "zerossl"
|
"module": "acme"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,14 +58,6 @@ tls {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"ttl": 310000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"module": "zerossl"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ tls {
|
||||||
issuer acme {
|
issuer acme {
|
||||||
dns_ttl 5m10s
|
dns_ttl 5m10s
|
||||||
}
|
}
|
||||||
issuer zerossl {
|
issuer zerossl api_key {
|
||||||
dns_ttl 10m20s
|
dns_ttl 10m20s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +65,9 @@ tls {
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"challenges": {
|
"api_key": "api_key",
|
||||||
"dns": {
|
"cname_validation": {
|
||||||
"ttl": 620000000000
|
"ttl": 620000000000
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"module": "zerossl"
|
"module": "zerossl"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ tls {
|
||||||
propagation_delay 5m10s
|
propagation_delay 5m10s
|
||||||
propagation_timeout 10m20s
|
propagation_timeout 10m20s
|
||||||
}
|
}
|
||||||
issuer zerossl {
|
issuer zerossl api_key {
|
||||||
propagation_delay 5m30s
|
propagation_delay 5m30s
|
||||||
propagation_timeout -1
|
propagation_timeout -1
|
||||||
}
|
}
|
||||||
|
@ -68,11 +68,10 @@ tls {
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"challenges": {
|
"api_key": "api_key",
|
||||||
"dns": {
|
"cname_validation": {
|
||||||
"propagation_delay": 330000000000,
|
"propagation_delay": 330000000000,
|
||||||
"propagation_timeout": -1
|
"propagation_timeout": -1
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"module": "zerossl"
|
"module": "zerossl"
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,15 +60,6 @@ tls {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"module": "acme"
|
"module": "acme"
|
||||||
},
|
|
||||||
{
|
|
||||||
"challenges": {
|
|
||||||
"dns": {
|
|
||||||
"propagation_delay": 310000000000,
|
|
||||||
"propagation_timeout": 620000000000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"module": "zerossl"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# USAGE: go run -exec ./setcap.sh main.go <args...>
|
# USAGE:
|
||||||
|
# go run -exec ./setcap.sh main.go <args...>
|
||||||
#
|
#
|
||||||
# (Example: `go run -exec ./setcap.sh main.go run --config caddy.json`)
|
# (Example: `go run -exec ./setcap.sh main.go run --config caddy.json`)
|
||||||
#
|
#
|
||||||
# For some reason this does not work on my Arch system, so if you find that's
|
# For some reason this does not work on my Arch system, so if you find that's
|
||||||
# the case, you can instead do: go build && ./setcap.sh ./caddy <args...>
|
# the case, you can instead do:
|
||||||
# but this will leave the ./caddy binary laying around.
|
|
||||||
#
|
#
|
||||||
|
# go build && ./setcap.sh ./caddy <args...>
|
||||||
|
#
|
||||||
|
# but this will leave the ./caddy binary laying around.
|
||||||
#
|
#
|
||||||
|
|
||||||
sudo setcap cap_net_bind_service=+ep "$1"
|
sudo setcap cap_net_bind_service=+ep "$1"
|
||||||
|
|
15
go.mod
15
go.mod
|
@ -1,22 +1,21 @@
|
||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.21
|
go 1.22.0
|
||||||
|
|
||||||
toolchain go1.21.4
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3
|
github.com/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/alecthomas/chroma/v2 v2.13.0
|
github.com/alecthomas/chroma/v2 v2.13.0
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||||
github.com/caddyserver/certmagic v0.20.0
|
github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570
|
||||||
|
github.com/caddyserver/zerossl v0.1.2
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/go-chi/chi/v5 v5.0.12
|
github.com/go-chi/chi/v5 v5.0.12
|
||||||
github.com/google/cel-go v0.20.0
|
github.com/google/cel-go v0.20.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/klauspost/compress v1.17.0
|
github.com/klauspost/compress v1.17.0
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5
|
github.com/klauspost/cpuid/v2 v2.2.7
|
||||||
github.com/mholt/acmez v1.2.0
|
github.com/mholt/acmez/v2 v2.0.0-beta.2
|
||||||
github.com/prometheus/client_golang v1.19.0
|
github.com/prometheus/client_golang v1.19.0
|
||||||
github.com/quic-go/quic-go v0.42.0
|
github.com/quic-go/quic-go v0.42.0
|
||||||
github.com/smallstep/certificates v0.25.3-rc5
|
github.com/smallstep/certificates v0.25.3-rc5
|
||||||
|
@ -112,12 +111,12 @@ require (
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgtype v1.14.0 // indirect
|
github.com/jackc/pgtype v1.14.0 // indirect
|
||||||
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
github.com/jackc/pgx/v4 v4.18.3 // indirect
|
||||||
github.com/libdns/libdns v0.2.1 // indirect
|
github.com/libdns/libdns v0.2.2 // indirect
|
||||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/miekg/dns v1.1.55 // indirect
|
github.com/miekg/dns v1.1.58 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
|
22
go.sum
22
go.sum
|
@ -68,8 +68,10 @@ github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
|
github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570 h1:SsAXjoQx2wOmLl6mEwJEwh7wwys2hb/l/mhtmxA3wts=
|
||||||
github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
|
github.com/caddyserver/certmagic v0.20.1-0.20240412214119-167015dd6570/go.mod h1:e1NhB1rF5KZnAuAX6oSyhE7sg1Ru5bWgggw5RtauhEY=
|
||||||
|
github.com/caddyserver/zerossl v0.1.2 h1:tlEu1VzWGoqcCpivs9liKAKhfpJWYJkHEMmlxRbVAxE=
|
||||||
|
github.com/caddyserver/zerossl v0.1.2/go.mod h1:wtiJEHbdvunr40ZzhXlnIkOB8Xj4eKtBKizCcZitJiQ=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
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
|
||||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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=
|
||||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
|
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||||
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
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
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
|
github.com/mholt/acmez/v2 v2.0.0-beta.2 h1:GIgGILx8AWN0ePyTd+bjs2WDgNiIWm0nBwDLWp59aHc=
|
||||||
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
|
github.com/mholt/acmez/v2 v2.0.0-beta.2/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
|
||||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
|
|
@ -287,6 +287,16 @@ uniqueDomainsLoop:
|
||||||
for _, ap := range app.tlsApp.Automation.Policies {
|
for _, ap := range app.tlsApp.Automation.Policies {
|
||||||
for _, apHost := range ap.Subjects() {
|
for _, apHost := range ap.Subjects() {
|
||||||
if apHost == d {
|
if apHost == d {
|
||||||
|
// if the automation policy has all internal subjects but no issuers,
|
||||||
|
// it will default to CertMagic's issuers which are public CAs; use
|
||||||
|
// our internal issuer instead
|
||||||
|
if len(ap.Issuers) == 0 && ap.AllInternalSubjects() {
|
||||||
|
iss := new(caddytls.InternalIssuer)
|
||||||
|
if err := iss.Provision(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ap.Issuers = append(ap.Issuers, iss)
|
||||||
|
}
|
||||||
continue uniqueDomainsLoop
|
continue uniqueDomainsLoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,8 +213,6 @@ func DisabledTest(t *testing.T) {
|
||||||
// TODO: test chunked reader
|
// TODO: test chunked reader
|
||||||
globalt = t
|
globalt = t
|
||||||
|
|
||||||
rand.Seed(time.Now().UTC().UnixNano())
|
|
||||||
|
|
||||||
// server
|
// server
|
||||||
go func() {
|
go func() {
|
||||||
listener, err := net.Listen("tcp", ipPort)
|
listener, err := net.Listen("tcp", ipPort)
|
||||||
|
|
|
@ -17,14 +17,19 @@ package caddytls
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez"
|
"github.com/caddyserver/zerossl"
|
||||||
"github.com/mholt/acmez/acme"
|
"github.com/mholt/acmez/v2"
|
||||||
|
"github.com/mholt/acmez/v2/acme"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
@ -142,12 +147,14 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
|
||||||
iss.Challenges.DNS.solver = deprecatedProvider
|
iss.Challenges.DNS.solver = deprecatedProvider
|
||||||
} else {
|
} else {
|
||||||
iss.Challenges.DNS.solver = &certmagic.DNS01Solver{
|
iss.Challenges.DNS.solver = &certmagic.DNS01Solver{
|
||||||
DNSProvider: val.(certmagic.ACMEDNSProvider),
|
DNSManager: certmagic.DNSManager{
|
||||||
TTL: time.Duration(iss.Challenges.DNS.TTL),
|
DNSProvider: val.(certmagic.DNSProvider),
|
||||||
PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
|
TTL: time.Duration(iss.Challenges.DNS.TTL),
|
||||||
PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
|
PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
|
||||||
Resolvers: iss.Challenges.DNS.Resolvers,
|
PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
|
||||||
OverrideDomain: iss.Challenges.DNS.OverrideDomain,
|
Resolvers: iss.Challenges.DNS.Resolvers,
|
||||||
|
OverrideDomain: iss.Challenges.DNS.OverrideDomain,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,6 +217,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ZeroSSL requires EAB, but we can generate that automatically (requires an email address be configured)
|
||||||
|
if strings.HasPrefix(iss.CA, "https://acme.zerossl.com/") {
|
||||||
|
template.NewAccountFunc = func(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
|
||||||
|
if acmeIss.ExternalAccount != nil {
|
||||||
|
return acct, nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
acmeIss.ExternalAccount, acct, err = iss.generateZeroSSLEABCredentials(ctx, acct)
|
||||||
|
return acct, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return template, nil
|
return template, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +267,65 @@ func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateRes
|
||||||
// to be accessed and manipulated.
|
// to be accessed and manipulated.
|
||||||
func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss }
|
func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss }
|
||||||
|
|
||||||
|
// generateZeroSSLEABCredentials generates ZeroSSL EAB credentials for the primary contact email
|
||||||
|
// on the issuer. It should only be usedif the CA endpoint is ZeroSSL. An email address is required.
|
||||||
|
func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) {
|
||||||
|
if strings.TrimSpace(iss.Email) == "" {
|
||||||
|
return nil, acme.Account{}, fmt.Errorf("your email address is required to use ZeroSSL's ACME endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(acct.Contact) == 0 {
|
||||||
|
// we borrow the email from config or the default email, so ensure it's saved with the account
|
||||||
|
acct.Contact = []string{"mailto:" + iss.Email}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := zerossl.BaseURL + "/acme/eab-credentials-email"
|
||||||
|
form := url.Values{"email": []string{iss.Email}}
|
||||||
|
body := strings.NewReader(form.Encode())
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, acct, fmt.Errorf("forming request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("User-Agent", certmagic.UserAgent)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"error"`
|
||||||
|
EABKID string `json:"eab_kid"`
|
||||||
|
EABHMACKey string `json:"eab_hmac_key"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, acct, fmt.Errorf("decoding API response: %v", err)
|
||||||
|
}
|
||||||
|
if result.Error.Code != 0 {
|
||||||
|
// do this check first because ZeroSSL's API returns 200 on errors
|
||||||
|
return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)",
|
||||||
|
resp.StatusCode, result.Error.Type, result.Error.Code)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID))
|
||||||
|
|
||||||
|
return &acme.EAB{
|
||||||
|
KeyID: result.EABKID,
|
||||||
|
MACKey: result.EABHMACKey,
|
||||||
|
}, acct, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
|
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
|
||||||
//
|
//
|
||||||
// ... acme [<directory_url>] {
|
// ... acme [<directory_url>] {
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez"
|
"github.com/mholt/acmez/v2"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
@ -201,6 +201,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
|
||||||
// store them on the policy before putting it on the config
|
// store them on the policy before putting it on the config
|
||||||
|
|
||||||
// load and provision any cert manager modules
|
// load and provision any cert manager modules
|
||||||
|
hadExplicitManagers := len(ap.ManagersRaw) > 0
|
||||||
if ap.ManagersRaw != nil {
|
if ap.ManagersRaw != nil {
|
||||||
vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw")
|
vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -256,12 +257,25 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
|
||||||
if ap.OnDemand || len(ap.Managers) > 0 {
|
if ap.OnDemand || len(ap.Managers) > 0 {
|
||||||
// permission module is now required after a number of negligence cases that allowed abuse;
|
// permission module is now required after a number of negligence cases that allowed abuse;
|
||||||
// but it may still be optional for explicit subjects (bounded, non-wildcard), for the
|
// but it may still be optional for explicit subjects (bounded, non-wildcard), for the
|
||||||
// internal issuer since it doesn't cause public PKI pressure on ACME servers
|
// internal issuer since it doesn't cause public PKI pressure on ACME servers; subtly, it
|
||||||
if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) {
|
// is useful to allow on-demand TLS to be enabled so Managers can be used, but to still
|
||||||
return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
|
// prevent issuance from Issuers (when Managers don't provide a certificate) if there's no
|
||||||
|
// permission module configured
|
||||||
|
noProtections := ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil)
|
||||||
|
failClosed := noProtections && hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured
|
||||||
|
if noProtections {
|
||||||
|
if !hadExplicitManagers {
|
||||||
|
// no managers, no explicitly-configured permission module, this is a config error
|
||||||
|
return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
|
||||||
|
}
|
||||||
|
// allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers
|
||||||
|
tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified")
|
||||||
}
|
}
|
||||||
ond = &certmagic.OnDemandConfig{
|
ond = &certmagic.OnDemandConfig{
|
||||||
DecisionFunc: func(ctx context.Context, name string) error {
|
DecisionFunc: func(ctx context.Context, name string) error {
|
||||||
|
if failClosed {
|
||||||
|
return fmt.Errorf("no permission module configured; certificates not allowed except from external Managers")
|
||||||
|
}
|
||||||
if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil {
|
if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -344,6 +358,16 @@ func (ap *AutomationPolicy) Subjects() []string {
|
||||||
return ap.subjects
|
return ap.subjects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllInternalSubjects returns true if all the subjects on this policy are internal.
|
||||||
|
func (ap *AutomationPolicy) AllInternalSubjects() bool {
|
||||||
|
for _, subj := range ap.subjects {
|
||||||
|
if !certmagic.SubjectIsInternal(subj) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (ap *AutomationPolicy) onlyInternalIssuer() bool {
|
func (ap *AutomationPolicy) onlyInternalIssuer() bool {
|
||||||
if len(ap.Issuers) != 1 {
|
if len(ap.Issuers) != 1 {
|
||||||
return false
|
return false
|
||||||
|
@ -370,17 +394,21 @@ func (ap *AutomationPolicy) isWildcardOrDefault() bool {
|
||||||
|
|
||||||
// DefaultIssuers returns empty Issuers (not provisioned) to be used as defaults.
|
// DefaultIssuers returns empty Issuers (not provisioned) to be used as defaults.
|
||||||
// This function is experimental and has no compatibility promises.
|
// This function is experimental and has no compatibility promises.
|
||||||
func DefaultIssuers() []certmagic.Issuer {
|
func DefaultIssuers(userEmail string) []certmagic.Issuer {
|
||||||
return []certmagic.Issuer{
|
issuers := []certmagic.Issuer{new(ACMEIssuer)}
|
||||||
new(ACMEIssuer),
|
if strings.TrimSpace(userEmail) != "" {
|
||||||
&ZeroSSLIssuer{ACMEIssuer: new(ACMEIssuer)},
|
issuers = append(issuers, &ACMEIssuer{
|
||||||
|
CA: certmagic.ZeroSSLProductionCA,
|
||||||
|
Email: userEmail,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return issuers
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultIssuersProvisioned returns empty but provisioned default Issuers from
|
// DefaultIssuersProvisioned returns empty but provisioned default Issuers from
|
||||||
// DefaultIssuers(). This function is experimental and has no compatibility promises.
|
// DefaultIssuers(). This function is experimental and has no compatibility promises.
|
||||||
func DefaultIssuersProvisioned(ctx caddy.Context) ([]certmagic.Issuer, error) {
|
func DefaultIssuersProvisioned(ctx caddy.Context) ([]certmagic.Issuer, error) {
|
||||||
issuers := DefaultIssuers()
|
issuers := DefaultIssuers("")
|
||||||
for i, iss := range issuers {
|
for i, iss := range issuers {
|
||||||
if prov, ok := iss.(caddy.Provisioner); ok {
|
if prov, ok := iss.(caddy.Provisioner); ok {
|
||||||
err := prov.Provision(ctx)
|
err := prov.Provision(ctx)
|
||||||
|
@ -453,6 +481,7 @@ type TLSALPNChallengeConfig struct {
|
||||||
type DNSChallengeConfig struct {
|
type DNSChallengeConfig struct {
|
||||||
// The DNS provider module to use which will manage
|
// The DNS provider module to use which will manage
|
||||||
// the DNS records relevant to the ACME challenge.
|
// the DNS records relevant to the ACME challenge.
|
||||||
|
// Required.
|
||||||
ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
|
ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
|
||||||
|
|
||||||
// The TTL of the TXT record used for the DNS challenge.
|
// The TTL of the TXT record used for the DNS challenge.
|
||||||
|
|
|
@ -579,8 +579,7 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
|
||||||
customTransport.TLSClientConfig = tlsConfig
|
customTransport.TLSClientConfig = tlsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClient *http.Client
|
httpClient := *http.DefaultClient
|
||||||
*httpClient = *http.DefaultClient
|
|
||||||
httpClient.Transport = customTransport
|
httpClient.Transport = customTransport
|
||||||
|
|
||||||
for _, uri := range hcp.Endpoints {
|
for _, uri := range hcp.Endpoints {
|
||||||
|
|
|
@ -96,6 +96,11 @@ type HTTPCertGetter struct {
|
||||||
// To be valid, the response must be HTTP 200 with a PEM body
|
// To be valid, the response must be HTTP 200 with a PEM body
|
||||||
// consisting of blocks for the certificate chain and the private
|
// consisting of blocks for the certificate chain and the private
|
||||||
// key.
|
// key.
|
||||||
|
//
|
||||||
|
// To indicate that this manager is not managing a certificate for
|
||||||
|
// the described handshake, the endpoint should return HTTP 204
|
||||||
|
// (No Content). Error statuses will indicate that the manager is
|
||||||
|
// capable of providing a certificate but was unable to.
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
@ -147,6 +152,10 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == http.StatusNoContent {
|
||||||
|
// endpoint is not managing certs for this handshake
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("got HTTP %d", resp.StatusCode)
|
return nil, fmt.Errorf("got HTTP %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mholt/acmez"
|
"github.com/mholt/acmez/v2"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
|
|
@ -176,8 +176,9 @@ func (t *TLS) Provision(ctx caddy.Context) error {
|
||||||
t.Automation.OnDemand.permission = val.(OnDemandPermission)
|
t.Automation.OnDemand.permission = val.(OnDemandPermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
// on-demand rate limiting
|
// on-demand rate limiting (TODO: deprecated, and should be removed later; rate limiting is ineffective now that permission modules are required)
|
||||||
if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
|
if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
|
||||||
|
t.logger.Warn("DEPRECATED: on_demand.rate_limit will be removed in a future release; use permission modules or external certificate managers instead")
|
||||||
onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst)
|
onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst)
|
||||||
onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
|
onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
|
||||||
} else {
|
} else {
|
||||||
|
@ -413,35 +414,49 @@ func (t *TLS) Manage(names []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleHTTPChallenge ensures that the HTTP challenge is handled for the
|
// HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP
|
||||||
// certificate named by r.Host, if it is an HTTP challenge request. It
|
// validation request is handled for the certificate named by r.Host, if it
|
||||||
// requires that the automation policy for r.Host has an issuer of type
|
// is an HTTP challenge request. It requires that the automation policy for
|
||||||
// *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()).
|
// r.Host has an issuer that implements GetACMEIssuer() or is a *ZeroSSLIssuer.
|
||||||
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
|
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
acmeChallenge := certmagic.LooksLikeHTTPChallenge(r)
|
||||||
|
zerosslValidation := certmagic.LooksLikeZeroSSLHTTPValidation(r)
|
||||||
|
|
||||||
// no-op if it's not an ACME challenge request
|
// no-op if it's not an ACME challenge request
|
||||||
if !certmagic.LooksLikeHTTPChallenge(r) {
|
if !acmeChallenge && !zerosslValidation {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// try all the issuers until we find the one that initiated the challenge
|
// try all the issuers until we find the one that initiated the challenge
|
||||||
ap := t.getAutomationPolicyForName(r.Host)
|
ap := t.getAutomationPolicyForName(r.Host)
|
||||||
type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
|
|
||||||
for _, iss := range ap.magic.Issuers {
|
if acmeChallenge {
|
||||||
if am, ok := iss.(acmeCapable); ok {
|
type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
|
||||||
iss := am.GetACMEIssuer()
|
|
||||||
if iss.issuer.HandleHTTPChallenge(w, r) {
|
for _, iss := range ap.magic.Issuers {
|
||||||
return true
|
if acmeIssuer, ok := iss.(acmeCapable); ok {
|
||||||
|
if acmeIssuer.GetACMEIssuer().issuer.HandleHTTPChallenge(w, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// it's possible another server in this process initiated the challenge;
|
// it's possible another server in this process initiated the challenge;
|
||||||
// users have requested that Caddy only handle HTTP challenges it initiated,
|
// users have requested that Caddy only handle HTTP challenges it initiated,
|
||||||
// so that users can proxy the others through to their backends; but we
|
// so that users can proxy the others through to their backends; but we
|
||||||
// might not have an automation policy for all identifiers that are trying
|
// might not have an automation policy for all identifiers that are trying
|
||||||
// to get certificates (e.g. the admin endpoint), so we do this manual check
|
// to get certificates (e.g. the admin endpoint), so we do this manual check
|
||||||
if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok {
|
if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok {
|
||||||
return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge)
|
return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge)
|
||||||
|
}
|
||||||
|
} else if zerosslValidation {
|
||||||
|
for _, iss := range ap.magic.Issuers {
|
||||||
|
if ziss, ok := iss.(*ZeroSSLIssuer); ok {
|
||||||
|
if ziss.issuer.HandleZeroSSLHTTPValidation(w, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -17,19 +17,15 @@ package caddytls
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"strconv"
|
||||||
"net/http"
|
"time"
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,24 +33,36 @@ func init() {
|
||||||
caddy.RegisterModule(new(ZeroSSLIssuer))
|
caddy.RegisterModule(new(ZeroSSLIssuer))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZeroSSLIssuer makes an ACME issuer for getting certificates
|
// ZeroSSLIssuer uses the ZeroSSL API to get certificates.
|
||||||
// from ZeroSSL by automatically generating EAB credentials.
|
// Note that this is distinct from ZeroSSL's ACME endpoint.
|
||||||
// Please be sure to set a valid email address in your config
|
// To use ZeroSSL's ACME endpoint, use the ACMEIssuer
|
||||||
// so you can access/manage your domains in your ZeroSSL account.
|
// configured with ZeroSSL's ACME directory endpoint.
|
||||||
//
|
|
||||||
// This issuer is only needed for automatic generation of EAB
|
|
||||||
// credentials. If manually configuring/reusing EAB credentials,
|
|
||||||
// the standard ACMEIssuer may be used if desired.
|
|
||||||
type ZeroSSLIssuer struct {
|
type ZeroSSLIssuer struct {
|
||||||
*ACMEIssuer
|
|
||||||
|
|
||||||
// The API key (or "access key") for using the ZeroSSL API.
|
// The API key (or "access key") for using the ZeroSSL API.
|
||||||
// This is optional, but can be used if you have an API key
|
// REQUIRED.
|
||||||
// already and don't want to supply your email address.
|
|
||||||
APIKey string `json:"api_key,omitempty"`
|
APIKey string `json:"api_key,omitempty"`
|
||||||
|
|
||||||
mu sync.Mutex
|
// How many days the certificate should be valid for.
|
||||||
logger *zap.Logger
|
// Only certain values are accepted; see ZeroSSL docs.
|
||||||
|
ValidityDays int `json:"validity_days,omitempty"`
|
||||||
|
|
||||||
|
// The host to bind to when opening a listener for
|
||||||
|
// verifying domain names (or IPs).
|
||||||
|
ListenHost string `json:"listen_host,omitempty"`
|
||||||
|
|
||||||
|
// If HTTP is forwarded from port 80, specify the
|
||||||
|
// forwarded port here.
|
||||||
|
AlternateHTTPPort int `json:"alternate_http_port,omitempty"`
|
||||||
|
|
||||||
|
// Use CNAME validation instead of HTTP. ZeroSSL's
|
||||||
|
// API uses CNAME records for DNS validation, similar
|
||||||
|
// to how Let's Encrypt uses TXT records for the
|
||||||
|
// DNS challenge.
|
||||||
|
CNAMEValidation *DNSChallengeConfig `json:"cname_validation,omitempty"`
|
||||||
|
|
||||||
|
logger *zap.Logger
|
||||||
|
storage certmagic.Storage
|
||||||
|
issuer *certmagic.ZeroSSLIssuer
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
// CaddyModule returns the Caddy module information.
|
||||||
|
@ -65,178 +73,184 @@ func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets up iss.
|
// Provision sets up the issuer.
|
||||||
func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error {
|
func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error {
|
||||||
iss.logger = ctx.Logger()
|
iss.logger = ctx.Logger()
|
||||||
if iss.ACMEIssuer == nil {
|
iss.storage = ctx.Storage()
|
||||||
iss.ACMEIssuer = new(ACMEIssuer)
|
repl := caddy.NewReplacer()
|
||||||
}
|
|
||||||
if iss.ACMEIssuer.CA == "" {
|
|
||||||
iss.ACMEIssuer.CA = certmagic.ZeroSSLProductionCA
|
|
||||||
}
|
|
||||||
return iss.ACMEIssuer.Provision(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAccountCallback generates EAB if not already provided. It also sets a valid default contact on the account if not set.
|
var dnsManager *certmagic.DNSManager
|
||||||
func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
|
if iss.CNAMEValidation != nil && len(iss.CNAMEValidation.ProviderRaw) > 0 {
|
||||||
if acmeIss.ExternalAccount != nil {
|
val, err := ctx.LoadModule(iss.CNAMEValidation, "ProviderRaw")
|
||||||
return acct, nil
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("loading DNS provider module: %v", err)
|
||||||
var err error
|
|
||||||
acmeIss.ExternalAccount, acct, err = iss.generateEABCredentials(ctx, acct)
|
|
||||||
return acct, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateEABCredentials generates EAB credentials using the API key if provided,
|
|
||||||
// otherwise using the primary contact email on the issuer. If an email is not set
|
|
||||||
// on the issuer, a default generic email is used.
|
|
||||||
func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) {
|
|
||||||
var endpoint string
|
|
||||||
var body io.Reader
|
|
||||||
|
|
||||||
// there are two ways to generate EAB credentials: authenticated with
|
|
||||||
// their API key, or unauthenticated with their email address
|
|
||||||
if iss.APIKey != "" {
|
|
||||||
apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "")
|
|
||||||
if apiKey == "" {
|
|
||||||
return nil, acct, fmt.Errorf("missing API key: '%v'", iss.APIKey)
|
|
||||||
}
|
}
|
||||||
qs := url.Values{"access_key": []string{apiKey}}
|
dnsManager = &certmagic.DNSManager{
|
||||||
endpoint = fmt.Sprintf("%s/eab-credentials?%s", zerosslAPIBase, qs.Encode())
|
DNSProvider: val.(certmagic.DNSProvider),
|
||||||
} else {
|
TTL: time.Duration(iss.CNAMEValidation.TTL),
|
||||||
email := iss.Email
|
PropagationDelay: time.Duration(iss.CNAMEValidation.PropagationDelay),
|
||||||
if email == "" {
|
PropagationTimeout: time.Duration(iss.CNAMEValidation.PropagationTimeout),
|
||||||
iss.logger.Warn("missing email address for ZeroSSL; it is strongly recommended to set one for next time")
|
Resolvers: iss.CNAMEValidation.Resolvers,
|
||||||
email = "caddy@zerossl.com" // special email address that preserves backwards-compat, but which black-holes dashboard features, oh well
|
OverrideDomain: iss.CNAMEValidation.OverrideDomain,
|
||||||
|
Logger: iss.logger.Named("cname"),
|
||||||
}
|
}
|
||||||
if len(acct.Contact) == 0 {
|
|
||||||
// we borrow the email from config or the default email, so ensure it's saved with the account
|
|
||||||
acct.Contact = []string{"mailto:" + email}
|
|
||||||
}
|
|
||||||
endpoint = zerosslAPIBase + "/eab-credentials-email"
|
|
||||||
form := url.Values{"email": []string{email}}
|
|
||||||
body = strings.NewReader(form.Encode())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
|
iss.issuer = &certmagic.ZeroSSLIssuer{
|
||||||
if err != nil {
|
APIKey: repl.ReplaceAll(iss.APIKey, ""),
|
||||||
return nil, acct, fmt.Errorf("forming request: %v", err)
|
ValidityDays: iss.ValidityDays,
|
||||||
}
|
ListenHost: iss.ListenHost,
|
||||||
if body != nil {
|
AltHTTPPort: iss.AlternateHTTPPort,
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
Storage: iss.storage,
|
||||||
}
|
CNAMEValidation: dnsManager,
|
||||||
req.Header.Set("User-Agent", certmagic.UserAgent)
|
Logger: iss.logger,
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, acct, fmt.Errorf("performing EAB credentials request: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Error struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
} `json:"error"`
|
|
||||||
EABKID string `json:"eab_kid"`
|
|
||||||
EABHMACKey string `json:"eab_hmac_key"`
|
|
||||||
}
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, acct, fmt.Errorf("decoding API response: %v", err)
|
|
||||||
}
|
|
||||||
if result.Error.Code != 0 {
|
|
||||||
return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)",
|
|
||||||
resp.StatusCode, result.Error.Type, result.Error.Code)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID))
|
return nil
|
||||||
|
|
||||||
return &acme.EAB{
|
|
||||||
KeyID: result.EABKID,
|
|
||||||
MACKey: result.EABHMACKey,
|
|
||||||
}, acct, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize modifies the template for the underlying ACMEIssuer
|
|
||||||
// values by setting the CA endpoint to the ZeroSSL directory and
|
|
||||||
// setting the NewAccountFunc callback to one which allows us to
|
|
||||||
// generate EAB credentials only if a new account is being made.
|
|
||||||
// Since it modifies the stored template, its effect should only
|
|
||||||
// be needed once, but it is fine to call it repeatedly.
|
|
||||||
func (iss *ZeroSSLIssuer) initialize() {
|
|
||||||
iss.mu.Lock()
|
|
||||||
defer iss.mu.Unlock()
|
|
||||||
if iss.ACMEIssuer.issuer.NewAccountFunc == nil {
|
|
||||||
iss.ACMEIssuer.issuer.NewAccountFunc = iss.newAccountCallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreCheck implements the certmagic.PreChecker interface.
|
|
||||||
func (iss *ZeroSSLIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
|
|
||||||
iss.initialize()
|
|
||||||
return iss.ACMEIssuer.PreCheck(ctx, names, interactive)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue obtains a certificate for the given csr.
|
// Issue obtains a certificate for the given csr.
|
||||||
func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
|
func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
|
||||||
iss.initialize()
|
return iss.issuer.Issue(ctx, csr)
|
||||||
return iss.ACMEIssuer.Issue(ctx, csr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssuerKey returns the unique issuer key for the configured CA endpoint.
|
// IssuerKey returns the unique issuer key for the configured CA endpoint.
|
||||||
func (iss *ZeroSSLIssuer) IssuerKey() string {
|
func (iss *ZeroSSLIssuer) IssuerKey() string {
|
||||||
iss.initialize()
|
return iss.issuer.IssuerKey()
|
||||||
return iss.ACMEIssuer.IssuerKey()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke revokes the given certificate.
|
// Revoke revokes the given certificate.
|
||||||
func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error {
|
func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error {
|
||||||
iss.initialize()
|
return iss.issuer.Revoke(ctx, cert, reason)
|
||||||
return iss.ACMEIssuer.Revoke(ctx, cert, reason)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
|
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
|
||||||
//
|
//
|
||||||
// ... zerossl [<api_key>] {
|
// ... zerossl <api_key> {
|
||||||
// ...
|
// validity_days <days>
|
||||||
|
// alt_http_port <port>
|
||||||
|
// dns <provider_name> ...
|
||||||
|
// propagation_delay <duration>
|
||||||
|
// propagation_timeout <duration>
|
||||||
|
// resolvers <list...>
|
||||||
|
// dns_ttl <duration>
|
||||||
// }
|
// }
|
||||||
//
|
|
||||||
// Any of the subdirectives for the ACME issuer can be used in the block.
|
|
||||||
func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
d.Next() // consume issuer name
|
d.Next() // consume issuer name
|
||||||
|
|
||||||
|
// API key is required
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
iss.APIKey = d.Val()
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
iss.APIKey = d.Val()
|
return d.ArgErr()
|
||||||
if d.NextArg() {
|
}
|
||||||
return d.ArgErr()
|
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "validity_days":
|
||||||
|
if iss.ValidityDays != 0 {
|
||||||
|
return d.Errf("validity days is already specified: %d", iss.ValidityDays)
|
||||||
|
}
|
||||||
|
days, err := strconv.Atoi(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return d.Errf("invalid number of days %s: %v", d.Val(), err)
|
||||||
|
}
|
||||||
|
iss.ValidityDays = days
|
||||||
|
|
||||||
|
case "alt_http_port":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return d.Errf("invalid port %s: %v", d.Val(), err)
|
||||||
|
}
|
||||||
|
iss.AlternateHTTPPort = port
|
||||||
|
|
||||||
|
case "dns":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
provName := d.Val()
|
||||||
|
if iss.CNAMEValidation == nil {
|
||||||
|
iss.CNAMEValidation = new(DNSChallengeConfig)
|
||||||
|
}
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iss.CNAMEValidation.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil)
|
||||||
|
|
||||||
|
case "propagation_delay":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
delayStr := d.Val()
|
||||||
|
delay, err := caddy.ParseDuration(delayStr)
|
||||||
|
if err != nil {
|
||||||
|
return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err)
|
||||||
|
}
|
||||||
|
if iss.CNAMEValidation == nil {
|
||||||
|
iss.CNAMEValidation = new(DNSChallengeConfig)
|
||||||
|
}
|
||||||
|
iss.CNAMEValidation.PropagationDelay = caddy.Duration(delay)
|
||||||
|
|
||||||
|
case "propagation_timeout":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
timeoutStr := d.Val()
|
||||||
|
var timeout time.Duration
|
||||||
|
if timeoutStr == "-1" {
|
||||||
|
timeout = time.Duration(-1)
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
timeout, err = caddy.ParseDuration(timeoutStr)
|
||||||
|
if err != nil {
|
||||||
|
return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if iss.CNAMEValidation == nil {
|
||||||
|
iss.CNAMEValidation = new(DNSChallengeConfig)
|
||||||
|
}
|
||||||
|
iss.CNAMEValidation.PropagationTimeout = caddy.Duration(timeout)
|
||||||
|
|
||||||
|
case "resolvers":
|
||||||
|
if iss.CNAMEValidation == nil {
|
||||||
|
iss.CNAMEValidation = new(DNSChallengeConfig)
|
||||||
|
}
|
||||||
|
iss.CNAMEValidation.Resolvers = d.RemainingArgs()
|
||||||
|
if len(iss.CNAMEValidation.Resolvers) == 0 {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "dns_ttl":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
ttlStr := d.Val()
|
||||||
|
ttl, err := caddy.ParseDuration(ttlStr)
|
||||||
|
if err != nil {
|
||||||
|
return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err)
|
||||||
|
}
|
||||||
|
if iss.CNAMEValidation == nil {
|
||||||
|
iss.CNAMEValidation = new(DNSChallengeConfig)
|
||||||
|
}
|
||||||
|
iss.CNAMEValidation.TTL = caddy.Duration(ttl)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return d.Errf("unrecognized zerossl issuer property: %s", d.Val())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if iss.ACMEIssuer == nil {
|
|
||||||
iss.ACMEIssuer = new(ACMEIssuer)
|
|
||||||
}
|
|
||||||
err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const zerosslAPIBase = "https://api.zerossl.com/acme"
|
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ certmagic.PreChecker = (*ZeroSSLIssuer)(nil)
|
_ certmagic.Issuer = (*ZeroSSLIssuer)(nil)
|
||||||
_ certmagic.Issuer = (*ZeroSSLIssuer)(nil)
|
_ certmagic.Revoker = (*ZeroSSLIssuer)(nil)
|
||||||
_ certmagic.Revoker = (*ZeroSSLIssuer)(nil)
|
_ caddy.Provisioner = (*ZeroSSLIssuer)(nil)
|
||||||
_ caddy.Provisioner = (*ZeroSSLIssuer)(nil)
|
|
||||||
_ ConfigSetter = (*ZeroSSLIssuer)(nil)
|
|
||||||
|
|
||||||
// a type which properly embeds an ACMEIssuer should implement
|
|
||||||
// this interface so it can be treated as an ACMEIssuer
|
|
||||||
_ interface{ GetACMEIssuer() *ACMEIssuer } = (*ZeroSSLIssuer)(nil)
|
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue