// Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package apicerts import ( "crypto/x509/pkix" "fmt" "time" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "go.pinniped.dev/internal/certauthority" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" ) const ( //nolint: gosec certsSecretName = "api-serving-cert" caCertificateSecretKey = "caCertificate" tlsPrivateKeySecretKey = "tlsPrivateKey" tlsCertificateChainSecretKey = "tlsCertificateChain" ) type certsManagerController struct { namespace string k8sClient kubernetes.Interface secretInformer corev1informers.SecretInformer // certDuration is the lifetime of both the serving certificate and its CA // certificate that this controller will use when issuing the certificates. certDuration time.Duration generatedCACommonName string serviceNameForGeneratedCertCommonName string } func NewCertsManagerController(namespace string, k8sClient kubernetes.Interface, secretInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, withInitialEvent pinnipedcontroller.WithInitialEventOptionFunc, certDuration time.Duration, generatedCACommonName string, serviceNameForGeneratedCertCommonName string, ) controllerlib.Controller { return controllerlib.New( controllerlib.Config{ Name: "certs-manager-controller", Syncer: &certsManagerController{ namespace: namespace, k8sClient: k8sClient, secretInformer: secretInformer, certDuration: certDuration, generatedCACommonName: generatedCACommonName, serviceNameForGeneratedCertCommonName: serviceNameForGeneratedCertCommonName, }, }, withInformer( secretInformer, pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(certsSecretName, namespace), controllerlib.InformerOption{}, ), // Be sure to run once even if the Secret that the informer is watching doesn't exist. withInitialEvent(controllerlib.Key{ Namespace: namespace, Name: certsSecretName, }), ) } func (c *certsManagerController) Sync(ctx controllerlib.Context) error { // Try to get the secret from the informer cache. _, err := c.secretInformer.Lister().Secrets(c.namespace).Get(certsSecretName) notFound := k8serrors.IsNotFound(err) if err != nil && !notFound { return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, certsSecretName, err) } if !notFound { // The secret already exists, so nothing to do. return nil } // Create a CA. aggregatedAPIServerCA, err := certauthority.New(pkix.Name{CommonName: c.generatedCACommonName}, c.certDuration) if err != nil { return fmt.Errorf("could not initialize CA: %w", err) } // Using the CA from above, create a TLS server cert for the aggregated API server to use. serviceEndpoint := c.serviceNameForGeneratedCertCommonName + "." + c.namespace + ".svc" aggregatedAPIServerTLSCert, err := aggregatedAPIServerCA.Issue( pkix.Name{CommonName: serviceEndpoint}, []string{serviceEndpoint}, c.certDuration, ) if err != nil { return fmt.Errorf("could not issue serving certificate: %w", err) } // Write the CA's public key bundle and the serving certs to a secret. tlsCertChainPEM, tlsPrivateKeyPEM, err := certauthority.ToPEM(aggregatedAPIServerTLSCert) if err != nil { return fmt.Errorf("could not PEM encode serving certificate: %w", err) } secret := corev1.Secret{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: certsSecretName, Namespace: c.namespace, }, StringData: map[string]string{ caCertificateSecretKey: string(aggregatedAPIServerCA.Bundle()), tlsPrivateKeySecretKey: string(tlsPrivateKeyPEM), tlsCertificateChainSecretKey: string(tlsCertChainPEM), }, } _, err = c.k8sClient.CoreV1().Secrets(c.namespace).Create(ctx.Context, &secret, metav1.CreateOptions{}) if err != nil { return fmt.Errorf("could not create secret: %w", err) } klog.Info("certsManagerController Sync successfully created secret") return nil }