38802c2184
- Setting a Secret in the supervisor's namespace with a special name will cause it to get picked up and served as the supervisor's TLS cert for any request which does not have a matching SNI cert. - This is especially useful for when there is no DNS record for an issuer and the user will be accessing it via IP address. This is not how we would expect it to be used in production, but it might be useful for other cases. - Includes a new integration test - Also suppress all of the warnings about ignoring the error returned by Close() in lines like `defer x.Close()` to make GoLand happier
139 lines
4.6 KiB
Go
139 lines
4.6 KiB
Go
// 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 (
|
|
caCertificateSecretKey = "caCertificate"
|
|
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.
|
|
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},
|
|
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(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: c.certsSecretResourceName,
|
|
Namespace: c.namespace,
|
|
Labels: c.certsSecretLabels,
|
|
},
|
|
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
|
|
}
|