mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-23 22:46:59 +00:00
* chore: save progress Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
710 lines
20 KiB
Go
710 lines
20 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
//nolint:nestif
|
|
package tls
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/go-logr/logr"
|
|
"golang.org/x/sync/errgroup"
|
|
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/util/retry"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/builder"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
"github.com/projectcapsule/capsule/internal/controllers/utils"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/cert"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/predicates"
|
|
)
|
|
|
|
const (
|
|
certificateExpirationThreshold = 3 * 24 * time.Hour
|
|
certificateValidity = 6 * 30 * 24 * time.Hour
|
|
)
|
|
|
|
type Reconciler struct {
|
|
client.Client
|
|
|
|
Log logr.Logger
|
|
Scheme *runtime.Scheme
|
|
Namespace string
|
|
Configuration configuration.Configuration
|
|
}
|
|
|
|
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
|
|
enqueueFn := handler.EnqueueRequestsFromMapFunc(func(context.Context, client.Object) []reconcile.Request {
|
|
return []reconcile.Request{
|
|
{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: configuration.ControllerNamespace(),
|
|
Name: r.Configuration.TLSSecretName(),
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
For(
|
|
&corev1.Secret{},
|
|
builder.WithPredicates(
|
|
predicates.NamesMatchingPredicate{
|
|
Names: []string{r.Configuration.TLSSecretName()},
|
|
},
|
|
),
|
|
).
|
|
Named("capsule/tls").
|
|
WatchesMetadata(
|
|
&admissionregistrationv1.ValidatingWebhookConfiguration{},
|
|
enqueueFn,
|
|
builder.WithPredicates(
|
|
predicates.NamesMatchingPredicate{
|
|
Names: []string{string(r.Configuration.Admission().Validating.Name)},
|
|
},
|
|
),
|
|
).
|
|
WatchesMetadata(
|
|
&admissionregistrationv1.MutatingWebhookConfiguration{},
|
|
enqueueFn,
|
|
builder.WithPredicates(
|
|
predicates.NamesMatchingPredicate{
|
|
Names: []string{string(r.Configuration.Admission().Mutating.Name)},
|
|
},
|
|
),
|
|
).
|
|
WatchesMetadata(
|
|
&apiextensionsv1.CustomResourceDefinition{},
|
|
enqueueFn,
|
|
builder.WithPredicates(
|
|
predicates.NamesMatchingPredicate{
|
|
Names: r.managedCRDNames(),
|
|
},
|
|
),
|
|
).
|
|
WithOptions(ctrlConfig.Runtime.ToControllerOptions()).
|
|
Complete(r)
|
|
}
|
|
|
|
func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
|
|
log := r.Log.WithValues(
|
|
"Request.Namespace", request.Namespace,
|
|
"Request.Name", request.Name,
|
|
)
|
|
|
|
if request.Namespace == "" {
|
|
request.Namespace = r.Namespace
|
|
}
|
|
|
|
if request.Name == "" {
|
|
request.Name = r.Configuration.TLSSecretName()
|
|
}
|
|
|
|
log.V(4).Info("TLS reconciliation started")
|
|
|
|
certSecret := &corev1.Secret{}
|
|
if err := r.Get(ctx, request.NamespacedName, certSecret); err != nil {
|
|
if !apierrors.IsNotFound(err) {
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
certSecret = &corev1.Secret{}
|
|
certSecret.Name = request.Name
|
|
certSecret.Namespace = request.Namespace
|
|
certSecret.Data = map[string][]byte{}
|
|
}
|
|
|
|
if err := r.ReconcileCertificates(ctx, log, certSecret); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
servingCert, err := cert.GetCertificateFromBytes(certSecret.Data[corev1.TLSCertKey])
|
|
if err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
requeueTime := servingCert.NotAfter.Add(-(certificateExpirationThreshold - time.Second))
|
|
|
|
requeueAfter := max(time.Until(requeueTime), 0)
|
|
|
|
log.V(4).Info("TLS reconciliation completed", "requeueAfter", requeueAfter.String())
|
|
|
|
return ctrl.Result{
|
|
Requeue: true,
|
|
RequeueAfter: requeueAfter,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Reconciler) ReconcileCertificates(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
certSecret *corev1.Secret,
|
|
) error {
|
|
sans, err := r.desiredWebhookSANs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.V(4).Info(
|
|
"Resolved desired webhook certificate SANs",
|
|
"dnsNames", sans.DNSNames,
|
|
"ipAddresses", cert.IPsToStrings(sans.IPAddrs),
|
|
)
|
|
|
|
if err := r.reconcileTLSSecret(ctx, log, certSecret, sans); err != nil {
|
|
return err
|
|
}
|
|
|
|
caBundle := certSecret.Data[corev1.ServiceAccountRootCAKey]
|
|
if len(caBundle) == 0 {
|
|
return fmt.Errorf("missing %q field in %q secret", corev1.ServiceAccountRootCAKey, r.Configuration.TLSSecretName())
|
|
}
|
|
|
|
log.V(5).Info("Patching caBundle in admission webhooks and managed CRD conversions")
|
|
|
|
patchGroup, groupCtx := errgroup.WithContext(ctx)
|
|
patchGroup.Go(func() error {
|
|
return r.patchMutatingWebhookConfigurationCABundle(groupCtx, caBundle)
|
|
})
|
|
patchGroup.Go(func() error {
|
|
return r.patchValidatingWebhookConfigurationCABundle(groupCtx, caBundle)
|
|
})
|
|
|
|
for key, managed := range r.conversionManagedCRDs() {
|
|
patchGroup.Go(func() error {
|
|
if err := r.updateManagedCustomResourceDefinition(groupCtx, managed, caBundle); err != nil {
|
|
return fmt.Errorf("cannot update managed CRD %q (%s): %w", key, managed.Name, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
return patchGroup.Wait()
|
|
}
|
|
|
|
// reconcileTLSSecret calculates certificate material from the latest persisted
|
|
// Secret inside an optimistic-concurrency retry. This is deliberately not based
|
|
// on the object supplied by the caller: every controller replica performs the
|
|
// startup reconciliation before leader election, so that object may already be
|
|
// stale by the time certificate generation finishes.
|
|
func (r *Reconciler) reconcileTLSSecret(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
certSecret *corev1.Secret,
|
|
sans cert.CertificateSANs,
|
|
) error {
|
|
key := client.ObjectKeyFromObject(certSecret)
|
|
if key.Name == "" {
|
|
key.Name = r.Configuration.TLSSecretName()
|
|
}
|
|
|
|
if key.Namespace == "" {
|
|
key.Namespace = r.Namespace
|
|
}
|
|
|
|
err := retry.OnError(retry.DefaultBackoff, func(err error) bool {
|
|
return apierrors.IsAlreadyExists(err) || apierrors.IsConflict(err)
|
|
}, func() error {
|
|
// Use the TLS type when Capsule creates the Secret itself. If the Secret
|
|
// already exists, CreateOrUpdate loads its persisted type into desired;
|
|
// preserve it because Secret type is immutable. In particular, the Helm
|
|
// chart pre-creates an empty Opaque Secret so the controller Pod can mount
|
|
// it before this reconciliation supplies the certificate data.
|
|
desired := &corev1.Secret{
|
|
ObjectMeta: *certSecret.ObjectMeta.DeepCopy(),
|
|
Type: corev1.SecretTypeTLS,
|
|
}
|
|
desired.Name = key.Name
|
|
desired.Namespace = key.Namespace
|
|
|
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, desired, func() error {
|
|
if desired.Labels == nil {
|
|
desired.Labels = map[string]string{}
|
|
}
|
|
|
|
if desired.Annotations == nil {
|
|
desired.Annotations = map[string]string{}
|
|
}
|
|
|
|
ca, _, rotateServingCert, err := r.ensureCertificateMaterial(log, desired, sans)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.V(4).Info(
|
|
"certificate requires rotation",
|
|
"rotation", rotateServingCert,
|
|
)
|
|
|
|
if rotateServingCert {
|
|
if ca == nil {
|
|
return fmt.Errorf("cannot rotate serving certificate without CA private key")
|
|
}
|
|
|
|
crt, key, err := ca.GenerateCertificate(cert.NewCertOpts(
|
|
time.Now().Add(certificateValidity),
|
|
sans,
|
|
))
|
|
if err != nil {
|
|
return fmt.Errorf("generate serving TLS certificate: %w", err)
|
|
}
|
|
|
|
desired.Data[corev1.TLSCertKey] = crt.Bytes()
|
|
desired.Data[corev1.TLSPrivateKeyKey] = key.Bytes()
|
|
}
|
|
|
|
return r.validateSecretCertificate(desired, sans)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
certSecret.ObjectMeta = desired.ObjectMeta
|
|
certSecret.Type = desired.Type
|
|
certSecret.Immutable = desired.Immutable
|
|
certSecret.Data = copySecretData(desired.Data)
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
r.Log.Error(err, "cannot reconcile Capsule TLS Secret", "secret", key.String())
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// ensureCertificateMaterial ensures that the Secret contains a stable CA
|
|
// certificate/key pair and decides whether the serving certificate must be
|
|
// regenerated.
|
|
//
|
|
// Important behavior:
|
|
// - Only a new, empty Secret bootstraps a CA.
|
|
// - Existing valid CA is reused.
|
|
// - Serving certificate renewal never rotates the CA.
|
|
// - Established or externally managed CA material is never replaced
|
|
// automatically, because doing so would immediately invalidate every
|
|
// published caBundle.
|
|
func (r *Reconciler) ensureCertificateMaterial(
|
|
log logr.Logger,
|
|
certSecret *corev1.Secret,
|
|
sans cert.CertificateSANs,
|
|
) (*cert.CapsuleCA, []byte, bool, error) {
|
|
sans = sans.Normalize()
|
|
|
|
if sans.Empty() {
|
|
return nil, nil, false, fmt.Errorf("cannot ensure TLS material without SANs")
|
|
}
|
|
|
|
if certSecret.Data == nil {
|
|
certSecret.Data = map[string][]byte{}
|
|
}
|
|
|
|
caBundle := certSecret.Data[corev1.ServiceAccountRootCAKey]
|
|
caKey := certSecret.Data["ca.key"]
|
|
|
|
hasCABundle := len(caBundle) > 0
|
|
hasCAKey := len(caKey) > 0
|
|
|
|
var ca *cert.CapsuleCA
|
|
|
|
rotateServingCert := false
|
|
|
|
switch {
|
|
case hasCABundle && hasCAKey:
|
|
loadedCA, err := cert.NewCertificateAuthorityFromBytes(caBundle, caKey)
|
|
if err != nil {
|
|
return nil, nil, false, fmt.Errorf(
|
|
"TLS Secret %s contains invalid CA certificate/key material; refusing automatic CA replacement: %w",
|
|
client.ObjectKeyFromObject(certSecret).String(),
|
|
err,
|
|
)
|
|
}
|
|
|
|
ca = loadedCA
|
|
|
|
case hasCABundle && !hasCAKey:
|
|
// This is an externally managed or legacy Secret. It is safe to keep
|
|
// serving while its certificate remains valid, but Capsule cannot renew
|
|
// it without the CA key. Replacing that CA in-place would make the API
|
|
// server distrust one or more running webhook replicas during rollout.
|
|
if err := r.validateSecretCertificate(certSecret, sans); err != nil {
|
|
return nil, nil, false, fmt.Errorf(
|
|
"TLS Secret %s has no CA private key and its serving certificate needs renewal; refusing automatic CA replacement: %w",
|
|
client.ObjectKeyFromObject(certSecret).String(),
|
|
err,
|
|
)
|
|
}
|
|
|
|
log.V(3).Info(
|
|
"Keeping externally managed TLS material without a CA private key",
|
|
"secret", client.ObjectKeyFromObject(certSecret).String(),
|
|
)
|
|
|
|
return nil, caBundle, false, nil
|
|
|
|
case !hasCABundle && hasCAKey:
|
|
return nil, nil, false, fmt.Errorf(
|
|
"TLS Secret %s contains a CA private key but no CA certificate; refusing automatic CA replacement",
|
|
client.ObjectKeyFromObject(certSecret).String(),
|
|
)
|
|
|
|
default:
|
|
if len(certSecret.Data[corev1.TLSCertKey]) > 0 || len(certSecret.Data[corev1.TLSPrivateKeyKey]) > 0 {
|
|
return nil, nil, false, fmt.Errorf(
|
|
"TLS Secret %s contains serving certificate material but no CA certificate; refusing automatic CA replacement",
|
|
client.ObjectKeyFromObject(certSecret).String(),
|
|
)
|
|
}
|
|
|
|
log.V(10).Info(
|
|
"TLS Secret is empty, generating initial CA",
|
|
"secret", client.ObjectKeyFromObject(certSecret).String(),
|
|
)
|
|
|
|
generatedCA, generatedCABundle, generatedCAKey, err := generateCertificateAuthorityMaterial()
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
|
|
ca = generatedCA
|
|
caBundle = generatedCABundle
|
|
|
|
certSecret.Data[corev1.ServiceAccountRootCAKey] = generatedCABundle
|
|
certSecret.Data["ca.key"] = generatedCAKey
|
|
|
|
rotateServingCert = true
|
|
}
|
|
|
|
servingCertPEM := certSecret.Data[corev1.TLSCertKey]
|
|
servingKeyPEM := certSecret.Data[corev1.TLSPrivateKeyKey]
|
|
|
|
if len(servingCertPEM) == 0 || len(servingKeyPEM) == 0 {
|
|
log.V(10).Info(
|
|
"TLS Secret is missing serving certificate material, rotating serving certificate",
|
|
"secret", client.ObjectKeyFromObject(certSecret).String(),
|
|
)
|
|
|
|
rotateServingCert = true
|
|
} else {
|
|
servingCert, err := cert.GetCertificateFromBytes(servingCertPEM)
|
|
if err != nil {
|
|
log.V(10).Info(
|
|
"Failed to parse serving certificate, rotating serving certificate",
|
|
"secret", client.ObjectKeyFromObject(certSecret).String(),
|
|
"error", err.Error(),
|
|
)
|
|
|
|
rotateServingCert = true
|
|
} else {
|
|
if time.Until(servingCert.NotAfter) <= certificateExpirationThreshold {
|
|
log.V(10).Info(
|
|
"Serving certificate is close to expiry, rotating serving certificate",
|
|
"secret", client.ObjectKeyFromObject(certSecret).String(),
|
|
"notAfter", servingCert.NotAfter,
|
|
)
|
|
|
|
rotateServingCert = true
|
|
}
|
|
|
|
if !sans.MatchesCertificate(servingCert) {
|
|
log.V(3).Info(
|
|
"Serving certificate SANs differ from desired SANs, rotating serving certificate",
|
|
"secret", client.ObjectKeyFromObject(certSecret).String(),
|
|
"desiredDNSNames", sans.DNSNames,
|
|
"desiredIPAddresses", cert.IPsToStrings(sans.IPAddrs),
|
|
"currentDNSNames", servingCert.DNSNames,
|
|
"currentIPAddresses", cert.IPsToStrings(servingCert.IPAddresses),
|
|
)
|
|
|
|
rotateServingCert = true
|
|
}
|
|
|
|
if err := r.validateSecretCertificate(certSecret, sans); err != nil {
|
|
log.V(10).Info(
|
|
"Serving certificate failed validation, rotating serving certificate",
|
|
"secret", client.ObjectKeyFromObject(certSecret).String(),
|
|
"error", err.Error(),
|
|
)
|
|
|
|
rotateServingCert = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if rotateServingCert && ca == nil {
|
|
return nil, nil, false, fmt.Errorf(
|
|
"cannot rotate serving certificate for TLS Secret %s without CA private key",
|
|
client.ObjectKeyFromObject(certSecret).String(),
|
|
)
|
|
}
|
|
|
|
return ca, caBundle, rotateServingCert, nil
|
|
}
|
|
|
|
func generateCertificateAuthorityMaterial() (*cert.CapsuleCA, []byte, []byte, error) {
|
|
ca, err := cert.GenerateCertificateAuthority()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
caCrt, err := ca.CACertificatePem()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
caKey, err := ca.CAPrivateKeyPem()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
return ca, caCrt.Bytes(), caKey.Bytes(), nil
|
|
}
|
|
|
|
func (r *Reconciler) patchMutatingWebhookConfigurationCABundle(ctx context.Context, caBundle []byte) error {
|
|
return r.patchAdmissionConfigurationCABundle(
|
|
ctx,
|
|
r.Configuration.MutatingWebhookConfigurationName(),
|
|
caBundle,
|
|
func() client.Object { return &admissionregistrationv1.MutatingWebhookConfiguration{} },
|
|
updateMutatingWebhookCABundles,
|
|
)
|
|
}
|
|
|
|
func (r *Reconciler) patchValidatingWebhookConfigurationCABundle(ctx context.Context, caBundle []byte) error {
|
|
return r.patchAdmissionConfigurationCABundle(
|
|
ctx,
|
|
r.Configuration.ValidatingWebhookConfigurationName(),
|
|
caBundle,
|
|
func() client.Object { return &admissionregistrationv1.ValidatingWebhookConfiguration{} },
|
|
updateValidatingWebhookCABundles,
|
|
)
|
|
}
|
|
|
|
func (r *Reconciler) patchAdmissionConfigurationCABundle(
|
|
ctx context.Context,
|
|
name string,
|
|
caBundle []byte,
|
|
newConfiguration func() client.Object,
|
|
updateCABundles func(client.Object, []byte) (bool, error),
|
|
) error {
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
|
|
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
|
configuration := newConfiguration()
|
|
if err := r.Get(ctx, types.NamespacedName{Name: name}, configuration); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
before, ok := configuration.DeepCopyObject().(client.Object)
|
|
if !ok {
|
|
return fmt.Errorf("admission configuration %q cannot be deep-copied as a client object", name)
|
|
}
|
|
|
|
changed, err := updateCABundles(configuration, caBundle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !changed {
|
|
return nil
|
|
}
|
|
|
|
return r.Patch(ctx, configuration, client.MergeFrom(before))
|
|
})
|
|
}
|
|
|
|
func updateMutatingWebhookCABundles(object client.Object, caBundle []byte) (bool, error) {
|
|
configuration, ok := object.(*admissionregistrationv1.MutatingWebhookConfiguration)
|
|
if !ok {
|
|
return false, fmt.Errorf("expected MutatingWebhookConfiguration, got %T", object)
|
|
}
|
|
|
|
changed := false
|
|
|
|
for index := range configuration.Webhooks {
|
|
if bytes.Equal(configuration.Webhooks[index].ClientConfig.CABundle, caBundle) {
|
|
continue
|
|
}
|
|
|
|
configuration.Webhooks[index].ClientConfig.CABundle = append([]byte(nil), caBundle...)
|
|
changed = true
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
func updateValidatingWebhookCABundles(object client.Object, caBundle []byte) (bool, error) {
|
|
configuration, ok := object.(*admissionregistrationv1.ValidatingWebhookConfiguration)
|
|
if !ok {
|
|
return false, fmt.Errorf("expected ValidatingWebhookConfiguration, got %T", object)
|
|
}
|
|
|
|
changed := false
|
|
|
|
for index := range configuration.Webhooks {
|
|
if bytes.Equal(configuration.Webhooks[index].ClientConfig.CABundle, caBundle) {
|
|
continue
|
|
}
|
|
|
|
configuration.Webhooks[index].ClientConfig.CABundle = append([]byte(nil), caBundle...)
|
|
changed = true
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
func (r *Reconciler) validateSecretCertificate(
|
|
certSecret *corev1.Secret,
|
|
sans cert.CertificateSANs,
|
|
) error {
|
|
caPEM := certSecret.Data[corev1.ServiceAccountRootCAKey]
|
|
if len(caPEM) == 0 {
|
|
return fmt.Errorf("missing %q in TLS Secret %s/%s",
|
|
corev1.ServiceAccountRootCAKey,
|
|
certSecret.Namespace,
|
|
certSecret.Name,
|
|
)
|
|
}
|
|
|
|
roots := x509.NewCertPool()
|
|
if ok := roots.AppendCertsFromPEM(caPEM); !ok {
|
|
return fmt.Errorf("failed to parse %q in TLS Secret %s/%s",
|
|
corev1.ServiceAccountRootCAKey,
|
|
certSecret.Namespace,
|
|
certSecret.Name,
|
|
)
|
|
}
|
|
|
|
leaf, err := cert.GetCertificateFromBytes(certSecret.Data[corev1.TLSCertKey])
|
|
if err != nil {
|
|
return fmt.Errorf("parse serving certificate from TLS Secret %s/%s: %w",
|
|
certSecret.Namespace,
|
|
certSecret.Name,
|
|
err,
|
|
)
|
|
}
|
|
|
|
keyPEM := certSecret.Data[corev1.TLSPrivateKeyKey]
|
|
if len(keyPEM) == 0 {
|
|
return fmt.Errorf("missing %q in TLS Secret %s/%s",
|
|
corev1.TLSPrivateKeyKey,
|
|
certSecret.Namespace,
|
|
certSecret.Name,
|
|
)
|
|
}
|
|
|
|
key, err := cert.GetPrivateKeyFromBytes(keyPEM)
|
|
if err != nil {
|
|
return fmt.Errorf("parse serving private key from TLS Secret %s/%s: %w",
|
|
certSecret.Namespace,
|
|
certSecret.Name,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if err := cert.ValidateCertificate(leaf, key, certificateExpirationThreshold); err != nil {
|
|
return fmt.Errorf("serving certificate/key pair is invalid or expiring: %w", err)
|
|
}
|
|
|
|
normalized := sans.Normalize()
|
|
if normalized.Empty() {
|
|
return fmt.Errorf("cannot validate serving certificate without desired SANs")
|
|
}
|
|
|
|
for _, dnsName := range normalized.DNSNames {
|
|
if _, err := leaf.Verify(x509.VerifyOptions{
|
|
DNSName: dnsName,
|
|
Roots: roots,
|
|
KeyUsages: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
},
|
|
}); err != nil {
|
|
return fmt.Errorf("serving certificate is not valid for DNS SAN %q: %w", dnsName, err)
|
|
}
|
|
}
|
|
|
|
for _, ip := range normalized.IPAddrs {
|
|
if _, err := leaf.Verify(x509.VerifyOptions{
|
|
DNSName: ip.String(),
|
|
Roots: roots,
|
|
KeyUsages: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
},
|
|
}); err != nil {
|
|
return fmt.Errorf("serving certificate is not valid for IP SAN %q: %w", ip.String(), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Reconciler) updateManagedCustomResourceDefinition(
|
|
ctx context.Context,
|
|
managed ManagedCRD,
|
|
caBundle []byte,
|
|
) error {
|
|
if !managed.ManageConversion {
|
|
return nil
|
|
}
|
|
|
|
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
|
crd := &apiextensionsv1.CustomResourceDefinition{}
|
|
if err := r.Get(ctx, types.NamespacedName{Name: managed.Name}, crd); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Only patch CRDs that already use webhook conversion.
|
|
if crd.Spec.Conversion == nil ||
|
|
crd.Spec.Conversion.Webhook == nil ||
|
|
crd.Spec.Conversion.Webhook.ClientConfig == nil {
|
|
return nil
|
|
}
|
|
|
|
current := crd.Spec.Conversion.Webhook.ClientConfig.CABundle
|
|
if bytes.Equal(current, caBundle) {
|
|
return nil
|
|
}
|
|
|
|
before := crd.DeepCopy()
|
|
|
|
crd.Spec.Conversion.Webhook.ClientConfig.CABundle = append([]byte(nil), caBundle...)
|
|
|
|
return r.Patch(ctx, crd, client.MergeFrom(before))
|
|
})
|
|
}
|
|
|
|
func copySecretData(in map[string][]byte) map[string][]byte {
|
|
out := make(map[string][]byte, len(in))
|
|
|
|
for key, value := range in {
|
|
out[key] = append([]byte(nil), value...)
|
|
}
|
|
|
|
return out
|
|
}
|