diff --git a/acme/localStore.go b/acme/localStore.go index e3a3fdd4b..3cdef8a04 100644 --- a/acme/localStore.go +++ b/acme/localStore.go @@ -46,38 +46,42 @@ func (s *LocalStore) Get() (*Account, error) { if err := json.Unmarshal(file, &account); err != nil { return nil, err } - - // Check if ACME Account is in ACME V1 format - if account != nil && account.Registration != nil { - isOldRegistration, err := regexp.MatchString(acme.RegistrationURLPathV1Regexp, account.Registration.URI) - if err != nil { - return nil, err - } - - if isOldRegistration { - account.Email = "" - account.Registration = nil - account.PrivateKey = nil - } - } } return account, nil } +// RemoveAccountV1Values removes ACME account V1 values +func RemoveAccountV1Values(account *Account) error { + // Check if ACME Account is in ACME V1 format + if account != nil && account.Registration != nil { + isOldRegistration, err := regexp.MatchString(acme.RegistrationURLPathV1Regexp, account.Registration.URI) + if err != nil { + return err + } + + if isOldRegistration { + account.Email = "" + account.Registration = nil + account.PrivateKey = nil + } + } + return nil +} + // ConvertToNewFormat converts old acme.json format to the new one and store the result into the file (used for the backward compatibility) func ConvertToNewFormat(fileName string) { localStore := acme.NewLocalStore(fileName) storeAccount, err := localStore.GetAccount() if err != nil { - log.Warnf("Failed to read new account, ACME data conversion is not available : %v", err) + log.Errorf("Failed to read new account, ACME data conversion is not available : %v", err) return } storeCertificates, err := localStore.GetCertificates() if err != nil { - log.Warnf("Failed to read new certificates, ACME data conversion is not available : %v", err) + log.Errorf("Failed to read new certificates, ACME data conversion is not available : %v", err) return } @@ -86,13 +90,25 @@ func ConvertToNewFormat(fileName string) { account, err := localStore.Get() if err != nil { - log.Warnf("Failed to read old account, ACME data conversion is not available : %v", err) + log.Errorf("Failed to read old account, ACME data conversion is not available : %v", err) return } // Convert ACME data from old to new format newAccount := &acme.Account{} if account != nil && len(account.Email) > 0 { + err = backupACMEFile(fileName, account) + if err != nil { + log.Errorf("Unable to create a backup for the V1 formatted ACME file: %s", err.Error()) + return + } + + err = RemoveAccountV1Values(account) + if err != nil { + log.Errorf("Unable to remove ACME Account V1 values: %s", err.Error()) + return + } + newAccount = &acme.Account{ PrivateKey: account.PrivateKey, Registration: account.Registration, @@ -107,8 +123,8 @@ func ConvertToNewFormat(fileName string) { Domain: cert.Domains, }) } - // If account is in the old format, storeCertificates is nil or empty - // and has to be initialized + + // If account is in the old format, storeCertificates is nil or empty and has to be initialized storeCertificates = newCertificates } @@ -119,7 +135,16 @@ func ConvertToNewFormat(fileName string) { } } -// FromNewToOldFormat converts new acme.json format to the old one (used for the backward compatibility) +func backupACMEFile(originalFileName string, account interface{}) error { + // write account to file + data, err := json.MarshalIndent(account, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(originalFileName+".bak", data, 0600) +} + +// FromNewToOldFormat converts new acme account to the old one (used for the backward compatibility) func FromNewToOldFormat(fileName string) (*Account, error) { localStore := acme.NewLocalStore(fileName) diff --git a/cmd/storeconfig/storeconfig.go b/cmd/storeconfig/storeconfig.go index 4beb75ba5..83fc9048b 100644 --- a/cmd/storeconfig/storeconfig.go +++ b/cmd/storeconfig/storeconfig.go @@ -134,10 +134,16 @@ func migrateACMEData(fileName string) (*acme.Account, error) { if accountFromNewFormat == nil { // convert ACME json file to KV store (used for backward compatibility) localStore := acme.NewLocalStore(fileName) + account, err = localStore.Get() if err != nil { return nil, err } + + err = acme.RemoveAccountV1Values(account) + if err != nil { + return nil, err + } } else { account = accountFromNewFormat } diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index aadf4873a..95f69b77f 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -543,3 +543,14 @@ Do not hesitate to complete it. | [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | Not tested yet | | [Route 53](https://aws.amazon.com/route53/) | `route53` | YES | | [VULTR](https://www.vultr.com) | `vultr` | Not tested yet | + +## ACME V2 migration + +During migration from ACME V1 to ACME V2 with a storage file, a backup is created with the content of the ACME V1 file. +To obtain the name of the backup file, Træfik concatenates the option `acme.storage` and the suffix `.bak`. + +For example : if `acme.storage` value is `/etc/traefik/acme/acme.json`, the backup file will be named `/etc/traefik/acme/acme.json.bak`. + +!!! note + When Træfik is launched in a container, do not forget to create a volume of the parent folder to get the backup file on the host. + Otherwise, the backup file will be deleted when the container will be stopped and Træfik will not generate it again. \ No newline at end of file diff --git a/provider/acme/local_store.go b/provider/acme/local_store.go index 4d638d001..bee69a3e8 100644 --- a/provider/acme/local_store.go +++ b/provider/acme/local_store.go @@ -52,6 +52,7 @@ func (s *LocalStore) get() (*StoredData, error) { return nil, err } } + // Check if ACME Account is in ACME V1 format if s.storedData.Account != nil && s.storedData.Account.Registration != nil { isOldRegistration, err := regexp.MatchString(RegistrationURLPathV1Regexp, s.storedData.Account.Registration.URI) @@ -63,6 +64,21 @@ func (s *LocalStore) get() (*StoredData, error) { s.SaveDataChan <- s.storedData } } + + // Delete all certificates with no value + var certificates []*Certificate + for _, certificate := range s.storedData.Certificates { + if len(certificate.Certificate) == 0 || len(certificate.Key) == 0 { + log.Debugf("Delete certificate %v for domains %v which have no value.", certificate, certificate.Domain.ToStrArray()) + continue + } + certificates = append(certificates, certificate) + } + + if len(certificates) < len(s.storedData.Certificates) { + s.storedData.Certificates = certificates + s.SaveDataChan <- s.storedData + } } } diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 26fc66ccd..19c9086e8 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -41,7 +41,7 @@ type Configuration struct { Storage string `description:"Storage to use."` EntryPoint string `description:"EntryPoint to use."` OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` - OnDemand bool `description:"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 + OnDemand bool `description:"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 DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"` HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge"` Domains []types.Domain `description:"CN and SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='*.main.net'. No SANs for wildcards domain. Wildcard domains only accepted with DNSChallenge"` @@ -225,11 +225,17 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati } bundle := true + certificate, failures := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple) if len(failures) > 0 { return nil, fmt.Errorf("cannot obtain certificates %+v", failures) } - log.Debugf("Certificates obtained for domain %+v", uncheckedDomains) + + if len(certificate.Certificate) == 0 || len(certificate.PrivateKey) == 0 { + return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, certificate) + } + log.Debugf("Certificates obtained for domains %+v", uncheckedDomains) + if len(uncheckedDomains) > 1 { domain = types.Domain{Main: uncheckedDomains[0], SANs: uncheckedDomains[1:]} } else { @@ -446,16 +452,25 @@ func (p *Provider) renewCertificates() { log.Infof("Error renewing certificate from LE : %+v, %v", certificate.Domain, err) continue } + log.Infof("Renewing certificate from LE : %+v", certificate.Domain) + renewedCert, err := client.RenewCertificate(acme.CertificateResource{ Domain: certificate.Domain.Main, PrivateKey: certificate.Key, Certificate: certificate.Certificate, }, true, OSCPMustStaple) + if err != nil { log.Errorf("Error renewing certificate from LE: %v, %v", certificate.Domain, err) continue } + + if len(renewedCert.Certificate) == 0 || len(renewedCert.PrivateKey) == 0 { + log.Errorf("domains %v renew certificate with no value: %v", certificate.Domain.ToStrArray(), certificate) + continue + } + p.addCertificateForDomain(certificate.Domain, renewedCert.Certificate, renewedCert.PrivateKey) } } @@ -473,6 +488,7 @@ func (p *Provider) AddRoutes(router *mux.Router) { log.Debugf("Unable to split host and port: %v. Fallback to request host.", err) domain = req.Host } + tokenValue := getTokenValue(token, domain, p.Store) if len(tokenValue) > 0 { rw.WriteHeader(http.StatusOK)