Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create backup file during migration from ACME V1 to ACME V2 #3191

Merged
merged 5 commits into from
Apr 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 44 additions & 19 deletions acme/localStore.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,27 @@ 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
}
return account, nil
}

if isOldRegistration {
account.Email = ""
account.Registration = nil
account.PrivateKey = 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
}
}

return account, nil
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)
Expand All @@ -71,13 +75,13 @@ func ConvertToNewFormat(fileName string) {

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
}

Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions cmd/storeconfig/storeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 11 additions & 0 deletions docs/configuration/acme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
16 changes: 16 additions & 0 deletions provider/acme/local_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
}
}

Expand Down
20 changes: 18 additions & 2 deletions provider/acme/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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)
Expand Down