Skip to content

Commit 5a448ab

Browse files
committed
Ability to disable distributed solvers
This makes it possible to avoid storage writes, and still supports distributed solving of the http-01 challenge only (requires the correct ACME account to be used -- configuring it in AccountKeyPEM can be helpful).
1 parent 7084df0 commit 5a448ab

File tree

9 files changed

+193
-78
lines changed

9 files changed

+193
-78
lines changed

account.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,27 @@ import (
3636
"go.uber.org/zap"
3737
)
3838

39-
// getAccount either loads or creates a new account, depending on if
39+
// getAccountToUse will either load or create an account based on the configuration of the issuer.
40+
// It will try to get one from storage if one exists, and if not, it will create one, all the while
41+
// honoring the configured account key PEM (if any) to restrict which account is used.
42+
func (iss *ACMEIssuer) getAccountToUse(ctx context.Context, directory string) (acme.Account, error) {
43+
var account acme.Account
44+
var err error
45+
if iss.AccountKeyPEM != "" {
46+
iss.Logger.Info("using configured ACME account")
47+
account, err = iss.GetAccount(ctx, []byte(iss.AccountKeyPEM))
48+
} else {
49+
account, err = iss.loadOrCreateAccount(ctx, directory, iss.getEmail())
50+
}
51+
if err != nil {
52+
return acme.Account{}, fmt.Errorf("getting ACME account: %v", err)
53+
}
54+
return account, nil
55+
}
56+
57+
// loadOrCreateAccount either loads or creates a new account, depending on if
4058
// an account can be found in storage for the given CA + email combo.
41-
func (am *ACMEIssuer) getAccount(ctx context.Context, ca, email string) (acme.Account, error) {
59+
func (am *ACMEIssuer) loadOrCreateAccount(ctx context.Context, ca, email string) (acme.Account, error) {
4260
acct, err := am.loadAccount(ctx, ca, email)
4361
if errors.Is(err, fs.ErrNotExist) {
4462
am.Logger.Info("creating new account because no account for configured email is known to us",
@@ -407,7 +425,7 @@ func (am *ACMEIssuer) mostRecentAccountEmail(ctx context.Context, caURL string)
407425
return "", false
408426
}
409427

410-
account, err := am.getAccount(ctx, caURL, path.Base(accountList[0]))
428+
account, err := am.loadOrCreateAccount(ctx, caURL, path.Base(accountList[0]))
411429
if err != nil {
412430
return "", false
413431
}

account_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func TestSaveAccount(t *testing.T) {
210210
if err != nil {
211211
t.Fatalf("Error saving account: %v", err)
212212
}
213-
_, err = am.getAccount(ctx, am.CA, email)
213+
_, err = am.loadOrCreateAccount(ctx, am.CA, email)
214214
if err != nil {
215215
t.Errorf("Cannot access account data, error: %v", err)
216216
}
@@ -228,7 +228,7 @@ func TestGetAccountDoesNotAlreadyExist(t *testing.T) {
228228
}
229229
am.config = testConfig
230230

231-
account, err := am.getAccount(ctx, am.CA, "account_does_not_exist@foobar.com")
231+
account, err := am.loadOrCreateAccount(ctx, am.CA, "account_does_not_exist@foobar.com")
232232
if err != nil {
233233
t.Fatalf("Error getting account: %v", err)
234234
}
@@ -271,7 +271,7 @@ func TestGetAccountAlreadyExists(t *testing.T) {
271271
}
272272

273273
// Expect to load account from disk
274-
loadedAccount, err := am.getAccount(ctx, am.CA, email)
274+
loadedAccount, err := am.loadOrCreateAccount(ctx, am.CA, email)
275275
if err != nil {
276276
t.Fatalf("Error getting account: %v", err)
277277
}

acmeclient.go

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,7 @@ func (iss *ACMEIssuer) newACMEClientWithAccount(ctx context.Context, useTestCA,
5555
// we try loading the account from storage before a potential
5656
// lock, and after obtaining the lock as well, to ensure we don't
5757
// repeat work done by another instance or goroutine
58-
getAccount := func() (acme.Account, error) {
59-
// look up or create the ACME account
60-
var account acme.Account
61-
if iss.AccountKeyPEM != "" {
62-
iss.Logger.Info("using configured ACME account")
63-
account, err = iss.GetAccount(ctx, []byte(iss.AccountKeyPEM))
64-
} else {
65-
account, err = iss.getAccount(ctx, client.Directory, iss.getEmail())
66-
}
67-
if err != nil {
68-
return acme.Account{}, fmt.Errorf("getting ACME account: %v", err)
69-
}
70-
return account, nil
71-
}
72-
73-
// first try getting the account
74-
account, err := getAccount()
58+
account, err := iss.getAccountToUse(ctx, client.Directory)
7559
if err != nil {
7660
return nil, err
7761
}
@@ -95,7 +79,7 @@ func (iss *ACMEIssuer) newACMEClientWithAccount(ctx context.Context, useTestCA,
9579
}()
9680

9781
// if we're not the only one waiting for this account, then by this point it should already be registered and in storage; reload it
98-
account, err = getAccount()
82+
account, err = iss.getAccountToUse(ctx, client.Directory)
9983
if err != nil {
10084
return nil, err
10185
}
@@ -207,26 +191,34 @@ func (iss *ACMEIssuer) newACMEClient(useTestCA bool) (*acmez.Client, error) {
207191
if iss.DNS01Solver == nil {
208192
// enable HTTP-01 challenge
209193
if !iss.DisableHTTPChallenge {
210-
client.ChallengeSolvers[acme.ChallengeTypeHTTP01] = distributedSolver{
211-
storage: iss.config.Storage,
212-
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
213-
solver: &httpSolver{
214-
handler: iss.HTTPChallengeHandler(http.NewServeMux()),
215-
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getHTTPPort())),
216-
},
194+
var solver acmez.Solver = &httpSolver{
195+
handler: iss.HTTPChallengeHandler(http.NewServeMux()),
196+
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getHTTPPort())),
197+
}
198+
if !iss.DisableDistributedSolvers {
199+
solver = distributedSolver{
200+
storage: iss.config.Storage,
201+
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
202+
solver: solver,
203+
}
217204
}
205+
client.ChallengeSolvers[acme.ChallengeTypeHTTP01] = solver
218206
}
219207

220208
// enable TLS-ALPN-01 challenge
221209
if !iss.DisableTLSALPNChallenge {
222-
client.ChallengeSolvers[acme.ChallengeTypeTLSALPN01] = distributedSolver{
223-
storage: iss.config.Storage,
224-
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
225-
solver: &tlsALPNSolver{
226-
config: iss.config,
227-
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getTLSALPNPort())),
228-
},
210+
var solver acmez.Solver = &tlsALPNSolver{
211+
config: iss.config,
212+
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getTLSALPNPort())),
213+
}
214+
if !iss.DisableDistributedSolvers {
215+
solver = distributedSolver{
216+
storage: iss.config.Storage,
217+
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
218+
solver: solver,
219+
}
229220
}
221+
client.ChallengeSolvers[acme.ChallengeTypeTLSALPN01] = solver
230222
}
231223
} else {
232224
// use DNS challenge exclusively

acmeissuer.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ type ACMEIssuer struct {
9393
// Disable all TLS-ALPN challenges
9494
DisableTLSALPNChallenge bool
9595

96+
// Disable distributed solving; avoids writing
97+
// challenge info to storage backend and will
98+
// only use data in memory to solve the HTTP and
99+
// TLS-ALPN challenges; will still attempt to
100+
// solve distributed HTTP challenges blindly by
101+
// using available account and challenge token
102+
// as read from request URI
103+
DisableDistributedSolvers bool
104+
96105
// The host (ONLY the host, not port) to listen
97106
// on if necessary to start a listener to solve
98107
// an ACME challenge
@@ -340,7 +349,7 @@ func (iss *ACMEIssuer) isAgreed() bool {
340349
// IP certificates via ACME are defined in RFC 8738.
341350
func (am *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
342351
publicCAsAndIPCerts := map[string]bool{ // map of public CAs to whether they support IP certificates (last updated: Q1 2024)
343-
"api.letsencrypt.org": true, // https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate/
352+
"api.letsencrypt.org": true, // https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate/
344353
"acme.zerossl.com": false, // only supported via their API, not ACME endpoint
345354
"api.pki.goog": true, // https://pki.goog/faq/#faq-IPCerts
346355
"api.buypass.com": false, // https://community.buypass.com/t/h7hm76w/buypass-support-for-rfc-8738

config.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,20 +1158,29 @@ func (cfg *Config) TLSConfig() *tls.Config {
11581158
}
11591159
}
11601160

1161-
// getChallengeInfo loads the challenge info from either the internal challenge memory
1161+
// getACMEChallengeInfo loads the challenge info from either the internal challenge memory
11621162
// or the external storage (implying distributed solving). The second return value
11631163
// indicates whether challenge info was loaded from external storage. If true, the
11641164
// challenge is being solved in a distributed fashion; if false, from internal memory.
11651165
// If no matching challenge information can be found, an error is returned.
1166-
func (cfg *Config) getChallengeInfo(ctx context.Context, identifier string) (Challenge, bool, error) {
1166+
func (cfg *Config) getACMEChallengeInfo(ctx context.Context, identifier string, allowDistributed bool) (Challenge, bool, error) {
11671167
// first, check if our process initiated this challenge; if so, just return it
11681168
chalData, ok := GetACMEChallenge(identifier)
11691169
if ok {
11701170
return chalData, false, nil
11711171
}
11721172

1173+
// if distributed solving is disabled, and we don't have it in memory, return an error
1174+
if !allowDistributed {
1175+
return Challenge{}, false, fmt.Errorf("distributed solving disabled and no challenge information found internally for identifier: %s", identifier)
1176+
}
1177+
11731178
// otherwise, perhaps another instance in the cluster initiated it; check
1174-
// the configured storage to retrieve challenge data
1179+
// the configured storage to retrieve challenge data (requires storage)
1180+
1181+
if cfg.Storage == nil {
1182+
return Challenge{}, false, errors.New("challenge was not initiated internally and no storage is configured for distributed solving")
1183+
}
11751184

11761185
var chalInfo acme.Challenge
11771186
var chalInfoBytes []byte

go.mod

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,22 @@ toolchain go1.24.0
66

77
require (
88
github.com/caddyserver/zerossl v0.1.3
9-
github.com/klauspost/cpuid/v2 v2.2.10
10-
github.com/libdns/libdns v1.0.0
11-
github.com/mholt/acmez/v3 v3.1.2
12-
github.com/miekg/dns v1.1.63
9+
github.com/klauspost/cpuid/v2 v2.3.0
10+
github.com/libdns/libdns v1.1.1
11+
github.com/mholt/acmez/v3 v3.1.3
12+
github.com/miekg/dns v1.1.68
1313
github.com/zeebo/blake3 v0.2.4
1414
go.uber.org/zap v1.27.0
1515
go.uber.org/zap/exp v0.3.0
16-
golang.org/x/crypto v0.36.0
17-
golang.org/x/net v0.38.0
16+
golang.org/x/crypto v0.41.0
17+
golang.org/x/net v0.43.0
1818
)
1919

2020
require (
2121
go.uber.org/multierr v1.11.0 // indirect
22-
golang.org/x/mod v0.24.0 // indirect
23-
golang.org/x/sync v0.12.0 // indirect
24-
golang.org/x/sys v0.31.0 // indirect
25-
golang.org/x/text v0.23.0 // indirect
26-
golang.org/x/tools v0.31.0 // indirect
22+
golang.org/x/mod v0.26.0 // indirect
23+
golang.org/x/sync v0.16.0 // indirect
24+
golang.org/x/sys v0.35.0 // indirect
25+
golang.org/x/text v0.28.0 // indirect
26+
golang.org/x/tools v0.35.0 // indirect
2727
)

go.sum

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
66
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7-
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
8-
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
9-
github.com/libdns/libdns v1.0.0 h1:IvYaz07JNz6jUQ4h/fv2R4sVnRnm77J/aOuC9B+TQTA=
10-
github.com/libdns/libdns v1.0.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
11-
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
12-
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
13-
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
14-
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
7+
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
8+
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
9+
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
10+
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
11+
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
12+
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
13+
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
14+
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
1515
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1616
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1717
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
@@ -30,19 +30,19 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
3030
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
3131
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
3232
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
33-
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
34-
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
35-
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
36-
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
37-
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
38-
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
39-
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
40-
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
41-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
42-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
43-
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
44-
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
45-
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
46-
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
33+
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
34+
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
35+
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
36+
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
37+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
38+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
39+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
40+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
41+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
42+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
43+
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
44+
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
45+
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
46+
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
4747
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4848
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

handshake.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ func (cfg *Config) getCertFromAnyCertManager(ctx context.Context, hello *tls.Cli
853853
// solving). True is returned if the challenge is being solved distributed (there
854854
// is no semantic difference with distributed solving; it is mainly for logging).
855855
func (cfg *Config) getTLSALPNChallengeCert(clientHello *tls.ClientHelloInfo) (*tls.Certificate, bool, error) {
856-
chalData, distributed, err := cfg.getChallengeInfo(clientHello.Context(), clientHello.ServerName)
856+
chalData, distributed, err := cfg.getACMEChallengeInfo(clientHello.Context(), clientHello.ServerName, true)
857857
if err != nil {
858858
return nil, distributed, err
859859
}

0 commit comments

Comments
 (0)