cert-manager/pkg/issuer/acme/issue.go
James Munnelly d323a1df0d Add unit tests for acmeorders and acmechallenges
Signed-off-by: James Munnelly <james@munnelly.eu>
2018-10-12 12:40:39 +01:00

343 lines
11 KiB
Go

/*
Copyright 2018 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 (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"hash/fnv"
"time"
"github.com/golang/glog"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"github.com/jetstack/cert-manager/pkg/acme"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer"
"github.com/jetstack/cert-manager/pkg/util/errors"
"github.com/jetstack/cert-manager/pkg/util/kube"
"github.com/jetstack/cert-manager/pkg/util/pki"
)
const (
createOrderWaitDuration = time.Minute * 5
)
var (
certificateGvk = v1alpha1.SchemeGroupVersion.WithKind("Certificate")
)
func (a *Acme) Issue(ctx context.Context, crt *v1alpha1.Certificate) (issuer.IssueResponse, error) {
// initially, we do not set the csr on the order resource we build.
// this is to save having the overhead of generating a new CSR in the case
// where the Order resource is up to date already
expectedOrder, err := buildOrder(crt, nil)
if err != nil {
return issuer.IssueResponse{}, err
}
// attempt to retrieve the existing order resource for this Certificate
var existingOrder *v1alpha1.Order
glog.V(4).Infof("Attempting to retrieve existing orders for %q/%q", crt.Namespace, crt.Name)
labelMap := certLabels(crt.Name)
selector := labels.NewSelector()
for k, v := range labelMap {
req, err := labels.NewRequirement(k, selection.Equals, []string{v})
if err != nil {
return issuer.IssueResponse{}, err
}
selector.Add(*req)
}
existingOrders, err := a.orderLister.Orders(crt.Namespace).List(selector)
if err != nil {
return issuer.IssueResponse{}, err
}
for _, o := range existingOrders {
// Don't touch any objects that don't have this certificate set as the
// owner reference.
if !metav1.IsControlledBy(o, crt) {
continue
}
if o.Name == expectedOrder.Name {
existingOrder = o
glog.V(4).Infof("Found existing order %q for certificate %s/%s", existingOrder.Name, crt.Namespace, crt.Name)
continue
}
// delete any old order resources
glog.Infof("Deleting Order resource %s/%s", o.Namespace, o.Name)
err := a.CMClient.CertmanagerV1alpha1().Orders(o.Namespace).Delete(o.Name, nil)
if err != nil {
return issuer.IssueResponse{}, err
}
}
// get existing certificate private key
glog.V(4).Infof("Attempting to fetch existing certificate private key")
key, err := kube.SecretTLSKey(a.secretsLister, crt.Namespace, crt.Spec.SecretName)
if err != nil {
// if the private key is not found, or is formatted incorrectly, we
// will generate a new one and create/overwrite the secret.
if !apierrors.IsNotFound(err) && !errors.IsInvalidData(err) {
return issuer.IssueResponse{}, err
}
// TODO: perhaps we shouldn't overwrite the secret if it is invalid,
// and instead bail out here?
glog.V(4).Infof("Generating new private key for %s/%s", crt.Namespace, crt.Name)
// generate a new private key.
key, err := pki.GenerateRSAPrivateKey(2048)
if err != nil {
return issuer.IssueResponse{}, err
}
keyBytes, err := pki.EncodePrivateKey(key)
if err != nil {
return issuer.IssueResponse{}, err
}
glog.V(4).Infof("Storing new certificate private key for %s/%s", crt.Namespace, crt.Name)
// We return the private key here early, and trigger an immediate requeue.
// This is because we have just generated a new one, and to keep our
// later logic simple we store it immediately.
// TODO: remove the 'requeue: true' as it could cause a race condition
// where our lister has not observed the new private key generated above
return issuer.IssueResponse{PrivateKey: keyBytes}, nil
}
// if there is an existing order, we check to make sure it is up to date
// with the current certificate & issuer configuration.
// if it is not, we will abandon the old order and create a new one.
// The 'retry' cases here will bypass the controller's rate-limiting, as
// well as the back-off applied to failing ACME Orders.
// They should therefore *only* match on changes to the actual Certificate
// resource, or underlying Order (i.e. user interaction).
if existingOrder != nil {
glog.V(4).Infof("Validating existing order CSR for Certificate %s/%s", crt.Namespace, crt.Name)
// if the existing order has expired, we should create a new one
// TODO: implement this order state in the acmeorders controller
if existingOrder.Status.State == v1alpha1.Expired {
return a.retryOrder(existingOrder)
}
// check if the existing order has failed.
// If it has, we check to see when it last failed and 'back-off' if it
// failed in the recent past.
if acme.IsFailureState(existingOrder.Status.State) {
if crt.Status.LastFailureTime == nil {
nowTime := metav1.NewTime(a.clock.Now())
crt.Status.LastFailureTime = &nowTime
}
if time.Now().Sub(crt.Status.LastFailureTime.Time) < createOrderWaitDuration {
return issuer.IssueResponse{}, fmt.Errorf("applying acme order back-off for certificate %s/%s because it has failed within the last %s", crt.Namespace, crt.Name, createOrderWaitDuration)
}
// otherwise, we clear the lastFailureTime and create a new order
// as the back-off time has passed.
crt.Status.LastFailureTime = nil
return a.retryOrder(existingOrder)
}
// check the CSR is created by the private key that we hold
csrBytes := existingOrder.Spec.CSR
if len(csrBytes) == 0 {
return a.retryOrder(existingOrder)
}
existingCSR, err := x509.ParseCertificateRequest(csrBytes)
if err != nil {
return a.retryOrder(existingOrder)
}
matches, err := pki.PublicKeyMatchesCSR(key.Public(), existingCSR)
if err != nil {
return issuer.IssueResponse{}, err
}
if !matches {
glog.V(4).Infof("CSR on existing order resource does not match certificate %s/%s private key. Creating new order.", crt.Namespace, crt.Name)
return a.retryOrder(existingOrder)
}
}
if existingOrder == nil {
glog.V(4).Infof("Creating new Order resource for Certificate %s/%s", crt.Namespace, crt.Name)
csr, err := pki.GenerateCSR(a.issuer, crt)
if err != nil {
// TODO: what errors can be produced here? some error types might
// be permenant, and we should handle that properly.
return issuer.IssueResponse{}, err
}
csrBytes, err := pki.EncodeCSR(csr, key)
if err != nil {
return issuer.IssueResponse{}, err
}
// set the CSR field on the order to be created
expectedOrder.Spec.CSR = csrBytes
existingOrder, err = a.CMClient.CertmanagerV1alpha1().Orders(crt.Namespace).Create(expectedOrder)
if err != nil {
return issuer.IssueResponse{}, err
}
glog.V(4).Infof("Created new Order resource named %q for Certificate %s/%s", expectedOrder.Name, crt.Namespace, crt.Name)
return issuer.IssueResponse{}, nil
}
if existingOrder.Status.State != v1alpha1.Valid {
glog.Infof("Order %s/%s is not in 'valid' state. Waiting for Order to transition before attempting to issue Certificate.", existingOrder.Namespace, existingOrder.Name)
// We don't immediately requeue, as the change to the Order resource on
// transition should trigger the certificate to be re-synced.
return issuer.IssueResponse{}, nil
}
// If the order is valid, we can attempt to retrieve the Certificate.
// First obtain an ACME client to make this easier.
cl, err := a.helper.ClientForIssuer(a.issuer)
if err != nil {
return issuer.IssueResponse{}, err
}
certSlice, err := cl.GetCertificate(ctx, existingOrder.Status.CertificateURL)
if err != nil {
// TODO: parse returned ACME error and potentially re-create order.
return issuer.IssueResponse{}, err
}
if len(certSlice) == 0 {
// TODO: parse returned ACME error and potentially re-create order.
return issuer.IssueResponse{}, fmt.Errorf("invalid certificate returned from acme server")
}
x509Cert, err := x509.ParseCertificate(certSlice[0])
if err != nil {
// TODO: parse returned ACME error and potentially re-create order.
return issuer.IssueResponse{}, fmt.Errorf("failed to parse returned x509 certificate: %v", err.Error())
}
if a.Context.IssuerOptions.CertificateNeedsRenew(x509Cert) {
// existing order's certificate is near expiry
return a.retryOrder(existingOrder)
}
// encode the retrieved certificates (including the chain)
certBuffer := bytes.NewBuffer([]byte{})
for _, cert := range certSlice {
err := pem.Encode(certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
if err != nil {
return issuer.IssueResponse{}, err
}
}
// encode the private key and return
keyPem, err := pki.EncodePrivateKey(key)
if err != nil {
// TODO: this is probably an internal error - we should fail safer here
return issuer.IssueResponse{}, err
}
return issuer.IssueResponse{
Certificate: certBuffer.Bytes(),
PrivateKey: keyPem,
}, nil
}
// retryOrder will delete the existing order with the foreground
// deletion policy.
// If delete successfully (i.e. cleaned up), the order name will be
// reset to empty and a resync of the resource will begin.
func (a *Acme) retryOrder(existingOrder *v1alpha1.Order) (issuer.IssueResponse, error) {
foregroundDeletion := metav1.DeletePropagationForeground
err := a.CMClient.CertmanagerV1alpha1().Orders(existingOrder.Namespace).Delete(existingOrder.Name, &metav1.DeleteOptions{
PropagationPolicy: &foregroundDeletion,
})
if err != nil {
return issuer.IssueResponse{}, err
}
// Updating the certificate status will trigger a requeue once the change
// has been observed by the informer.
// If we set Requeue: true here, we may cause a race where the lister has
// not observed the updated orderRef.
return issuer.IssueResponse{}, nil
}
func buildOrder(crt *v1alpha1.Certificate, csr []byte) (*v1alpha1.Order, error) {
spec := v1alpha1.OrderSpec{
CSR: csr,
IssuerRef: crt.Spec.IssuerRef,
CommonName: crt.Spec.CommonName,
DNSNames: crt.Spec.DNSNames,
Config: crt.Spec.ACME.Config,
}
hash, err := hashOrder(spec)
if err != nil {
return nil, err
}
return &v1alpha1.Order{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", crt.Name, hash),
Namespace: crt.Namespace,
Labels: certLabels(crt.Name),
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)},
},
Spec: spec,
}, nil
}
func certLabels(crtName string) map[string]string {
return map[string]string{
"acme.cert-manager.io/certificate-name": crtName,
}
}
func hashOrder(orderSpec v1alpha1.OrderSpec) (uint32, error) {
// create a shallow copy of the OrderSpec so we can overwrite the CSR field
orderSpec.CSR = nil
orderSpecBytes, err := json.Marshal(orderSpec)
if err != nil {
return 0, err
}
hashF := fnv.New32()
_, err = hashF.Write(orderSpecBytes)
if err != nil {
return 0, err
}
return hashF.Sum32(), nil
}