Create backup file during migration from ACME V1 to ACME V2
This commit is contained in:
parent
f0589b310f
commit
a2e03e3bd0
5 changed files with 96 additions and 22 deletions
|
@ -46,38 +46,42 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
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)
|
// 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) {
|
func ConvertToNewFormat(fileName string) {
|
||||||
localStore := acme.NewLocalStore(fileName)
|
localStore := acme.NewLocalStore(fileName)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue