Adds extensible issuing controller

Signed-off-by: JoshVanL <vleeuwenjoshua@gmail.com>
This commit is contained in:
JoshVanL 2020-04-05 20:32:46 +01:00 committed by James Munnelly
parent 9556b32e81
commit ffb5201d95
12 changed files with 1041 additions and 14 deletions

View File

@ -119,6 +119,15 @@ func GetCertificateCondition(crt *cmapi.Certificate, conditionType cmapi.Certifi
return nil
}
func GetCertificateRequestCondition(req *cmapi.CertificateRequest, conditionType cmapi.CertificateRequestConditionType) *cmapi.CertificateRequestCondition {
for _, cond := range req.Status.Conditions {
if cond.Type == conditionType {
return &cond
}
}
return nil
}
// SetCertificateCondition will set a 'condition' on the given Certificate.
// - If no condition of the same type already exists, the condition will be
// inserted with the LastTransitionTime set to the current time.
@ -164,6 +173,21 @@ func SetCertificateCondition(crt *cmapi.Certificate, conditionType cmapi.Certifi
klog.Infof("Setting lastTransitionTime for Certificate %q condition %q to %v", crt.Name, conditionType, nowTime.Time)
}
// RemoteCertificateCondition will remove any condition with this condition type
func RemoveCertificateCondition(crt *cmapi.Certificate, conditionType cmapi.CertificateConditionType) {
var updatedConditions []cmapi.CertificateCondition
// Search through existing conditions
for _, cond := range crt.Status.Conditions {
// Only add unrelated conditions
if cond.Type != conditionType {
updatedConditions = append(updatedConditions, cond)
}
}
crt.Status.Conditions = updatedConditions
}
// SetCertificateRequestCondition will set a 'condition' on the given CertificateRequest.
// - If no condition of the same type already exists, the condition will be
// inserted with the LastTransitionTime set to the current time.

View File

