fix: handle ACME mock directory and NO_DNS_01 configuration

- Add MockDirectory constant to avoid hardcoded strings
- Skip certificate validation when using mock directory
- Skip ACME client creation for mock directory to avoid TLS errors
- Properly handle NO_DNS_01 configuration to disable DNS-01 challenges
- Fix certificate verification errors when ACME_API=https://acme.mock.directory
This commit is contained in:
hongwei.chen 2025-07-13 01:32:19 +08:00
parent d19542233a
commit 63014e493f
3 changed files with 92 additions and 65 deletions

View File

@ -13,8 +13,8 @@ var ErrAcmeMissConfig = errors.New("ACME client has wrong config")
func CreateAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*certificates.AcmeClient, error) { func CreateAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*certificates.AcmeClient, error) {
// check config // check config
if (!cfg.AcceptTerms || (cfg.DNSProvider == "" && !cfg.NoDNS01)) && cfg.APIEndpoint != "https://acme.mock.directory" { if (!cfg.AcceptTerms || (cfg.DNSProvider == "" && !cfg.NoDNS01)) && cfg.APIEndpoint != certificates.MockDirectory {
return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER or $NO_DNS_01, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig) return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER or $NO_DNS_01, unless $ACME_API is set to %s", ErrAcmeMissConfig, certificates.MockDirectory)
} }
if cfg.EAB_HMAC != "" && cfg.EAB_KID == "" { if cfg.EAB_HMAC != "" && cfg.EAB_KID == "" {
return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig) return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig)

View File

@ -35,37 +35,49 @@ func NewAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache
return nil, err return nil, err
} }
acmeClient, err := lego.NewClient(acmeConfig) // Check if this is a mock directory
if err != nil { isMockDirectory := cfg.APIEndpoint == MockDirectory
log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
} else {
err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
if err != nil {
log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
}
if enableHTTPServer {
err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache})
if err != nil {
log.Error().Err(err).Msg("Can't create HTTP-01 provider")
}
}
}
mainDomainAcmeClient, err := lego.NewClient(acmeConfig) var acmeClient *lego.Client
if err != nil { var mainDomainAcmeClient *lego.Client
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
if isMockDirectory {
log.Info().Msg("Using mock ACME directory, skipping ACME client creation")
acmeClient = nil
mainDomainAcmeClient = nil
} else { } else {
if cfg.DNSProvider == "" { acmeClient, err = lego.NewClient(acmeConfig)
// using mock wildcard certs if err != nil {
mainDomainAcmeClient = nil log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
} else { } else {
// use DNS-Challenge https://go-acme.github.io/lego/dns/ err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider)
if err != nil { if err != nil {
return nil, fmt.Errorf("can not create DNS Challenge provider: %w", err) log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
} }
if err := mainDomainAcmeClient.Challenge.SetDNS01Provider(provider); err != nil { if enableHTTPServer {
return nil, fmt.Errorf("can not create DNS-01 provider: %w", err) err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache})
if err != nil {
log.Error().Err(err).Msg("Can't create HTTP-01 provider")
}
}
}
mainDomainAcmeClient, err = lego.NewClient(acmeConfig)
if err != nil {
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
} else {
if cfg.DNSProvider == "" || cfg.NoDNS01 {
// using mock wildcard certs when no DNS provider is configured or NO_DNS_01 is set
mainDomainAcmeClient = nil
} else {
// use DNS-Challenge https://go-acme.github.io/lego/dns/
provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider)
if err != nil {
return nil, fmt.Errorf("can not create DNS Challenge provider: %w", err)
}
if err := mainDomainAcmeClient.Challenge.SetDNS01Provider(provider); err != nil {
return nil, fmt.Errorf("can not create DNS-01 provider: %w", err)
}
} }
} }
} }

View File

