cert-manager/pkg/issuer/acme/setup.go
James Munnelly 9d28261da5 Properly handle ErrNoAccount
Signed-off-by: James Munnelly <james@munnelly.eu>
2019-12-03 16:49:31 +00:00

335 lines
12 KiB
Go

/*
Copyright 2019 The Jetstack cert-manager contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package acme
import (
"context"
"crypto/rsa"
"fmt"
"net/url"
"strings"
acmeapi "golang.org/x/crypto/acme"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/jetstack/cert-manager/pkg/acme"
"github.com/jetstack/cert-manager/pkg/acme/client"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
logf "github.com/jetstack/cert-manager/pkg/logs"
"github.com/jetstack/cert-manager/pkg/util/errors"
"github.com/jetstack/cert-manager/pkg/util/pki"
)
const (
errorAccountRegistrationFailed = "ErrRegisterACMEAccount"
errorAccountVerificationFailed = "ErrVerifyACMEAccount"
errorAccountUpdateFailed = "ErrUpdateACMEAccount"
successAccountRegistered = "ACMEAccountRegistered"
successAccountVerified = "ACMEAccountVerified"
messageAccountRegistrationFailed = "Failed to register ACME account: "
messageAccountVerificationFailed = "Failed to verify ACME account: "
messageAccountUpdateFailed = "Failed to update ACME account:"
messageAccountRegistered = "The ACME account was registered with the ACME server"
messageAccountVerified = "The ACME account was verified with the ACME server"
)
// Setup will verify an existing ACME registration, or create one if not
// already registered.
func (a *Acme) Setup(ctx context.Context) error {
log := logf.FromContext(ctx)
// check if user has specified a v1 account URL, and set a status condition if so.
if newURL, ok := acmev1ToV2Mappings[a.issuer.GetSpec().ACME.Server]; ok {
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, "InvalidConfig",
fmt.Sprintf("Your ACME server URL is set to a v1 endpoint (%s). "+
"You should update the spec.acme.server field to %q", a.issuer.GetSpec().ACME.Server, newURL))
// return nil so that Setup only gets called again after the spec is updated
return nil
}
// if the namespace field is not set, we are working on a ClusterIssuer resource
// therefore we should check for the ACME private key in the 'cluster resource namespace'.
ns := a.issuer.GetObjectMeta().Namespace
if ns == "" {
ns = a.IssuerOptions.ClusterResourceNamespace
}
log = logf.WithRelatedResourceName(log, a.issuer.GetSpec().ACME.PrivateKey.Name, ns, "Secret")
// attempt to obtain the existing private key from the apiserver.
// if it does not exist then we generate one
// if it contains invalid data, warn the user and return without error.
// if any other error occurs, return it and retry.
pk, err := a.helper.ReadPrivateKey(a.issuer.GetSpec().ACME.PrivateKey, ns)
switch {
case apierrors.IsNotFound(err):
log.Info("generating acme account private key")
pk, err = a.createAccountPrivateKey(a.issuer.GetSpec().ACME.PrivateKey, ns)
if err != nil {
s := messageAccountRegistrationFailed + err.Error()
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, errorAccountRegistrationFailed, s)
return fmt.Errorf(s)
}
// We clear the ACME account URI as we have generated a new private key
a.issuer.GetStatus().ACMEStatus().URI = ""
case errors.IsInvalidData(err):
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, errorAccountVerificationFailed, fmt.Sprintf("Account private key is invalid: %v", err))
return nil
case err != nil:
s := messageAccountVerificationFailed + err.Error()
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, errorAccountVerificationFailed, s)
return fmt.Errorf(s)
}
acme.ClearClientCache()
cl, err := acme.ClientWithKey(a.issuer, pk)
if err != nil {
s := messageAccountVerificationFailed + err.Error()
log.Error(err, "failed to verify acme account")
a.Recorder.Event(a.issuer, v1.EventTypeWarning, errorAccountVerificationFailed, s)
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, errorAccountVerificationFailed, s)
return err
}
// TODO: perform a complex check to determine whether we need to verify
// the existing registration with the ACME server.
// This should take into account the ACME server URL, as well as a checksum
// of the private key's contents.
// Alternatively, we could add 'observed generation' fields here, tracking
// the most recent copy of the Issuer and Secret resource we have checked
// already.
rawServerURL := a.issuer.GetSpec().ACME.Server
parsedServerURL, err := url.Parse(rawServerURL)
if err != nil {
r := "InvalidURL"
s := fmt.Sprintf("Failed to parse existing ACME server URI %q: %v", rawServerURL, err)
a.Recorder.Eventf(a.issuer, v1.EventTypeWarning, r, s)
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, r, s)
// absorb errors as retrying will not help resolve this error
return nil
}
rawAccountURL := a.issuer.GetStatus().ACMEStatus().URI
parsedAccountURL, err := url.Parse(rawAccountURL)
if err != nil {
r := "InvalidURL"
s := fmt.Sprintf("Failed to parse existing ACME account URI %q: %v", rawAccountURL, err)
a.Recorder.Eventf(a.issuer, v1.EventTypeWarning, r, s)
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, r, s)
// absorb errors as retrying will not help resolve this error
return nil
}
hasReadyCondition := apiutil.IssuerHasCondition(a.issuer, v1alpha2.IssuerCondition{
Type: v1alpha2.IssuerConditionReady,
Status: cmmeta.ConditionTrue,
})
// If the Host components of the server URL and the account URL match,
// and the cached email matches the registered email, then
// we skip re-checking the account status to save excess calls to the
// ACME api.
if hasReadyCondition &&
a.issuer.GetStatus().ACMEStatus().URI != "" &&
parsedAccountURL.Host == parsedServerURL.Host &&
a.issuer.GetStatus().ACMEStatus().LastRegisteredEmail == a.issuer.GetSpec().ACME.Email {
log.Info("skipping re-verifying ACME account as cached registration " +
"details look sufficient")
return nil
}
if parsedAccountURL.Host != parsedServerURL.Host {
log.Info("ACME server URL host and ACME private key registration " +
"host differ. Re-checking ACME account registration")
a.issuer.GetStatus().ACMEStatus().URI = ""
}
// registerAccount will also verify the account exists if it already
// exists.
account, err := a.registerAccount(ctx, cl)
if err != nil {
s := messageAccountVerificationFailed + err.Error()
log.Error(err, "failed to verify ACME account")
a.Recorder.Event(a.issuer, v1.EventTypeWarning, errorAccountVerificationFailed, s)
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, errorAccountRegistrationFailed, s)
acmeErr, ok := err.(*acmeapi.Error)
// If this is not an ACME error, we will simply return it and retry later
if !ok {
return err
}
// If the status code is 400 (BadRequest), we will *not* retry this registration
// as it implies that something about the request (i.e. email address or private key)
// is invalid.
if acmeErr.StatusCode >= 400 && acmeErr.StatusCode < 500 {
log.Error(acmeErr, "skipping retrying account registration as a "+
"BadRequest response was returned from the ACME server")
return nil
}
// Otherwise if we receive anything other than a 400, we will retry.
return err
}
// if we got an account successfully, we must check if the registered
// email is the same as in the issuer spec
specEmail := a.issuer.GetSpec().ACME.Email
account, registeredEmail, err := ensureEmailUpToDate(ctx, cl, account, specEmail)
if err != nil {
s := messageAccountUpdateFailed + err.Error()
log.Error(err, "failed to update ACME account")
a.Recorder.Event(a.issuer, v1.EventTypeWarning, errorAccountUpdateFailed, s)
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionFalse, errorAccountUpdateFailed, s)
acmeErr, ok := err.(*acmeapi.Error)
// If this is not an ACME error, we will simply return it and retry later
if !ok {
return err
}
// If the status code is 400 (BadRequest), we will *not* retry this registration
// as it implies that something about the request (i.e. email address or private key)
// is invalid.
if acmeErr.StatusCode >= 400 && acmeErr.StatusCode < 500 {
log.Error(acmeErr, "skipping updating account email as a "+
"BadRequest response was returned from the ACME server")
return nil
}
// Otherwise if we receive anything other than a 400, we will retry.
return err
}
log.Info("verified existing registration with ACME server")
apiutil.SetIssuerCondition(a.issuer, v1alpha2.IssuerConditionReady, cmmeta.ConditionTrue, successAccountRegistered, messageAccountRegistered)
a.issuer.GetStatus().ACMEStatus().URI = account.URI
a.issuer.GetStatus().ACMEStatus().LastRegisteredEmail = registeredEmail
return nil
}
func ensureEmailUpToDate(ctx context.Context, cl client.Interface, acc *acmeapi.Account, specEmail string) (*acmeapi.Account, string, error) {
log := logf.FromContext(ctx)
// if no email was specified, then registeredEmail will remain empty
registeredEmail := ""
if len(acc.Contact) > 0 {
registeredEmail = strings.Replace(acc.Contact[0], "mailto:", "", 1)
}
// if they are different, we update the account
if registeredEmail != specEmail {
log.Info("updating ACME account email address", "email", specEmail)
emailurl := []string(nil)
if specEmail != "" {
emailurl = []string{fmt.Sprintf("mailto:%s", strings.ToLower(specEmail))}
}
acc.Contact = emailurl
var err error
acc, err = cl.UpdateReg(ctx, acc)
if err != nil {
return nil, "", err
}
// update the registeredEmail var so it is updated properly in the status below
registeredEmail = specEmail
}
return acc, registeredEmail, nil
}
// registerAccount will register a new ACME account with the server. If an
// account with the clients private key already exists, it will attempt to look
// up and verify the corresponding account, and will return that. If this fails
// due to a not found error it will register a new account with the given key.
func (a *Acme) registerAccount(ctx context.Context, cl client.Interface) (*acmeapi.Account, error) {
// check if the account already exists
acc, err := cl.GetReg(ctx, "")
if err == nil {
return acc, nil
}
if err != acmeapi.ErrNoAccount {
return nil, err
}
emailurl := []string(nil)
if a.issuer.GetSpec().ACME.Email != "" {
emailurl = []string{fmt.Sprintf("mailto:%s", strings.ToLower(a.issuer.GetSpec().ACME.Email))}
}
acc = &acmeapi.Account{
Contact: emailurl,
}
acc, err = cl.Register(ctx, acc, acmeapi.AcceptTOS)
if err != nil {
return nil, err
}
// TODO: re-enable this check once this field is set by Pebble
// if acc.Status != acme.StatusValid {
// return nil, fmt.Errorf("acme account is not valid")
// }
return acc, nil
}
// createAccountPrivateKey will generate a new RSA private key, and create it
// as a secret resource in the apiserver.
func (a *Acme) createAccountPrivateKey(sel cmmeta.SecretKeySelector, ns string) (*rsa.PrivateKey, error) {
sel = acme.PrivateKeySelector(sel)
accountPrivKey, err := pki.GenerateRSAPrivateKey(pki.MinRSAKeySize)
if err != nil {
return nil, err
}
_, err = a.Client.CoreV1().Secrets(ns).Create(&v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sel.Name,
Namespace: ns,
},
Data: map[string][]byte{
sel.Key: pki.EncodePKCS1PrivateKey(accountPrivKey),
},
})
if err != nil {
return nil, err
}
return accountPrivKey, err
}
var acmev1ToV2Mappings = map[string]string{
"https://acme-v01.api.letsencrypt.org/directory": "https://acme-v02.api.letsencrypt.org/directory",
"https://acme-staging.api.letsencrypt.org/directory": "https://acme-staging-v02.api.letsencrypt.org/directory",
"https://acme-v01.api.letsencrypt.org/directory/": "https://acme-v02.api.letsencrypt.org/directory",
"https://acme-staging.api.letsencrypt.org/directory/": "https://acme-staging-v02.api.letsencrypt.org/directory",
}