@ -55,6 +55,9 @@ const (
// Annotation names for CertificateRequests
const (
CRPrivateKeyAnnotationKey = "cert-manager.io/private-key-secret-name"
// Annotation to declare the CertificateRequest "revision", beloning to a Certificate Resource
CertificateRequestRevisionAnnotationKey = "cert-manager.io/certificate-revision"
)
const (

View File

@ -55,6 +55,8 @@ const (
// Annotation names for CertificateRequests
const (
CRPrivateKeyAnnotationKey = "cert-manager.io/private-key-secret-name"
// Annotation to declare the CertificateRequest "revision", beloning to a Certificate Resource
CertificateRequestRevisionAnnotationKey = "cert-manager.io/certificate-revision"
)
const (

View File

@ -30,6 +30,7 @@ filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/controller/expcertificates/issuing:all-srcs",
"//pkg/controller/expcertificates/trigger:all-srcs",
],
tags = ["automanaged"],

View File

@ -0,0 +1,65 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"issuing_controller.go",
"keystore.go",
"secret.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/controller/expcertificates/issuing",
visibility = ["//visibility:public"],
deps = [
"//pkg/api/util:go_default_library",
"//pkg/apis/certmanager/v1alpha2:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/clientset/versioned:go_default_library",
"//pkg/client/informers/externalversions:go_default_library",
"//pkg/client/listers/certmanager/v1alpha2:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/controller/expcertificates:go_default_library",
"//pkg/logs:go_default_library",
"//pkg/util/kube:go_default_library",
"//pkg/util/pki:go_default_library",
"@com_github_go_logr_logr//:go_default_library",
"@com_github_pavel_v_chernykh_keystore_go//:go_default_library",
"@com_sslmate_software_src_go_pkcs12//:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/api/errors:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/labels:go_default_library",
"@io_k8s_client_go//informers:go_default_library",
"@io_k8s_client_go//kubernetes:go_default_library",
"@io_k8s_client_go//listers/core/v1:go_default_library",
"@io_k8s_client_go//tools/cache:go_default_library",
"@io_k8s_client_go//tools/record:go_default_library",
"@io_k8s_client_go//util/workqueue:go_default_library",
"@io_k8s_utils//clock:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["keystore_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/apis/certmanager/v1alpha2:go_default_library",
"//pkg/util/pki:go_default_library",
"@com_github_pavel_v_chernykh_keystore_go//:go_default_library",
"@com_sslmate_software_src_go_pkcs12//:go_default_library",
],
)

View File

@ -0,0 +1,368 @@
/*
Copyright 2020 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 issuing
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"k8s.io/utils/clock"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
cminformers "github.com/jetstack/cert-manager/pkg/client/informers/externalversions"
cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha2"
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
certificates "github.com/jetstack/cert-manager/pkg/controller/expcertificates"
logf "github.com/jetstack/cert-manager/pkg/logs"
utilkube "github.com/jetstack/cert-manager/pkg/util/kube"
utilpki "github.com/jetstack/cert-manager/pkg/util/pki"
)
const (
ControllerName = "CertificateIssuing"
ctxTimeout = time.Second * 10
)
var (
certificateGvk = cmapi.SchemeGroupVersion.WithKind("Certificate")
)
// This controller observes the state of the certificate's 'Issuing' condition,
// which will then copy the singed certificates and private key to the target
// Secret resource.
type controller struct {
certificateLister cmlisters.CertificateLister
certificateRequestLister cmlisters.CertificateRequestLister
secretLister corelisters.SecretLister
recorder record.EventRecorder
clock clock.Clock
client cmclient.Interface
kubeClient kubernetes.Interface
// if true, Secret resources created by the controller will have an
// 'owner reference' set, meaning when the Certificate is deleted, the
// Secret resource will be automatically deleted.
// This option is disabled by default.
enableSecretOwnerReferences bool
// experimentalIssuePKCS12, if true, will make the certificates controller
// create a `keystore.p12` in the Secret resource for each Certificate.
// This can only be toggled globally, and the keystore will be encrypted
// with the supplied ExperimentalPKCS12KeystorePassword.
// This flag is likely to be removed in future in favour of native PKCS12
// keystore bundle support.
experimentalIssuePKCS12 bool
// ExperimentalPKCS12KeystorePassword is the password used to encrypt and
// decrypt PKCS#12 bundles stored in Secret resources.
// This option only has any affect is ExperimentalIssuePKCS12 is true.
experimentalPKCS12KeystorePassword string
// experimentalIssueJKS, if true, will make the certificates controller
// create a `keystore.jks` in the Secret resource for each Certificate.
// This can only be toggled globally, and the keystore will be encrypted
// with the supplied ExperimentalJKSPassword.
// This flag is likely to be removed in future in favour of native JKS
// keystore bundle support.
experimentalIssueJKS bool
// experimentalJKSPassword is the password used to encrypt and
// decrypt JKS files stored in Secret resources.
// This option only has any affect is experimentalIssueJKS is true.
experimentalJKSPassword string
}
func NewController(
log logr.Logger,
kubeClient kubernetes.Interface,
client cmclient.Interface,
factory informers.SharedInformerFactory,
cmFactory cminformers.SharedInformerFactory,
recorder record.EventRecorder,
clock clock.Clock,
certificateControllerOptions controllerpkg.CertificateOptions,
) (*controller, workqueue.RateLimitingInterface, []cache.InformerSynced) {
// create a queue used to queue up items to be processed
queue := workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(time.Second*1, time.Second*30), ControllerName)
// obtain references to all the informers used by this controller
certificateInformer := cmFactory.Certmanager().V1alpha2().Certificates()
certificateRequestInformer := cmFactory.Certmanager().V1alpha2().CertificateRequests()
secretsInformer := factory.Core().V1().Secrets()
certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue})
certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{
WorkFunc: controllerpkg.HandleOwnedResourceNamespacedFunc(log, queue, certificateGvk, certificates.CertificateGetFunc(certificateInformer.Lister())),
})
secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{
// Issuer reconciles on changes to the Secret named `spec.nextPrivateKeySecretName`
WorkFunc: certificates.EnqueueCertificatesForSecretNameFunc(log, certificateInformer.Lister(), labels.Everything(),
certificates.WithNextPrivateKeySecretNamePredicateFunc, queue),
})
// build a list of InformerSynced functions that will be returned by the Register method.
// the controller will only begin processing items once all of these informers have synced.
mustSync := []cache.InformerSynced{
certificateRequestInformer.Informer().HasSynced,
secretsInformer.Informer().HasSynced,
certificateInformer.Informer().HasSynced,
}
return &controller{
certificateLister: certificateInformer.Lister(),
certificateRequestLister: certificateRequestInformer.Lister(),
secretLister: secretsInformer.Lister(),
kubeClient: kubeClient,
client: client,
recorder: recorder,
clock: clock,
enableSecretOwnerReferences: certificateControllerOptions.EnableOwnerRef,
experimentalIssuePKCS12: certificateControllerOptions.ExperimentalIssuePKCS12,
experimentalPKCS12KeystorePassword: certificateControllerOptions.ExperimentalPKCS12KeystorePassword,
experimentalIssueJKS: certificateControllerOptions.ExperimentalIssueJKS,
experimentalJKSPassword: certificateControllerOptions.ExperimentalJKSPassword,
}, queue, mustSync
}
func (c *controller) ProcessItem(ctx context.Context, key string) error {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
log := logf.FromContext(ctx).WithValues("key", key)
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return nil
}
crt, err := c.certificateLister.Certificates(namespace).Get(name)
if apierrors.IsNotFound(err) {
log.Error(err, "certificate not found for key")
return nil
}
if err != nil {
return err
}
if !apiutil.CertificateHasCondition(crt, cmapi.CertificateCondition{
Type: cmapi.CertificateConditionIssuing,
Status: cmmeta.ConditionTrue,
}) {
// Do nothing if an issuance is not in progress.
return nil
}
if crt.Status.NextPrivateKeySecretName == nil ||
len(*crt.Status.NextPrivateKeySecretName) == 0 {
// Do nothing if the next private key secret name is not set
return nil
}
nextPrivateKeySecretName := *crt.Status.NextPrivateKeySecretName
// CertificateRequest revisions begin from 1. If no revision is set on the
// status then assume no revision yet set.
nextRevision := 1
if crt.Status.Revision != nil {
nextRevision = *crt.Status.Revision + 1
}
reqs, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace),
labels.Everything(),
certificates.WithCertificateRevisionPredicateFunc(nextRevision),
certificates.WithCertificateRequestOwnerPredicateFunc(crt),
)
if err != nil || len(reqs) != 1 {
// If error return.
// if no error but none exist do nothing.
// If no error but multiple exist, then leave to requestmanager controller
// to clean up.
return err
}
req := reqs[0]
reqReason := apiutil.GetCertificateRequestCondition(req, cmapi.CertificateRequestConditionReady).Reason
switch reqReason {
// If the certificate request has failed, set the last failure time to now,
// and set the Issuing status condition to False with reason.
case cmapi.CertificateRequestReasonFailed:
nowTime := metav1.NewTime(c.clock.Now())
crt.Status.LastFailureTime = &nowTime
var reason, message string
condition := apiutil.GetCertificateRequestCondition(req, cmapi.CertificateRequestConditionReady)
reason = condition.Reason
message = fmt.Sprintf("The certificate request has failed to complete and will be retried: %s",
condition.Message)
crt = crt.DeepCopy()
apiutil.SetCertificateCondition(crt, cmapi.CertificateConditionIssuing, cmmeta.ConditionFalse, reason, message)
_, err := c.client.CertmanagerV1alpha2().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{})
if err != nil {
return err
}
c.recorder.Event(crt, corev1.EventTypeWarning, reason, message)
return nil
// If the CertificateRequest is valid, verify its status and update
// accordingly.
case cmapi.CertificateRequestReasonIssued:
return c.issueCertificate(ctx, namespace, nextPrivateKeySecretName, nextRevision, crt, req)
// CertificateRequest is not in a final state so do nothing.
default:
return nil
}
}
// issueCertificate will ensure the public key of the CSR matches the signed
// certificate, and then store the certificate, CA and private key into the
// Secret in the appropriate format type.
func (c *controller) issueCertificate(ctx context.Context, namespace, nextPrivateKeySecretName string, nextRevision int, crt *cmapi.Certificate, req *cmapi.CertificateRequest) error {
csr, err := utilpki.DecodeX509CertificateRequestBytes(req.Spec.CSRPEM)
if err != nil {
return err
}
key, err := utilkube.SecretTLSKeyRef(ctx, c.secretLister, namespace, nextPrivateKeySecretName, corev1.TLSPrivateKeyKey)
if apierrors.IsNotFound(err) {
// If secret does not exist, then the public key does not match. Do
// nothing (requestmanager will handle this).
return nil
}
if err != nil {
return err
}
publicKeyMatches, err := utilpki.PublicKeyMatchesCSR(key.Public, csr)
if err != nil {
return err
}
// If public key does not match, do nothing (requestmanager will handle this).
if !publicKeyMatches {
return nil
}
// Verify the CSR options match what is requested in certificate.spec.
violations, err := certificates.RequestMatchesSpec(req, crt.Spec)
if err != nil {
return err
}
// If there are violations in the spec, then the requestmanager will handle this.
if len(violations) > 0 {
return nil
}
//Encode and issue the key-pair (store it in the Secret)
nextPrivateKeySecret, err := c.secretLister.Secrets(namespace).Get(*crt.Status.NextPrivateKeySecretName)
if err != nil {
// Even if the secret does not exist, we should error here as something
// potentially has gone very wrong. We should back off to be safe and try
// again once the next Secret has been created with the private key.
return err
}
keyData, ok := nextPrivateKeySecret.Data[corev1.TLSPrivateKeyKey]
if !ok {
return fmt.Errorf("failed to find private key in Secret %s/%s at key %q",
namespace, *crt.Status.NextPrivateKeySecretName, corev1.TLSPrivateKeyKey)
}
signedCertificate := req.Status.Certificate
ca := req.Status.CA
err = c.updateSecretData(ctx, namespace, crt, secretData{sk: keyData, cert: signedCertificate, ca: ca})
if err != nil {
return err
}
//Set status.revision to revision of the CertificateRequest
crt.Status.Revision = &nextRevision
crt = crt.DeepCopy()
// Remove Issuing status condition
apiutil.RemoveCertificateCondition(crt, cmapi.CertificateConditionIssuing)
//Clear status.lastFailureTime (if set)
crt.Status.LastFailureTime = nil
_, err = c.client.CertmanagerV1alpha2().Certificates(crt.Namespace).UpdateStatus(ctx, crt, metav1.UpdateOptions{})
if err != nil {
return err
}
message := "The certificate has been successfully issued"
c.recorder.Event(crt, corev1.EventTypeNormal, "Issuing", message)
return nil
}
// controllerWrapper wraps the `controller` structure to make it implement
// the controllerpkg.queueingController interface
type controllerWrapper struct {
*controller
}
func (c *controllerWrapper) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) {
// construct a new named logger to be reused throughout the controller
log := logf.FromContext(ctx.RootContext, ControllerName)
ctrl, queue, mustSync := NewController(log,
ctx.Client,
ctx.CMClient,
ctx.KubeSharedInformerFactory,
ctx.SharedInformerFactory,
ctx.Recorder,
ctx.Clock,
ctx.CertificateOptions,
)
c.controller = ctrl
return queue, mustSync, nil
}
func init() {
controllerpkg.Register(ControllerName, func(ctx *controllerpkg.Context) (controllerpkg.Interface, error) {
return controllerpkg.NewBuilder(ctx, ControllerName).
For(&controllerWrapper{}).
Complete()
})
}

View File

@ -0,0 +1,136 @@
/*
Copyright 2020 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.
*/
// This file defines methods used for PKCS#12 support.
// This is an experimental feature and the contents of this file are intended
// to be absorbed into a more fully fledged implementing ahead of the v0.15
// release.
// This should hopefully not exist by the next time you come to read this :)
package issuing
import (
"bytes"
"crypto/rand"
"crypto/x509"
"time"
jks "github.com/pavel-v-chernykh/keystore-go"
"software.sslmate.com/src/go-pkcs12"
"github.com/jetstack/cert-manager/pkg/util/pki"
)
const (
// pkcs12SecretKey is the name of the data entry in the Secret resource
// used to store the p12 file.
pkcs12SecretKey = "keystore.p12"
// jksSecretKey is the name of the data entry in the Secret resource
// used to store the jks file.
jksSecretKey = "keystore.jks"
)
// encodePKCS12Keystore will encode a PKCS12 keystore using the password provided.
// The key, certificate and CA data must be provided in PKCS1 or PKCS8 PEM format.
// If the certificate data contains multiple certificates, the first will be used
// as the keystores 'certificate' and the remaining certificates will be prepended
// to the list of CAs in the resulting keystore.
func encodePKCS12Keystore(password string, rawKey []byte, certPem []byte, caPem []byte) ([]byte, error) {
key, err := pki.DecodePrivateKeyBytes(rawKey)
if err != nil {
return nil, err
}
certs, err := pki.DecodeX509CertificateChainBytes(certPem)
if err != nil {
return nil, err
}
var cas []*x509.Certificate
if len(caPem) > 0 {
cas, err = pki.DecodeX509CertificateChainBytes(caPem)
if err != nil {
return nil, err
}
// prepend the certificate chain to the list of certificates as the PKCS12
// library only allows setting a single certificate.
if len(certs) > 1 {
cas = append(certs[1:], cas...)
}
}
keystoreData, err := pkcs12.Encode(rand.Reader, key, certs[0], cas, password)
if err != nil {
return nil, err
}
return keystoreData, nil
}
func encodeJKSKeystore(password string, rawKey []byte, certPem []byte, caPem []byte) ([]byte, error) {
// encode the private key to PKCS8
key, err := pki.DecodePrivateKeyBytes(rawKey)
if err != nil {
return nil, err
}
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, err
}
// encode the certificate chain
chain, err := pki.DecodeX509CertificateChainBytes(certPem)
if err != nil {
return nil, err
}
certs := make([]jks.Certificate, len(chain))
for i, cert := range chain {
certs[i] = jks.Certificate{
Type: "X509",
Content: cert.Raw,
}
}
ks := jks.KeyStore{
"certificate": &jks.PrivateKeyEntry{
Entry: jks.Entry{
CreationDate: time.Now(),
},
PrivKey: keyDER,
CertChain: certs,
},
}
// add the CA certificate, if set
if len(caPem) > 0 {
ca, err := pki.DecodeX509CertificateBytes(caPem)
if err != nil {
return nil, err
}
ks["ca"] = &jks.TrustedCertificateEntry{
Entry: jks.Entry{
CreationDate: time.Now(),
},
Certificate: jks.Certificate{
Type: "X509",
Content: ca.Raw,
},
}
}
buf := &bytes.Buffer{}
if err := jks.Encode(buf, ks, []byte(password)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,225 @@
/*
Copyright 2020 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 issuing
import (
"bytes"
"testing"
jks "github.com/pavel-v-chernykh/keystore-go"
"software.sslmate.com/src/go-pkcs12"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
"github.com/jetstack/cert-manager/pkg/util/pki"
)
func mustGeneratePrivateKey(t *testing.T, encoding cmapi.KeyEncoding) []byte {
pk, err := pki.GenerateRSAPrivateKey(2048)
if err != nil {
t.Fatal(err)
}
pkBytes, err := pki.EncodePrivateKey(pk, encoding)
if err != nil {
t.Fatal(err)
}
return pkBytes
}
func mustSelfSignCertificate(t *testing.T, pkBytes []byte) []byte {
if pkBytes == nil {
pkBytes = mustGeneratePrivateKey(t, cmapi.PKCS8)
}
pk, err := pki.DecodePrivateKeyBytes(pkBytes)
if err != nil {
t.Fatal(err)
}
x509Crt, err := pki.GenerateTemplate(&cmapi.Certificate{
Spec: cmapi.CertificateSpec{
DNSNames: []string{"example.com"},
},
})
if err != nil {
t.Fatal(err)
}
certBytes, _, err := pki.SignCertificate(x509Crt, x509Crt, pk.Public(), pk)
if err != nil {
t.Fatal(err)
}
return certBytes
}
func TestEncodeJKSKeystore(t *testing.T) {
tests := map[string]struct {
password string
rawKey, certPEM, caPEM []byte
verify func(t *testing.T, out []byte, err error)
}{
"encode a JKS bundle for a PKCS1 key and certificate only": {
password: "password",
rawKey: mustGeneratePrivateKey(t, cmapi.PKCS1),
certPEM: mustSelfSignCertificate(t, nil),
verify: func(t *testing.T, out []byte, err error) {
if err != nil {
t.Errorf("expected no error but got: %v", err)
return
}
buf := bytes.NewBuffer(out)
ks, err := jks.Decode(buf, []byte("password"))
if err != nil {
t.Errorf("error decoding keystore: %v", err)
return
}
if ks["certificate"] == nil {
t.Errorf("no certificate data found in keystore")
}
if ks["ca"] != nil {
t.Errorf("unexpected ca data found in keystore")
}
},
},
"encode a JKS bundle for a PKCS8 key and certificate only": {
password: "password",
rawKey: mustGeneratePrivateKey(t, cmapi.PKCS8),
certPEM: mustSelfSignCertificate(t, nil),
verify: func(t *testing.T, out []byte, err error) {
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
buf := bytes.NewBuffer(out)
ks, err := jks.Decode(buf, []byte("password"))
if err != nil {
t.Errorf("error decoding keystore: %v", err)
return
}
if ks["certificate"] == nil {
t.Errorf("no certificate data found in keystore")
}
if ks["ca"] != nil {
t.Errorf("unexpected ca data found in keystore")
}
},
},
"encode a JKS bundle for a key, certificate and ca": {
password: "password",
rawKey: mustGeneratePrivateKey(t, cmapi.PKCS8),
certPEM: mustSelfSignCertificate(t, nil),
caPEM: mustSelfSignCertificate(t, nil),
verify: func(t *testing.T, out []byte, err error) {
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
buf := bytes.NewBuffer(out)
ks, err := jks.Decode(buf, []byte("password"))
if err != nil {
t.Errorf("error decoding keystore: %v", err)
return
}
if ks["certificate"] == nil {
t.Errorf("no certificate data found in keystore")
}
if ks["ca"] == nil {
t.Errorf("no ca data found in keystore")
}
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
out, err := encodeJKSKeystore(test.password, test.rawKey, test.certPEM, test.caPEM)
test.verify(t, out, err)
})
}
}
func TestEncodePKCS12Keystore(t *testing.T) {
tests := map[string]struct {
password string
rawKey, certPEM, caPEM []byte
verify func(t *testing.T, out []byte, err error)
}{
"encode a JKS bundle for a PKCS1 key and certificate only": {
password: "password",
rawKey: mustGeneratePrivateKey(t, cmapi.PKCS1),
certPEM: mustSelfSignCertificate(t, nil),
verify: func(t *testing.T, out []byte, err error) {
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
pk, cert, err := pkcs12.Decode(out, "password")
if err != nil {
t.Errorf("error decoding keystore: %v", err)
return
}
if cert == nil {
t.Errorf("no certificate data found in keystore")
}
if pk == nil {
t.Errorf("no ca data found in keystore")
}
},
},
"encode a JKS bundle for a PKCS8 key and certificate only": {
password: "password",
rawKey: mustGeneratePrivateKey(t, cmapi.PKCS8),
certPEM: mustSelfSignCertificate(t, nil),
verify: func(t *testing.T, out []byte, err error) {
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
pk, cert, err := pkcs12.Decode(out, "password")
if err != nil {
t.Errorf("error decoding keystore: %v", err)
return
}
if cert == nil {
t.Errorf("no certificate data found in keystore")
}
if pk == nil {
t.Errorf("no ca data found in keystore")
}
},
},
"encode a JKS bundle for a key, certificate and ca": {
password: "password",
rawKey: mustGeneratePrivateKey(t, cmapi.PKCS8),
certPEM: mustSelfSignCertificate(t, nil),
caPEM: mustSelfSignCertificate(t, nil),
verify: func(t *testing.T, out []byte, err error) {
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
// The pkcs12 package does not expose a way to decode the CA
// data that has been written.
// It will return an error when attempting to decode a file
// with more than one 'certbag', so we just ensure the error
// returned is the expected error and don't inspect the keystore
// contents.
_, _, err = pkcs12.Decode(out, "password")
if err == nil || err.Error() != "pkcs12: expected exactly two safe bags in the PFX PDU" {
t.Errorf("unexpected error string, exp=%q, got=%v", "pkcs12: expected exactly two safe bags in the PFX PDU", err)
return
}
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
out, err := encodePKCS12Keystore(test.password, test.rawKey, test.certPEM, test.caPEM)
test.verify(t, out, err)
})
}
}

View File

@ -0,0 +1,190 @@
/*
Copyright 2020 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.
*/
// This file defines methods used for PKCS#12 support.
// This is an experimental feature and the contents of this file are intended
// to be absorbed into a more fully fledged implementing ahead of the v0.15
// release.
// This should hopefully not exist by the next time you come to read this :)
package issuing
import (
"bytes"
"context"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
utilpki "github.com/jetstack/cert-manager/pkg/util/pki"
)
// secretData is a structure wrapping private key, certificate and CA data
type secretData struct {
sk, cert, ca []byte
}
// updateSecretData will ensure the Secret resource contains the given secret
// data as well as appropriate metadata.
// If the Secret resource does not exist, it will be created.
// Otherwise, the existing resource will be updated.
// The first return argument will be true if the resource was updated/created
// without error.
// updateSecretData will also update deprecated annotations if they exist.
func (c *controller) updateSecretData(ctx context.Context, namespace string, crt *cmapi.Certificate, data secretData) error {
// Fetch a copy of the existing Secret resource
secret, err := c.secretLister.Secrets(crt.Namespace).Get(crt.Spec.SecretName)
if !apierrors.IsNotFound(err) && err != nil {
// If secret doesn't exist yet, then don't error
return err
}
secretExists := (secret != nil)
// If the seret does not exist yet, then we need to create one
if !secretExists {
secret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: crt.Spec.SecretName,
Namespace: namespace,
},
Type: corev1.SecretTypeTLS,
}
}
// secret will be overwritten by 'existingSecret' if existingSecret is non-nil
if c.enableSecretOwnerReferences {
secret.OwnerReferences = []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)}
}
//newSecret := secret.DeepCopy()
err = c.setSecretValues(crt, secret, data)
if err != nil {
return err
}
// TODO: P12/JKS values use a random parameter so it's values will always
// change. Devise a better solution for checking change.
//if reflect.DeepEqual(secret, newSecret) {
// return nil
//}
// If secret does not exist then create it
if !secretExists {
_, err = c.kubeClient.CoreV1().Secrets(secret.Namespace).Create(ctx, secret, metav1.CreateOptions{})
return err
}
_, err = c.kubeClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{})
return err
}
// setSecretValues will update the Secret resource 's' with the data contained
// in the given secretData.
// It will update labels and annotations on the Secret resource appropriately.
// The Secret resource 's' must be non-nil, although may be a resource that does
// not exist in the Kubernetes apiserver yet.
// setSecretValues will NOT actually update the resource in the apiserver.
// If updating an existing Secret resource returned by an api client 'lister',
// make sure to DeepCopy the object first to avoid modifying data in-cache.
// It will also update depreciated issuer name and kind annotations if they exist.
func (c *controller) setSecretValues(crt *cmapi.Certificate, s *corev1.Secret, data secretData) error {
// initialize the `Data` field if it is nil
if s.Data == nil {
s.Data = make(map[string][]byte)
}
// Handle the experimental PKCS12 support
if c.experimentalIssuePKCS12 {
// Only write a new PKCS12 file if any of the private key/certificate/CA data has
// actually changed.
if data.sk != nil && data.cert != nil &&
(!bytes.Equal(s.Data[corev1.TLSPrivateKeyKey], data.sk) ||
!bytes.Equal(s.Data[corev1.TLSCertKey], data.cert) ||
!bytes.Equal(s.Data[cmmeta.TLSCAKey], data.ca)) {
keystoreData, err := encodePKCS12Keystore(c.experimentalPKCS12KeystorePassword, data.sk, data.cert, data.ca)
if err != nil {
return fmt.Errorf("error encoding PKCS12 bundle: %w", err)
}
// always overwrite the keystore entry for now
s.Data[pkcs12SecretKey] = keystoreData
}
}
// Handle the experimental JKS support
if c.experimentalIssueJKS {
// Only write a new JKS file if any of the private key/certificate/CA data has
// actually changed.
if data.sk != nil && data.cert != nil &&
(!bytes.Equal(s.Data[corev1.TLSPrivateKeyKey], data.sk) ||
!bytes.Equal(s.Data[corev1.TLSCertKey], data.cert) ||
!bytes.Equal(s.Data[cmmeta.TLSCAKey], data.ca)) {
keystoreData, err := encodeJKSKeystore(c.experimentalJKSPassword, data.sk, data.cert, data.ca)
if err != nil {
return fmt.Errorf("error encoding JKS bundle: %w", err)
}
// always overwrite the keystore entry for now
s.Data[jksSecretKey] = keystoreData
}
}
s.Data[corev1.TLSPrivateKeyKey] = data.sk
s.Data[corev1.TLSCertKey] = data.cert
s.Data[cmmeta.TLSCAKey] = data.ca
if s.Annotations == nil {
s.Annotations = make(map[string]string)
}
s.Annotations[cmapi.CertificateNameKey] = crt.Name
s.Annotations[cmapi.IssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name
s.Annotations[cmapi.IssuerKindAnnotationKey] = apiutil.IssuerKind(crt.Spec.IssuerRef)
// If deprecated annotations exist with any value, then they too shall be
// updated
if _, ok := s.Annotations[cmapi.DeprecatedIssuerNameAnnotationKey]; ok {
s.Annotations[cmapi.DeprecatedIssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name
}
if _, ok := s.Annotations[cmapi.DeprecatedIssuerKindAnnotationKey]; ok {
s.Annotations[cmapi.DeprecatedIssuerKindAnnotationKey] = apiutil.IssuerKind(crt.Spec.IssuerRef)
}
// if the certificate data is empty, clear the subject related annotations
if len(data.cert) == 0 {
delete(s.Annotations, cmapi.CommonNameAnnotationKey)
delete(s.Annotations, cmapi.AltNamesAnnotationKey)
delete(s.Annotations, cmapi.IPSANAnnotationKey)
delete(s.Annotations, cmapi.URISANAnnotationKey)
} else {
x509Cert, err := utilpki.DecodeX509CertificateBytes(data.cert)
// TODO: handle InvalidData here?
if err != nil {
return err
}
s.Annotations[cmapi.CommonNameAnnotationKey] = x509Cert.Subject.CommonName
s.Annotations[cmapi.AltNamesAnnotationKey] = strings.Join(x509Cert.DNSNames, ",")
s.Annotations[cmapi.IPSANAnnotationKey] = strings.Join(utilpki.IPAddressesToString(x509Cert.IPAddresses), ",")
s.Annotations[cmapi.URISANAnnotationKey] = strings.Join(utilpki.URLsToString(x509Cert.URIs), ",")
}
return nil
}

View File

@ -91,7 +91,8 @@ func NewController(
})
secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{
// Trigger reconciles on changes to the Secret named `spec.secretName`
WorkFunc: certificates.EnqueueCertificatesForSecretNameFunc(log, certificateInformer.Lister(), labels.Everything(), queue),
WorkFunc: certificates.EnqueueCertificatesForSecretNameFunc(log, certificateInformer.Lister(), labels.Everything(),
certificates.WithSecretNamePredicateFunc, queue),
})
// build a list of InformerSynced functions that will be returned by the Register method.
@ -181,9 +182,9 @@ func (c *controller) buildPolicyInputForCertificate(ctx context.Context, crt *cm
// Attempt to fetch the CertificateRequest resource for the current 'status.revision'.
var req *cmapi.CertificateRequest
if crt.Status.Revision != nil {
reqs, err := certificates.ListCertificateRequestsMatchingPredicate(c.certificateRequestLister.CertificateRequests(crt.Namespace),
reqs, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace),
labels.Everything(),
certificates.WithOwnerPredicateFunc(crt),
certificates.WithCertificateRequestOwnerPredicateFunc(crt),
certificates.WithCertificateRevisionPredicateFunc(*crt.Status.Revision),
)
if err != nil {

View File

@ -42,11 +42,12 @@ func CertificateGetFunc(lister cmlisters.CertificateLister) GetFunc {
}
// EnqueueCertificatesForSecretNameFunc will enqueue Certificate resources that
// specify a `spec.secretName` equal to the name of the Secret resource being
// processed.
// satisfy a CertificatePredicateFunc based upon the name of the Secret resource
// being processed.
// This is used to trigger Certificates to reconcile for changes to the Secret
// being managed.
func EnqueueCertificatesForSecretNameFunc(log logr.Logger, lister cmlisters.CertificateLister, selector labels.Selector, queue workqueue.Interface) func(obj interface{}) {
func EnqueueCertificatesForSecretNameFunc(log logr.Logger, lister cmlisters.CertificateLister, selector labels.Selector,
secretNamePredicate WithCertificatePredicateFunc, queue workqueue.Interface) func(obj interface{}) {
return func(obj interface{}) {
s, ok := obj.(*corev1.Secret)
if !ok {
@ -54,7 +55,7 @@ func EnqueueCertificatesForSecretNameFunc(log logr.Logger, lister cmlisters.Cert
return
}
certs, err := ListCertificatesMatchingPredicate(lister.Certificates(s.Namespace), selector, WithSecretNamePredicateFunc(s.Name))
certs, err := ListCertificatesMatchingPredicate(lister.Certificates(s.Namespace), selector, secretNamePredicate(s.Name))
if err != nil {
log.Error(err, "Failed listing Certificate resources")
return
@ -71,6 +72,8 @@ func EnqueueCertificatesForSecretNameFunc(log logr.Logger, lister cmlisters.Cert
}
}
type WithCertificatePredicateFunc func(string) CertificatePredicateFunc
type CertificatePredicateFunc func(*cmapi.Certificate) bool
func WithSecretNamePredicateFunc(name string) CertificatePredicateFunc {
@ -79,6 +82,15 @@ func WithSecretNamePredicateFunc(name string) CertificatePredicateFunc {
}
}
func WithNextPrivateKeySecretNamePredicateFunc(name string) CertificatePredicateFunc {
return func(crt *cmapi.Certificate) bool {
if crt.Status.NextPrivateKeySecretName == nil {
return false
}
return *crt.Status.NextPrivateKeySecretName == name
}
}
func ListCertificatesMatchingPredicate(lister cmlisters.CertificateNamespaceLister, selector labels.Selector, predicate CertificatePredicateFunc) ([]*cmapi.Certificate, error) {
crts, err := lister.List(selector)
if err != nil {
@ -93,10 +105,6 @@ func ListCertificatesMatchingPredicate(lister cmlisters.CertificateNamespaceList
return out, nil
}
const (
CertificateRevisionAnnotationKey = "cert-manager.io/certificate-revision"
)
type CertificateRequestPredicateFunc func(*cmapi.CertificateRequest) bool
func WithCertificateRevisionPredicateFunc(revision int) CertificateRequestPredicateFunc {
@ -104,17 +112,17 @@ func WithCertificateRevisionPredicateFunc(revision int) CertificateRequestPredic
if req.Annotations == nil {
return false
}
return req.Annotations[CertificateRevisionAnnotationKey] == fmt.Sprintf("%d", revision)
return req.Annotations[cmapi.CertificateRequestRevisionAnnotationKey] == fmt.Sprintf("%d", revision)
}
}
func WithOwnerPredicateFunc(owner metav1.Object) CertificateRequestPredicateFunc {
func WithCertificateRequestOwnerPredicateFunc(owner metav1.Object) CertificateRequestPredicateFunc {
return func(req *cmapi.CertificateRequest) bool {
return metav1.IsControlledBy(req, owner)
}
}
func ListCertificateRequestsMatchingPredicate(lister cmlisters.CertificateRequestNamespaceLister, selector labels.Selector, predicates ...CertificateRequestPredicateFunc) ([]*cmapi.CertificateRequest, error) {
func ListCertificateRequestsMatchingPredicates(lister cmlisters.CertificateRequestNamespaceLister, selector labels.Selector, predicates ...CertificateRequestPredicateFunc) ([]*cmapi.CertificateRequest, error) {
reqs, err := lister.List(selector)
if err != nil {
return nil, err
@ -132,6 +140,7 @@ func ListCertificateRequestsMatchingPredicate(lister cmlisters.CertificateReques
out = append(out, req)
}
}
return out, nil
}

View File

@ -30,6 +30,9 @@ const (
// Annotation names for CertificateRequests
const (
CRPrivateKeyAnnotationKey = "cert-manager.io/private-key-secret-name"
// Annotation to declare the CertificateRequest "revision", beloning to a Certificate Resource
CertificateRequestRevisionAnnotationKey = "cert-manager.io/certificate-revision"
)
const (