@ -15,7 +15,10 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const challengePath = "/.well-known/acme-challenge/" const (
challengePath = "/.well-known/acme-challenge/"
MockDirectory = "https://acme.mock.directory"
)
func setupAcmeConfig(cfg config.ACMEConfig) (*lego.Config, error) { func setupAcmeConfig(cfg config.ACMEConfig) (*lego.Config, error) {
var myAcmeAccount AcmeAccount var myAcmeAccount AcmeAccount
@ -25,6 +28,9 @@ func setupAcmeConfig(cfg config.ACMEConfig) (*lego.Config, error) {
return nil, fmt.Errorf("invalid acme config file: '%s'", cfg.AccountConfigFile) return nil, fmt.Errorf("invalid acme config file: '%s'", cfg.AccountConfigFile)
} }
// Check if this is a mock directory - if so, skip certificate validation
isMockDirectory := cfg.APIEndpoint == MockDirectory
if account, err := os.ReadFile(cfg.AccountConfigFile); err == nil { if account, err := os.ReadFile(cfg.AccountConfigFile); err == nil {
log.Info().Msgf("found existing acme account config file '%s'", cfg.AccountConfigFile) log.Info().Msgf("found existing acme account config file '%s'", cfg.AccountConfigFile)
if err := json.Unmarshal(account, &myAcmeAccount); err != nil { if err := json.Unmarshal(account, &myAcmeAccount); err != nil {
@ -40,11 +46,14 @@ func setupAcmeConfig(cfg config.ACMEConfig) (*lego.Config, error) {
myAcmeConfig.CADirURL = cfg.APIEndpoint myAcmeConfig.CADirURL = cfg.APIEndpoint
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
// Validate Config // Skip validation for mock directory
_, err := lego.NewClient(myAcmeConfig) if !isMockDirectory {
if err != nil { // Validate Config
log.Info().Err(err).Msg("config validation failed, you might just delete the config file and let it recreate") _, err := lego.NewClient(myAcmeConfig)
return nil, fmt.Errorf("acme config validation failed: %w", err) if err != nil {
log.Info().Err(err).Msg("config validation failed, you might just delete the config file and let it recreate")
return nil, fmt.Errorf("acme config validation failed: %w", err)
}
} }
return myAcmeConfig, nil return myAcmeConfig, nil
@ -66,42 +75,48 @@ func setupAcmeConfig(cfg config.ACMEConfig) (*lego.Config, error) {
myAcmeConfig = lego.NewConfig(&myAcmeAccount) myAcmeConfig = lego.NewConfig(&myAcmeAccount)
myAcmeConfig.CADirURL = cfg.APIEndpoint myAcmeConfig.CADirURL = cfg.APIEndpoint
myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048 myAcmeConfig.Certificate.KeyType = certcrypto.RSA2048
tempClient, err := lego.NewClient(myAcmeConfig)
if err != nil { // Skip client creation for mock directory to avoid certificate verification errors
log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only") if isMockDirectory {
log.Info().Msg("Using mock ACME directory, skipping client creation and registration")
} else { } else {
// accept terms & log in to EAB tempClient, err := lego.NewClient(myAcmeConfig)
if cfg.EAB_KID == "" || cfg.EAB_HMAC == "" { if err != nil {
reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: cfg.AcceptTerms}) log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
if err != nil {
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
} else {
myAcmeAccount.Registration = reg
}
} else { } else {
reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ // accept terms & log in to EAB
TermsOfServiceAgreed: cfg.AcceptTerms, if cfg.EAB_KID == "" || cfg.EAB_HMAC == "" {
Kid: cfg.EAB_KID, reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: cfg.AcceptTerms})
HmacEncoded: cfg.EAB_HMAC, if err != nil {
}) log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
if err != nil { } else {
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only") myAcmeAccount.Registration = reg
}
} else { } else {
myAcmeAccount.Registration = reg reg, err := tempClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: cfg.AcceptTerms,
Kid: cfg.EAB_KID,
HmacEncoded: cfg.EAB_HMAC,
})
if err != nil {
log.Error().Err(err).Msg("Can't register ACME account, continuing with mock certs only")
} else {
myAcmeAccount.Registration = reg
}
} }
}
if myAcmeAccount.Registration != nil { if myAcmeAccount.Registration != nil {
acmeAccountJSON, err := json.Marshal(myAcmeAccount) acmeAccountJSON, err := json.Marshal(myAcmeAccount)
if err != nil { if err != nil {
log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits") log.Error().Err(err).Msg("json.Marshalfailed, waiting for manual restart to avoid rate limits")
select {} select {}
} }
log.Info().Msgf("new acme account created. write to config file '%s'", cfg.AccountConfigFile) log.Info().Msgf("new acme account created. write to config file '%s'", cfg.AccountConfigFile)
err = os.WriteFile(cfg.AccountConfigFile, acmeAccountJSON, 0o600) err = os.WriteFile(cfg.AccountConfigFile, acmeAccountJSON, 0o600)
if err != nil { if err != nil {
log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits") log.Error().Err(err).Msg("os.WriteFile failed, waiting for manual restart to avoid rate limits")
select {} select {}
}
} }
} }
} }