Create backup file during migration from ACME V1 to ACME V2

This commit is contained in:
NicoMen 2018-04-16 19:34:04 +02:00 committed by Traefiker Bot
parent f0589b310f
commit a2e03e3bd0
5 changed files with 96 additions and 22 deletions

View file

@ -46,12 +46,18 @@ func (s *LocalStore) Get() (*Account, error) {
if err := json.Unmarshal(file, &account); err != nil { if err := json.Unmarshal(file, &account); err != nil {
return nil, err return nil, err
} }
}
return account, nil
}
// RemoveAccountV1Values removes ACME account V1 values
func RemoveAccountV1Values(account *Account) error {
// Check if ACME Account is in ACME V1 format // Check if ACME Account is in ACME V1 format
if account != nil && account.Registration != nil { if account != nil && account.Registration != nil {
isOldRegistration, err := regexp.MatchString(acme.RegistrationURLPathV1Regexp, account.Registration.URI) isOldRegistration, err := regexp.MatchString(acme.RegistrationURLPathV1Regexp, account.Registration.URI)
if err != nil { if err != nil {
return nil, err return err
} }
if isOldRegistration { if isOldRegistration {
@ -60,9 +66,7 @@ func (s *LocalStore) Get() (*Account, error) {
account.PrivateKey = nil account.PrivateKey = nil
} }
} }
} return nil
return account, nil
} }
// ConvertToNewFormat converts old acme.json format to the new one and store the result into the file (used for the backward compatibility) // ConvertToNewFormat converts old acme.json format to the new one and store the result into the file (used for the backward compatibility)
@ -71,13 +75,13 @@ func ConvertToNewFormat(fileName string) {
storeAccount, err := localStore.GetAccount() storeAccount, err := localStore.GetAccount()
if err != nil { 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 return
} }
storeCertificates, err := localStore.GetCertificates() storeCertificates, err := localStore.GetCertificates()
if err != nil { 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 return
} }
@ -86,13 +90,25 @@ func ConvertToNewFormat(fileName string) {
account, err := localStore.Get() account, err := localStore.Get()
if err != nil { 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 return
} }
// Convert ACME data from old to new format // Convert ACME data from old to new format
newAccount := &acme.Account{} newAccount := &acme.Account{}
if account != nil && len(account.Email) > 0 { 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{ newAccount = &acme.Account{
PrivateKey: account.PrivateKey, PrivateKey: account.PrivateKey,
Registration: account.Registration, Registration: account.Registration,
@ -107,8 +123,8 @@ func ConvertToNewFormat(fileName string) {
Domain: cert.Domains, 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 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) { func FromNewToOldFormat(fileName string) (*Account, error) {
localStore := acme.NewLocalStore(fileName) localStore := acme.NewLocalStore(fileName)

View file

@ -134,10 +134,16 @@ func migrateACMEData(fileName string) (*acme.Account, error) {
if accountFromNewFormat == nil { if accountFromNewFormat == nil {
// convert ACME json file to KV store (used for backward compatibility) // convert ACME json file to KV store (used for backward compatibility)
localStore := acme.NewLocalStore(fileName) localStore := acme.NewLocalStore(fileName)
account, err = localStore.Get() account, err = localStore.Get()
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = acme.RemoveAccountV1Values(account)
if err != nil {
return nil, err
}
} else { } else {
account = accountFromNewFormat account = accountFromNewFormat
} }

View file

@ -543,3 +543,14 @@ Do not hesitate to complete it.
| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | Not tested yet | | [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | Not tested yet |
| [Route 53](https://aws.amazon.com/route53/) | `route53` | YES | | [Route 53](https://aws.amazon.com/route53/) | `route53` | YES |
| [VULTR](https://www.vultr.com) | `vultr` | Not tested yet | | [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.

View file

@ -52,6 +52,7 @@ func (s *LocalStore) get() (*StoredData, error) {
return nil, err return nil, err
} }
} }
// Check if ACME Account is in ACME V1 format // Check if ACME Account is in ACME V1 format
if s.storedData.Account != nil && s.storedData.Account.Registration != nil { if s.storedData.Account != nil && s.storedData.Account.Registration != nil {
isOldRegistration, err := regexp.MatchString(RegistrationURLPathV1Regexp, s.storedData.Account.Registration.URI) isOldRegistration, err := regexp.MatchString(RegistrationURLPathV1Regexp, s.storedData.Account.Registration.URI)
@ -63,6 +64,21 @@ func (s *LocalStore) get() (*StoredData, error) {
s.SaveDataChan <- s.storedData 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
}
} }
} }

View file

@ -41,7 +41,7 @@ type Configuration struct {
Storage string `description:"Storage to use."` Storage string `description:"Storage to use."`
EntryPoint string `description:"EntryPoint to use."` EntryPoint string `description:"EntryPoint to use."`
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` 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"` DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"`
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-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"` 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 bundle := true
certificate, failures := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple) certificate, failures := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple)
if len(failures) > 0 { if len(failures) > 0 {
return nil, fmt.Errorf("cannot obtain certificates %+v", failures) 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 { if len(uncheckedDomains) > 1 {
domain = types.Domain{Main: uncheckedDomains[0], SANs: uncheckedDomains[1:]} domain = types.Domain{Main: uncheckedDomains[0], SANs: uncheckedDomains[1:]}
} else { } else {
@ -446,16 +452,25 @@ func (p *Provider) renewCertificates() {
log.Infof("Error renewing certificate from LE : %+v, %v", certificate.Domain, err) log.Infof("Error renewing certificate from LE : %+v, %v", certificate.Domain, err)
continue continue
} }
log.Infof("Renewing certificate from LE : %+v", certificate.Domain) log.Infof("Renewing certificate from LE : %+v", certificate.Domain)
renewedCert, err := client.RenewCertificate(acme.CertificateResource{ renewedCert, err := client.RenewCertificate(acme.CertificateResource{
Domain: certificate.Domain.Main, Domain: certificate.Domain.Main,
PrivateKey: certificate.Key, PrivateKey: certificate.Key,
Certificate: certificate.Certificate, Certificate: certificate.Certificate,
}, true, OSCPMustStaple) }, true, OSCPMustStaple)
if err != nil { if err != nil {
log.Errorf("Error renewing certificate from LE: %v, %v", certificate.Domain, err) log.Errorf("Error renewing certificate from LE: %v, %v", certificate.Domain, err)
continue 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) 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) log.Debugf("Unable to split host and port: %v. Fallback to request host.", err)
domain = req.Host domain = req.Host
} }
tokenValue := getTokenValue(token, domain, p.Store) tokenValue := getTokenValue(token, domain, p.Store)
if len(tokenValue) > 0 { if len(tokenValue) > 0 {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)