ContainerImage.Pinniped/internal/controller/apicerts/certs_manager.go
Andrew Keesler 92a6b7f4a4
Use same lifetime for serving cert and CA cert
So that operators won't look at the lifetime of the CA cert and be
like, "wtf, why does the serving cert have the lifetime that I
specified, but its CA cert is valid for 100 years".

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2020-08-27 15:59:47 -04:00

140 lines
4.5 KiB
Go

/*
Copyright 2020 VMware, Inc.
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"
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
"github.com/suzerain-io/controller-go"
"github.com/suzerain-io/pinniped/internal/certauthority"
pinnipedcontroller "github.com/suzerain-io/pinniped/internal/controller"
)
const (
//nolint: gosec
certsSecretName = "api-serving-cert"
caCertificateSecretKey = "caCertificate"
tlsPrivateKeySecretKey = "tlsPrivateKey"
tlsCertificateChainSecretKey = "tlsCertificateChain"
)
type certsManagerController struct {
namespace string
k8sClient kubernetes.Interface
aggregatorClient aggregatorclient.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
}
func NewCertsManagerController(
namespace string,
k8sClient kubernetes.Interface,
aggregatorClient aggregatorclient.Interface,
secretInformer corev1informers.SecretInformer,
withInformer pinnipedcontroller.WithInformerOptionFunc,
withInitialEvent pinnipedcontroller.WithInitialEventOptionFunc,
certDuration time.Duration,
) controller.Controller {
return controller.New(
controller.Config{
Name: "certs-manager-controller",
Syncer: &certsManagerController{
namespace: namespace,
k8sClient: k8sClient,
aggregatorClient: aggregatorClient,
secretInformer: secretInformer,
certDuration: certDuration,
},
},
withInformer(
secretInformer,
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(certsSecretName, namespace),
controller.InformerOption{},
),
// Be sure to run once even if the Secret that the informer is watching doesn't exist.
withInitialEvent(controller.Key{
Namespace: namespace,
Name: certsSecretName,
}),
)
}
func (c *certsManagerController) Sync(ctx controller.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: "Pinniped CA"}, c.certDuration)
if err != nil {
return fmt.Errorf("could not initialize CA: %w", err)
}
// This string must match the name of the Service declared in the deployment yaml.
const serviceName = "pinniped-api"
// Using the CA from above, create a TLS server cert for the aggregated API server to use.
serviceEndpoint := serviceName + "." + 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)
}
// Update the APIService to give it the new CA bundle.
if err := UpdateAPIService(ctx.Context, c.aggregatorClient, aggregatedAPIServerCA.Bundle()); err != nil {
return fmt.Errorf("could not update the API service: %w", err)
}
klog.Info("certsManagerController Sync successfully created secret and updated API service")
return nil
}