c82f568b2c
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.
145 lines
4.8 KiB
Go
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
|
|
}
|