ContainerImage.Pinniped/internal/controller/apicerts/certs_manager.go
Ryan Richard c82f568b2c certauthority.go: Refactor issuing client versus server certs
We were previously issuing both client certs and server certs with
both extended key usages included. Split the Issue*() methods into
separate methods for issuing server certs versus client certs so
they can have different extended key usages tailored for each use
case.

Also took the opportunity to clean up the parameters of the Issue*()
methods and New() methods to more closely match how we prefer to call
them. We were always only passing the common name part of the
pkix.Name to New(), so now the New() method just takes the common name
as a string. When making a server cert, we don't need to set the
deprecated common name field, so remove that param. When making a client
cert, we're always making it in the format expected by the Kube API
server, so just accept the username and group as parameters directly.
2021-03-12 16:09:37 -08:00

145 lines
4.8 KiB
Go

// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package apicerts
import (
"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 (
CACertificateSecretKey = "caCertificate"
CACertificatePrivateKeySecretKey = "caCertificatePrivateKey"
tlsPrivateKeySecretKey = "tlsPrivateKey"
TLSCertificateChainSecretKey = "tlsCertificateChain"
)
type certsManagerController struct {
namespace string
certsSecretResourceName string
certsSecretLabels map[string]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,
certsSecretResourceName string,
certsSecretLabels map[string]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,
certsSecretResourceName: certsSecretResourceName,
certsSecretLabels: certsSecretLabels,
k8sClient: k8sClient,
secretInformer: secretInformer,
certDuration: certDuration,
generatedCACommonName: generatedCACommonName,
serviceNameForGeneratedCertCommonName: serviceNameForGeneratedCertCommonName,
},
},
withInformer(
secretInformer,
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(certsSecretResourceName, 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: certsSecretResourceName,
}),
)
}
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(c.certsSecretResourceName)
notFound := k8serrors.IsNotFound(err)
if err != nil && !notFound {
return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err)
}
if !notFound {
// The secret already exists, so nothing to do.
return nil
}
// Create a CA.
ca, err := certauthority.New(c.generatedCACommonName, c.certDuration)
if err != nil {
return fmt.Errorf("could not initialize CA: %w", err)
}
caPrivateKeyPEM, err := ca.PrivateKeyToPEM()
if err != nil {
return fmt.Errorf("could not get CA private key: %w", err)
}
secret := corev1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: c.certsSecretResourceName,
Namespace: c.namespace,
Labels: c.certsSecretLabels,
},
StringData: map[string]string{
CACertificateSecretKey: string(ca.Bundle()),
CACertificatePrivateKeySecretKey: string(caPrivateKeyPEM),
},
}
// Using the CA from above, create a TLS server cert if we have service name.
if len(c.serviceNameForGeneratedCertCommonName) != 0 {
serviceEndpoint := c.serviceNameForGeneratedCertCommonName + "." + c.namespace + ".svc"
tlsCert, err := ca.IssueServerCert([]string{serviceEndpoint}, nil, 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(tlsCert)
if err != nil {
return fmt.Errorf("could not PEM encode serving certificate: %w", err)
}
secret.StringData[tlsPrivateKeySecretKey] = string(tlsPrivateKeyPEM)
secret.StringData[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
}