package acme import ( "context" "crypto/tls" "errors" "fmt" "io/ioutil" fmtlog "log" "net" "net/http" "net/url" "reflect" "strings" "sync" "time" "github.com/BurntSushi/ty/fun" "github.com/cenkalti/backoff" "github.com/containous/mux" "github.com/containous/staert" "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" acmeprovider "github.com/containous/traefik/provider/acme" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/containous/traefik/version" "github.com/eapache/channels" "github.com/xenolf/lego/certificate" "github.com/xenolf/lego/challenge" "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/challenge/http01" "github.com/xenolf/lego/lego" legolog "github.com/xenolf/lego/log" "github.com/xenolf/lego/providers/dns" "github.com/xenolf/lego/registration" ) var ( // OSCPMustStaple enables OSCP stapling as from https://github.com/xenolf/lego/issues/270 OSCPMustStaple = false ) // ACME allows to connect to lets encrypt and retrieve certs // Deprecated Please use provider/acme/Provider type ACME struct { Email string `description:"Email address used for registration"` Domains []types.Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` Storage string `description:"File or key used for certificates storage."` OnDemand bool `description:"(Deprecated) Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` // Deprecated OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` CAServer string `description:"CA server to use."` EntryPoint string `description:"Entrypoint to proxy acme challenge to."` KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. Default to 'RSA4096'"` DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"` HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"` TLSChallenge *acmeprovider.TLSChallenge `description:"Activate TLS-ALPN-01 Challenge"` ACMELogging bool `description:"Enable debug logging of ACME actions."` OverrideCertificates bool `description:"Enable to override certificates in key-value store when using storeconfig"` client *lego.Client store cluster.Store challengeHTTPProvider *challengeHTTPProvider challengeTLSProvider *challengeTLSProvider checkOnDemandDomain func(domain string) bool jobs *channels.InfiniteChannel TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` dynamicCerts *safe.Safe resolvingDomains map[string]struct{} resolvingDomainsMutex sync.RWMutex } func (a *ACME) init() error { if a.ACMELogging { legolog.Logger = log.WithoutContext() } else { legolog.Logger = fmtlog.New(ioutil.Discard, "", 0) } a.jobs = channels.NewInfiniteChannel() // Init the currently resolved domain map a.resolvingDomains = make(map[string]struct{}) return nil } // AddRoutes add routes on internal router func (a *ACME) AddRoutes(router *mux.Router) { router.Methods(http.MethodGet). Path(http01.ChallengePath("{token}")). Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if a.challengeHTTPProvider == nil { rw.WriteHeader(http.StatusNotFound) return } vars := mux.Vars(req) if token, ok := vars["token"]; ok { domain, _, err := net.SplitHostPort(req.Host) if err != nil { log.Debugf("Unable to split host and port: %v. Fallback to request host.", err) domain = req.Host } tokenValue := a.challengeHTTPProvider.getTokenValue(token, domain) if len(tokenValue) > 0 { _, err := rw.Write(tokenValue) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) } return } } rw.WriteHeader(http.StatusNotFound) })) } // CreateClusterConfig creates a tls.config using ACME configuration in cluster mode func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, certs *safe.Safe, checkOnDemandDomain func(domain string) bool) error { err := a.init() if err != nil { return err } if len(a.Storage) == 0 { return errors.New("empty Store, please provide a key for certs storage") } a.checkOnDemandDomain = checkOnDemandDomain a.dynamicCerts = certs tlsConfig.GetCertificate = a.getCertificate a.TLSConfig = tlsConfig listener := func(object cluster.Object) error { account := object.(*Account) err := account.Init() if err != nil { return err } if !leadership.IsLeader() { a.client, err = a.buildACMEClient(account) if err != nil { log.Errorf("Error building ACME client %+v: %s", object, err.Error()) } } return nil } datastore, err := cluster.NewDataStore( leadership.Pool.Ctx(), staert.KvSource{ Store: leadership.Store, Prefix: a.Storage, }, &Account{}, listener) if err != nil { return err } a.store = datastore a.challengeTLSProvider = &challengeTLSProvider{store: a.store} ticker := time.NewTicker(24 * time.Hour) leadership.Pool.AddGoCtx(func(ctx context.Context) { log.Info("Starting ACME renew job...") defer log.Info("Stopped ACME renew job...") for { select { case <-ctx.Done(): return case <-ticker.C: a.renewCertificates() } } }) leadership.AddListener(a.leadershipListener) return nil } func (a *ACME) leadershipListener(elected bool) error { if elected { _, err := a.store.Load() if err != nil { return err } transaction, object, err := a.store.Begin() if err != nil { return err } account := object.(*Account) err = account.Init() if err != nil { return err } // Reset Account values if caServer changed, thus registration URI can be updated if account != nil && account.Registration != nil && !isAccountMatchingCaServer(account.Registration.URI, a.CAServer) { log.Info("Account URI does not match the current CAServer. The account will be reset") account.reset() } var needRegister bool if account == nil || len(account.Email) == 0 { domainsCerts := DomainsCertificates{Certs: []*DomainsCertificate{}} if account != nil { domainsCerts = account.DomainsCertificate } account, err = NewAccount(a.Email, domainsCerts.Certs, a.KeyType) if err != nil { return err } needRegister = true } else if len(account.KeyType) == 0 { // Set the KeyType if not already defined in the account account.KeyType = acmeprovider.GetKeyType(context.Background(), a.KeyType) } a.client, err = a.buildACMEClient(account) if err != nil { return err } if needRegister { // New users will need to register; be sure to save it log.Debug("Register...") reg, err := a.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) if err != nil { return err } account.Registration = reg } err = transaction.Commit(account) if err != nil { return err } a.retrieveCertificates() a.renewCertificates() a.runJobs() } return nil } func isAccountMatchingCaServer(accountURI string, serverURI string) bool { aru, err := url.Parse(accountURI) if err != nil { log.Infof("Unable to parse account.Registration URL : %v", err) return false } cau, err := url.Parse(serverURI) if err != nil { log.Infof("Unable to parse CAServer URL : %v", err) return false } return cau.Hostname() == aru.Hostname() } func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { domain := types.CanonicalDomain(clientHello.ServerName) account := a.store.Get().(*Account) if challengeCert, ok := a.challengeTLSProvider.getCertificate(domain); ok { log.Debugf("ACME got challenge %s", domain) return challengeCert, nil } if providedCertificate := a.getProvidedCertificate(domain); providedCertificate != nil { return providedCertificate, nil } if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok { log.Debugf("ACME got domain cert %s", domain) return domainCert.tlsCert, nil } if a.OnDemand { if a.checkOnDemandDomain != nil && !a.checkOnDemandDomain(domain) { return nil, nil } return a.loadCertificateOnDemand(clientHello) } log.Debugf("No certificate found or generated for %s", domain) return nil, nil } func (a *ACME) retrieveCertificates() { a.jobs.In() <- func() { log.Info("Retrieving ACME certificates...") a.deleteUnnecessaryDomains() for i := 0; i < len(a.Domains); i++ { domain := a.Domains[i] // check if cert isn't already loaded account := a.store.Get().(*Account) if _, exists := account.DomainsCertificate.exists(domain); !exists { var domains []string domains = append(domains, domain.Main) domains = append(domains, domain.SANs...) domains, err := a.getValidDomains(domains, true) if err != nil { log.Errorf("Error validating ACME certificate for domain %q: %s", domains, err) continue } certificateResource, err := a.getDomainsCertificates(domains) if err != nil { log.Errorf("Error getting ACME certificate for domain %q: %s", domains, err) continue } transaction, object, err := a.store.Begin() if err != nil { log.Errorf("Error creating ACME store transaction from domain %q: %s", domain, err) continue } account = object.(*Account) _, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain) if err != nil { log.Errorf("Error adding ACME certificate for domain %q: %s", domains, err) continue } if err = transaction.Commit(account); err != nil { log.Errorf("Error Saving ACME account %+v: %s", account, err) continue } } } log.Info("Retrieved ACME certificates") } } func (a *ACME) renewCertificates() { a.jobs.In() <- func() { log.Info("Testing certificate renew...") account := a.store.Get().(*Account) for _, certificateResource := range account.DomainsCertificate.Certs { if certificateResource.needRenew() { log.Infof("Renewing certificate from LE : %+v", certificateResource.Domains) renewedACMECert, err := a.renewACMECertificate(certificateResource) if err != nil { log.Errorf("Error renewing certificate from LE: %v", err) continue } operation := func() error { return a.storeRenewedCertificate(certificateResource, renewedACMECert) } notify := func(err error, time time.Duration) { log.Warnf("Renewed certificate storage error: %v, retrying in %s", err, time) } ebo := backoff.NewExponentialBackOff() ebo.MaxElapsedTime = 60 * time.Second err = backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify) if err != nil { log.Errorf("Datastore cannot sync: %v", err) continue } } } } } func (a *ACME) renewACMECertificate(certificateResource *DomainsCertificate) (*Certificate, error) { renewedCert, err := a.client.Certificate.Renew(certificate.Resource{ Domain: certificateResource.Certificate.Domain, CertURL: certificateResource.Certificate.CertURL, CertStableURL: certificateResource.Certificate.CertStableURL, PrivateKey: certificateResource.Certificate.PrivateKey, Certificate: certificateResource.Certificate.Certificate, }, true, OSCPMustStaple) if err != nil { return nil, err } log.Infof("Renewed certificate from LE: %+v", certificateResource.Domains) return &Certificate{ Domain: renewedCert.Domain, CertURL: renewedCert.CertURL, CertStableURL: renewedCert.CertStableURL, PrivateKey: renewedCert.PrivateKey, Certificate: renewedCert.Certificate, }, nil } func (a *ACME) storeRenewedCertificate(certificateResource *DomainsCertificate, renewedACMECert *Certificate) error { transaction, object, err := a.store.Begin() if err != nil { return fmt.Errorf("error during transaction initialization for renewing certificate: %v", err) } log.Infof("Renewing certificate in data store : %+v ", certificateResource.Domains) account := object.(*Account) err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains) if err != nil { return fmt.Errorf("error renewing certificate in datastore: %v ", err) } log.Infof("Commit certificate renewed in data store : %+v", certificateResource.Domains) if err = transaction.Commit(account); err != nil { return fmt.Errorf("error saving ACME account %+v: %v", account, err) } oldAccount := a.store.Get().(*Account) for _, oldCertificateResource := range oldAccount.DomainsCertificate.Certs { if oldCertificateResource.Domains.Main == certificateResource.Domains.Main && strings.Join(oldCertificateResource.Domains.SANs, ",") == strings.Join(certificateResource.Domains.SANs, ",") && certificateResource.Certificate != renewedACMECert { return fmt.Errorf("renewed certificate not stored: %+v", certificateResource.Domains) } } log.Infof("Certificate successfully renewed in data store: %+v", certificateResource.Domains) return nil } func (a *ACME) buildACMEClient(account *Account) (*lego.Client, error) { log.Debug("Building ACME client...") caServer := "https://acme-v02.api.letsencrypt.org/directory" if len(a.CAServer) > 0 { caServer = a.CAServer } config := lego.NewConfig(account) config.CADirURL = caServer config.Certificate.KeyType = account.KeyType config.UserAgent = fmt.Sprintf("containous-traefik/%s", version.Version) client, err := lego.NewClient(config) if err != nil { return nil, err } // DNS challenge if a.DNSChallenge != nil && len(a.DNSChallenge.Provider) > 0 { log.Debugf("Using DNS Challenge provider: %s", a.DNSChallenge.Provider) var provider challenge.Provider provider, err = dns.NewDNSChallengeProviderByName(a.DNSChallenge.Provider) if err != nil { return nil, err } err = client.Challenge.SetDNS01Provider(provider, dns01.CondOption(len(a.DNSChallenge.Resolvers) > 0, dns01.AddRecursiveNameservers(a.DNSChallenge.Resolvers)), dns01.CondOption(a.DNSChallenge.DisablePropagationCheck || a.DNSChallenge.DelayBeforeCheck > 0, dns01.AddPreCheck(func(_, _ string) (bool, error) { if a.DNSChallenge.DelayBeforeCheck > 0 { log.Debugf("Delaying %d rather than validating DNS propagation now.", a.DNSChallenge.DelayBeforeCheck) time.Sleep(time.Duration(a.DNSChallenge.DelayBeforeCheck)) } return true, nil })), ) return client, err } // HTTP challenge if a.HTTPChallenge != nil && len(a.HTTPChallenge.EntryPoint) > 0 { log.Debug("Using HTTP Challenge provider.") a.challengeHTTPProvider = &challengeHTTPProvider{store: a.store} err = client.Challenge.SetHTTP01Provider(a.challengeHTTPProvider) return client, err } // TLS Challenge if a.TLSChallenge != nil { log.Debug("Using TLS Challenge provider.") err = client.Challenge.SetTLSALPN01Provider(a.challengeTLSProvider) return client, err } return nil, errors.New("ACME challenge not specified, please select TLS or HTTP or DNS Challenge") } func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { domain := types.CanonicalDomain(clientHello.ServerName) account := a.store.Get().(*Account) if certificateResource, ok := account.DomainsCertificate.getCertificateForDomain(domain); ok { return certificateResource.tlsCert, nil } certificate, err := a.getDomainsCertificates([]string{domain}) if err != nil { return nil, err } log.Debugf("Got certificate on demand for domain %s", domain) transaction, object, err := a.store.Begin() if err != nil { return nil, err } account = object.(*Account) cert, err := account.DomainsCertificate.addCertificateForDomains(certificate, types.Domain{Main: domain}) if err != nil { return nil, err } if err = transaction.Commit(account); err != nil { return nil, err } return cert.tlsCert, nil } // LoadCertificateForDomains loads certificates from ACME for given domains func (a *ACME) LoadCertificateForDomains(domains []string) { a.jobs.In() <- func() { log.Debugf("LoadCertificateForDomains %v...", domains) domains, err := a.getValidDomains(domains, false) if err != nil { log.Errorf("Error getting valid domain: %v", err) return } operation := func() error { if a.client == nil { return errors.New("ACME client still not built") } return nil } notify := func(err error, time time.Duration) { log.Errorf("Error getting ACME client: %v, retrying in %s", err, time) } ebo := backoff.NewExponentialBackOff() ebo.MaxElapsedTime = 30 * time.Second err = backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify) if err != nil { log.Errorf("Error getting ACME client: %v", err) return } account := a.store.Get().(*Account) // Check provided certificates uncheckedDomains := a.getUncheckedDomains(domains, account) if len(uncheckedDomains) == 0 { return } a.addResolvingDomains(uncheckedDomains) defer a.removeResolvingDomains(uncheckedDomains) cert, err := a.getDomainsCertificates(uncheckedDomains) if err != nil { log.Errorf("Error getting ACME certificates %+v : %v", uncheckedDomains, err) return } log.Debugf("Got certificate for domains %+v", uncheckedDomains) transaction, object, err := a.store.Begin() if err != nil { log.Errorf("Error creating transaction %+v : %v", uncheckedDomains, err) return } var domain types.Domain if len(uncheckedDomains) > 1 { domain = types.Domain{Main: uncheckedDomains[0], SANs: uncheckedDomains[1:]} } else { domain = types.Domain{Main: uncheckedDomains[0]} } account = object.(*Account) _, err = account.DomainsCertificate.addCertificateForDomains(cert, domain) if err != nil { log.Errorf("Error adding ACME certificates %+v : %v", uncheckedDomains, err) return } if err = transaction.Commit(account); err != nil { log.Errorf("Error Saving ACME account %+v: %v", account, err) return } } } func (a *ACME) addResolvingDomains(resolvingDomains []string) { a.resolvingDomainsMutex.Lock() defer a.resolvingDomainsMutex.Unlock() for _, domain := range resolvingDomains { a.resolvingDomains[domain] = struct{}{} } } func (a *ACME) removeResolvingDomains(resolvingDomains []string) { a.resolvingDomainsMutex.Lock() defer a.resolvingDomainsMutex.Unlock() for _, domain := range resolvingDomains { delete(a.resolvingDomains, domain) } } // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate { log.Debugf("Looking for provided certificate to validate %s...", domains) cert := searchProvidedCertificateForDomains(domains, a.TLSConfig.NameToCertificate) if cert == nil && a.dynamicCerts != nil && a.dynamicCerts.Get() != nil { cert = searchProvidedCertificateForDomains(domains, a.dynamicCerts.Get().(map[string]*tls.Certificate)) } if cert == nil { log.Debugf("No provided certificate found for domains %s, get ACME certificate.", domains) } return cert } func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Certificate) *tls.Certificate { // Use regex to test for provided certs that might have been added into TLSConfig for certDomains := range certs { domainChecked := false for _, certDomain := range strings.Split(certDomains, ",") { domainChecked = types.MatchDomain(domain, certDomain) if domainChecked { break } } if domainChecked { log.Debugf("Domain %q checked by provided certificate %q", domain, certDomains) return certs[certDomains] } } return nil } // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string { a.resolvingDomainsMutex.RLock() defer a.resolvingDomainsMutex.RUnlock() log.Debugf("Looking for provided certificate to validate %s...", domains) allCerts := make(map[string]*tls.Certificate) // Get static certificates for domains, certificate := range a.TLSConfig.NameToCertificate { allCerts[domains] = certificate } // Get dynamic certificates if a.dynamicCerts != nil && a.dynamicCerts.Get() != nil { for domains, certificate := range a.dynamicCerts.Get().(map[string]*tls.Certificate) { allCerts[domains] = certificate } } // Get ACME certificates if account != nil { for domains, certificate := range account.DomainsCertificate.toDomainsMap() { allCerts[domains] = certificate } } // Get currently resolved domains for domain := range a.resolvingDomains { if _, ok := allCerts[domain]; !ok { allCerts[domain] = &tls.Certificate{} } } // Get Configuration Domains for i := 0; i < len(a.Domains); i++ { allCerts[a.Domains[i].Main] = &tls.Certificate{} for _, san := range a.Domains[i].SANs { allCerts[san] = &tls.Certificate{} } } return searchUncheckedDomains(domains, allCerts) } func searchUncheckedDomains(domains []string, certs map[string]*tls.Certificate) []string { var uncheckedDomains []string for _, domainToCheck := range domains { if !isDomainAlreadyChecked(domainToCheck, certs) { uncheckedDomains = append(uncheckedDomains, domainToCheck) } } if len(uncheckedDomains) == 0 { log.Debugf("No ACME certificate to generate for domains %q.", domains) } else { log.Debugf("Domains %q need ACME certificates generation for domains %q.", domains, strings.Join(uncheckedDomains, ",")) } return uncheckedDomains } func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) { var cleanDomains []string for _, domain := range domains { canonicalDomain := types.CanonicalDomain(domain) cleanDomain := dns01.UnFqdn(canonicalDomain) if canonicalDomain != cleanDomain { log.Warnf("FQDN detected, please remove the trailing dot: %s", canonicalDomain) } cleanDomains = append(cleanDomains, cleanDomain) } log.Debugf("Loading ACME certificates %s...", cleanDomains) bundle := true request := certificate.ObtainRequest{ Domains: cleanDomains, Bundle: bundle, MustStaple: OSCPMustStaple, } cert, err := a.client.Certificate.Obtain(request) if err != nil { return nil, fmt.Errorf("cannot obtain certificates: %+v", err) } log.Debugf("Loaded ACME certificates %s", cleanDomains) return &Certificate{ Domain: cert.Domain, CertURL: cert.CertURL, CertStableURL: cert.CertStableURL, PrivateKey: cert.PrivateKey, Certificate: cert.Certificate, }, nil } func (a *ACME) runJobs() { safe.Go(func() { for job := range a.jobs.Out() { function := job.(func()) function() } }) } // getValidDomains checks if given domain is allowed to generate a ACME certificate and return it func (a *ACME) getValidDomains(domains []string, wildcardAllowed bool) ([]string, error) { // Check if the domains array is empty or contains only one empty value if len(domains) == 0 || (len(domains) == 1 && len(domains[0]) == 0) { return nil, errors.New("unable to generate a certificate when no domain is given") } if strings.HasPrefix(domains[0], "*") { if !wildcardAllowed { return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q from a 'Host' rule", strings.Join(domains, ",")) } if a.DNSChallenge == nil { return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ",")) } if strings.HasPrefix(domains[0], "*.*") { return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ",")) } } for _, san := range domains[1:] { if strings.HasPrefix(san, "*") { return nil, fmt.Errorf("unable to generate a certificate for domains %q: SANs can not be a wildcard domain", strings.Join(domains, ",")) } } domains = fun.Map(types.CanonicalDomain, domains).([]string) return domains, nil } func isDomainAlreadyChecked(domainToCheck string, existentDomains map[string]*tls.Certificate) bool { for certDomains := range existentDomains { for _, certDomain := range strings.Split(certDomains, ",") { if types.MatchDomain(domainToCheck, certDomain) { return true } } } return false } // deleteUnnecessaryDomains deletes from the configuration : // - Duplicated domains // - Domains which are checked by wildcard domain func (a *ACME) deleteUnnecessaryDomains() { var newDomains []types.Domain for idxDomainToCheck, domainToCheck := range a.Domains { keepDomain := true for idxDomain, domain := range a.Domains { if idxDomainToCheck == idxDomain { continue } if reflect.DeepEqual(domain, domainToCheck) { if idxDomainToCheck > idxDomain { log.Warnf("The domain %v is duplicated in the configuration but will be process by ACME only once.", domainToCheck) keepDomain = false } break } var newDomainsToCheck []string // Check if domains can be validated by the wildcard domain domainsMap := make(map[string]*tls.Certificate) domainsMap[domain.Main] = &tls.Certificate{} if len(domain.SANs) > 0 { domainsMap[strings.Join(domain.SANs, ",")] = &tls.Certificate{} } for _, domainProcessed := range domainToCheck.ToStrArray() { if idxDomain < idxDomainToCheck && isDomainAlreadyChecked(domainProcessed, domainsMap) { // The domain is duplicated in a CN log.Warnf("Domain %q is duplicated in the configuration or validated by the domain %v. It will be processed once.", domainProcessed, domain) continue } else if domain.Main != domainProcessed && strings.HasPrefix(domain.Main, "*") && types.MatchDomain(domainProcessed, domain.Main) { // Check if a wildcard can validate the domain log.Warnf("Domain %q will not be processed by ACME provider because it is validated by the wildcard %q", domainProcessed, domain.Main) continue } newDomainsToCheck = append(newDomainsToCheck, domainProcessed) } // Delete the domain if both Main and SANs can be validated by the wildcard domain // otherwise keep the unchecked values if newDomainsToCheck == nil { keepDomain = false break } domainToCheck.Set(newDomainsToCheck) } if keepDomain { newDomains = append(newDomains, domainToCheck) } } a.Domains = newDomains }