build: move e2e dependencies into e2e/go.mod

Several packages are only used while running the e2e suite. These
packages are less important to update, as the they can not influence the
final executable that is part of the Ceph-CSI container-image.

By moving these dependencies out of the main Ceph-CSI go.mod, it is
easier to identify if a reported CVE affects Ceph-CSI, or only the
testing (like most of the Kubernetes CVEs).

Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
Niels de Vos
2025-03-04 08:57:28 +01:00
committed by mergify[bot]
parent 15da101b1b
commit bec6090996
8047 changed files with 1407827 additions and 3453 deletions

View File

@ -0,0 +1,59 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"bytes"
)
// certKeyContent holds the content for the cert and key
type certKeyContent struct {
cert []byte
key []byte
}
func (c *certKeyContent) Equal(rhs *certKeyContent) bool {
if c == nil || rhs == nil {
return c == rhs
}
return bytes.Equal(c.key, rhs.key) && bytes.Equal(c.cert, rhs.cert)
}
// sniCertKeyContent holds the content for the cert and key as well as any explicit names
type sniCertKeyContent struct {
certKeyContent
sniNames []string
}
func (c *sniCertKeyContent) Equal(rhs *sniCertKeyContent) bool {
if c == nil || rhs == nil {
return c == rhs
}
if len(c.sniNames) != len(rhs.sniNames) {
return false
}
for i := range c.sniNames {
if c.sniNames[i] != rhs.sniNames[i] {
return false
}
}
return c.certKeyContent.Equal(&rhs.certKeyContent)
}

View File

@ -0,0 +1,69 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"bytes"
)
// dynamicCertificateContent holds the content that overrides the baseTLSConfig
type dynamicCertificateContent struct {
// clientCA holds the content for the clientCA bundle
clientCA caBundleContent
servingCert certKeyContent
sniCerts []sniCertKeyContent
}
// caBundleContent holds the content for the clientCA bundle. Wrapping the bytes makes the Equals work nicely with the
// method receiver.
type caBundleContent struct {
caBundle []byte
}
func (c *dynamicCertificateContent) Equal(rhs *dynamicCertificateContent) bool {
if c == nil || rhs == nil {
return c == rhs
}
if !c.clientCA.Equal(&rhs.clientCA) {
return false
}
if !c.servingCert.Equal(&rhs.servingCert) {
return false
}
if len(c.sniCerts) != len(rhs.sniCerts) {
return false
}
for i := range c.sniCerts {
if !c.sniCerts[i].Equal(&rhs.sniCerts[i]) {
return false
}
}
return true
}
func (c *caBundleContent) Equal(rhs *caBundleContent) bool {
if c == nil || rhs == nil {
return c == rhs
}
return bytes.Equal(c.caBundle, rhs.caBundle)
}

View File

@ -0,0 +1,277 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"bytes"
"context"
"crypto/x509"
"fmt"
"sync/atomic"
"time"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
// ConfigMapCAController provies a CAContentProvider that can dynamically react to configmap changes
// It also fulfills the authenticator interface to provide verifyoptions
type ConfigMapCAController struct {
name string
configmapLister corev1listers.ConfigMapLister
configmapNamespace string
configmapName string
configmapKey string
// configMapInformer is tracked so that we can start these on Run
configMapInformer cache.SharedIndexInformer
// caBundle is a caBundleAndVerifier that contains the last read, non-zero length content of the file
caBundle atomic.Value
listeners []Listener
queue workqueue.TypedRateLimitingInterface[string]
// preRunCaches are the caches to sync before starting the work of this control loop
preRunCaches []cache.InformerSynced
}
var _ CAContentProvider = &ConfigMapCAController{}
var _ ControllerRunner = &ConfigMapCAController{}
// NewDynamicCAFromConfigMapController returns a CAContentProvider based on a configmap that automatically reloads content.
// It is near-realtime via an informer.
func NewDynamicCAFromConfigMapController(purpose, namespace, name, key string, kubeClient kubernetes.Interface) (*ConfigMapCAController, error) {
if len(purpose) == 0 {
return nil, fmt.Errorf("missing purpose for ca bundle")
}
if len(namespace) == 0 {
return nil, fmt.Errorf("missing namespace for ca bundle")
}
if len(name) == 0 {
return nil, fmt.Errorf("missing name for ca bundle")
}
if len(key) == 0 {
return nil, fmt.Errorf("missing key for ca bundle")
}
caContentName := fmt.Sprintf("%s::%s::%s::%s", purpose, namespace, name, key)
// we construct our own informer because we need such a small subset of the information available. Just one namespace.
uncastConfigmapInformer := corev1informers.NewFilteredConfigMapInformer(kubeClient, namespace, 12*time.Hour, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, func(listOptions *v1.ListOptions) {
listOptions.FieldSelector = fields.OneTermEqualSelector("metadata.name", name).String()
})
configmapLister := corev1listers.NewConfigMapLister(uncastConfigmapInformer.GetIndexer())
c := &ConfigMapCAController{
name: caContentName,
configmapNamespace: namespace,
configmapName: name,
configmapKey: key,
configmapLister: configmapLister,
configMapInformer: uncastConfigmapInformer,
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{Name: fmt.Sprintf("DynamicConfigMapCABundle-%s", purpose)},
),
preRunCaches: []cache.InformerSynced{uncastConfigmapInformer.HasSynced},
}
uncastConfigmapInformer.AddEventHandler(cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
if cast, ok := obj.(*corev1.ConfigMap); ok {
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
}
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
if cast, ok := tombstone.Obj.(*corev1.ConfigMap); ok {
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
}
}
return true // always return true just in case. The checks are fairly cheap
},
Handler: cache.ResourceEventHandlerFuncs{
// we have a filter, so any time we're called, we may as well queue. We only ever check one configmap
// so we don't have to be choosy about our key.
AddFunc: func(obj interface{}) {
c.queue.Add(c.keyFn())
},
UpdateFunc: func(oldObj, newObj interface{}) {
c.queue.Add(c.keyFn())
},
DeleteFunc: func(obj interface{}) {
c.queue.Add(c.keyFn())
},
},
})
return c, nil
}
func (c *ConfigMapCAController) keyFn() string {
// this format matches DeletionHandlingMetaNamespaceKeyFunc for our single key
return c.configmapNamespace + "/" + c.configmapName
}
// AddListener adds a listener to be notified when the CA content changes.
func (c *ConfigMapCAController) AddListener(listener Listener) {
c.listeners = append(c.listeners, listener)
}
// loadCABundle determines the next set of content for the file.
func (c *ConfigMapCAController) loadCABundle() error {
configMap, err := c.configmapLister.ConfigMaps(c.configmapNamespace).Get(c.configmapName)
if err != nil {
return err
}
caBundle := configMap.Data[c.configmapKey]
if len(caBundle) == 0 {
return fmt.Errorf("missing content for CA bundle %q", c.Name())
}
// check to see if we have a change. If the values are the same, do nothing.
if !c.hasCAChanged([]byte(caBundle)) {
return nil
}
caBundleAndVerifier, err := newCABundleAndVerifier(c.Name(), []byte(caBundle))
if err != nil {
return err
}
c.caBundle.Store(caBundleAndVerifier)
for _, listener := range c.listeners {
listener.Enqueue()
}
return nil
}
// hasCAChanged returns true if the caBundle is different than the current.
func (c *ConfigMapCAController) hasCAChanged(caBundle []byte) bool {
uncastExisting := c.caBundle.Load()
if uncastExisting == nil {
return true
}
// check to see if we have a change. If the values are the same, do nothing.
existing, ok := uncastExisting.(*caBundleAndVerifier)
if !ok {
return true
}
if !bytes.Equal(existing.caBundle, caBundle) {
return true
}
return false
}
// RunOnce runs a single sync loop
func (c *ConfigMapCAController) RunOnce(ctx context.Context) error {
// Ignore the error when running once because when using a dynamically loaded ca file, because we think it's better to have nothing for
// a brief time than completely crash. If crashing is necessary, higher order logic like a healthcheck and cause failures.
_ = c.loadCABundle()
return nil
}
// Run starts the kube-apiserver and blocks until stopCh is closed.
func (c *ConfigMapCAController) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.InfoS("Starting controller", "name", c.name)
defer klog.InfoS("Shutting down controller", "name", c.name)
// we have a personal informer that is narrowly scoped, start it.
go c.configMapInformer.Run(ctx.Done())
// wait for your secondary caches to fill before starting your work
if !cache.WaitForNamedCacheSync(c.name, ctx.Done(), c.preRunCaches...) {
return
}
// doesn't matter what workers say, only start one.
go wait.Until(c.runWorker, time.Second, ctx.Done())
// start timer that rechecks every minute, just in case. this also serves to prime the controller quickly.
go wait.PollImmediateUntil(FileRefreshDuration, func() (bool, error) {
c.queue.Add(workItemKey)
return false, nil
}, ctx.Done())
<-ctx.Done()
}
func (c *ConfigMapCAController) runWorker() {
for c.processNextWorkItem() {
}
}
func (c *ConfigMapCAController) processNextWorkItem() bool {
dsKey, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(dsKey)
err := c.loadCABundle()
if err == nil {
c.queue.Forget(dsKey)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
c.queue.AddRateLimited(dsKey)
return true
}
// Name is just an identifier
func (c *ConfigMapCAController) Name() string {
return c.name
}
// CurrentCABundleContent provides ca bundle byte content
func (c *ConfigMapCAController) CurrentCABundleContent() []byte {
uncastObj := c.caBundle.Load()
if uncastObj == nil {
return nil // this can happen if we've been unable load data from the apiserver for some reason
}
return c.caBundle.Load().(*caBundleAndVerifier).caBundle
}
// VerifyOptions provides verifyoptions compatible with authenticators
func (c *ConfigMapCAController) VerifyOptions() (x509.VerifyOptions, bool) {
uncastObj := c.caBundle.Load()
if uncastObj == nil {
// This can happen if we've been unable load data from the apiserver for some reason.
// In this case, we should not accept any connections on the basis of this ca bundle.
return x509.VerifyOptions{}, false
}
return uncastObj.(*caBundleAndVerifier).verifyOptions, true
}

View File

@ -0,0 +1,294 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"bytes"
"context"
"crypto/x509"
"errors"
"fmt"
"os"
"sync/atomic"
"time"
"github.com/fsnotify/fsnotify"
"k8s.io/client-go/util/cert"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
// FileRefreshDuration is exposed so that integration tests can crank up the reload speed.
var FileRefreshDuration = 1 * time.Minute
// ControllerRunner is a generic interface for starting a controller
type ControllerRunner interface {
// RunOnce runs the sync loop a single time. This useful for synchronous priming
RunOnce(ctx context.Context) error
// Run should be called a go .Run
Run(ctx context.Context, workers int)
}
// DynamicFileCAContent provides a CAContentProvider that can dynamically react to new file content
// It also fulfills the authenticator interface to provide verifyoptions
type DynamicFileCAContent struct {
name string
// filename is the name the file to read.
filename string
// caBundle is a caBundleAndVerifier that contains the last read, non-zero length content of the file
caBundle atomic.Value
listeners []Listener
// queue only ever has one item, but it has nice error handling backoff/retry semantics
queue workqueue.TypedRateLimitingInterface[string]
}
var _ Notifier = &DynamicFileCAContent{}
var _ CAContentProvider = &DynamicFileCAContent{}
var _ ControllerRunner = &DynamicFileCAContent{}
type caBundleAndVerifier struct {
caBundle []byte
verifyOptions x509.VerifyOptions
}
// NewDynamicCAContentFromFile returns a CAContentProvider based on a filename that automatically reloads content
func NewDynamicCAContentFromFile(purpose, filename string) (*DynamicFileCAContent, error) {
if len(filename) == 0 {
return nil, fmt.Errorf("missing filename for ca bundle")
}
name := fmt.Sprintf("%s::%s", purpose, filename)
ret := &DynamicFileCAContent{
name: name,
filename: filename,
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{Name: fmt.Sprintf("DynamicCABundle-%s", purpose)},
),
}
if err := ret.loadCABundle(); err != nil {
return nil, err
}
return ret, nil
}
// AddListener adds a listener to be notified when the CA content changes.
func (c *DynamicFileCAContent) AddListener(listener Listener) {
c.listeners = append(c.listeners, listener)
}
// loadCABundle determines the next set of content for the file.
func (c *DynamicFileCAContent) loadCABundle() error {
caBundle, err := os.ReadFile(c.filename)
if err != nil {
return err
}
if len(caBundle) == 0 {
return fmt.Errorf("missing content for CA bundle %q", c.Name())
}
// check to see if we have a change. If the values are the same, do nothing.
if !c.hasCAChanged(caBundle) {
return nil
}
caBundleAndVerifier, err := newCABundleAndVerifier(c.Name(), caBundle)
if err != nil {
return err
}
c.caBundle.Store(caBundleAndVerifier)
klog.V(2).InfoS("Loaded a new CA Bundle and Verifier", "name", c.Name())
for _, listener := range c.listeners {
listener.Enqueue()
}
return nil
}
// hasCAChanged returns true if the caBundle is different than the current.
func (c *DynamicFileCAContent) hasCAChanged(caBundle []byte) bool {
uncastExisting := c.caBundle.Load()
if uncastExisting == nil {
return true
}
// check to see if we have a change. If the values are the same, do nothing.
existing, ok := uncastExisting.(*caBundleAndVerifier)
if !ok {
return true
}
if !bytes.Equal(existing.caBundle, caBundle) {
return true
}
return false
}
// RunOnce runs a single sync loop
func (c *DynamicFileCAContent) RunOnce(ctx context.Context) error {
return c.loadCABundle()
}
// Run starts the controller and blocks until stopCh is closed.
func (c *DynamicFileCAContent) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.InfoS("Starting controller", "name", c.name)
defer klog.InfoS("Shutting down controller", "name", c.name)
// doesn't matter what workers say, only start one.
go wait.Until(c.runWorker, time.Second, ctx.Done())
// start the loop that watches the CA file until stopCh is closed.
go wait.Until(func() {
if err := c.watchCAFile(ctx.Done()); err != nil {
klog.ErrorS(err, "Failed to watch CA file, will retry later")
}
}, time.Minute, ctx.Done())
<-ctx.Done()
}
func (c *DynamicFileCAContent) watchCAFile(stopCh <-chan struct{}) error {
// Trigger a check here to ensure the content will be checked periodically even if the following watch fails.
c.queue.Add(workItemKey)
w, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("error creating fsnotify watcher: %v", err)
}
defer w.Close()
if err = w.Add(c.filename); err != nil {
return fmt.Errorf("error adding watch for file %s: %v", c.filename, err)
}
// Trigger a check in case the file is updated before the watch starts.
c.queue.Add(workItemKey)
for {
select {
case e := <-w.Events:
if err := c.handleWatchEvent(e, w); err != nil {
return err
}
case err := <-w.Errors:
return fmt.Errorf("received fsnotify error: %v", err)
case <-stopCh:
return nil
}
}
}
// handleWatchEvent triggers reloading the CA file, and restarts a new watch if it's a Remove or Rename event.
func (c *DynamicFileCAContent) handleWatchEvent(e fsnotify.Event, w *fsnotify.Watcher) error {
// This should be executed after restarting the watch (if applicable) to ensure no file event will be missing.
defer c.queue.Add(workItemKey)
if !e.Has(fsnotify.Remove) && !e.Has(fsnotify.Rename) {
return nil
}
if err := w.Remove(c.filename); err != nil && !errors.Is(err, fsnotify.ErrNonExistentWatch) {
klog.InfoS("Failed to remove file watch, it may have been deleted", "file", c.filename, "err", err)
}
if err := w.Add(c.filename); err != nil {
return fmt.Errorf("error adding watch for file %s: %v", c.filename, err)
}
return nil
}
func (c *DynamicFileCAContent) runWorker() {
for c.processNextWorkItem() {
}
}
func (c *DynamicFileCAContent) processNextWorkItem() bool {
dsKey, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(dsKey)
err := c.loadCABundle()
if err == nil {
c.queue.Forget(dsKey)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
c.queue.AddRateLimited(dsKey)
return true
}
// Name is just an identifier
func (c *DynamicFileCAContent) Name() string {
return c.name
}
// CurrentCABundleContent provides ca bundle byte content
func (c *DynamicFileCAContent) CurrentCABundleContent() (cabundle []byte) {
return c.caBundle.Load().(*caBundleAndVerifier).caBundle
}
// VerifyOptions provides verifyoptions compatible with authenticators
func (c *DynamicFileCAContent) VerifyOptions() (x509.VerifyOptions, bool) {
uncastObj := c.caBundle.Load()
if uncastObj == nil {
return x509.VerifyOptions{}, false
}
return uncastObj.(*caBundleAndVerifier).verifyOptions, true
}
// newVerifyOptions creates a new verification func from a file. It reads the content and then fails.
// It will return a nil function if you pass an empty CA file.
func newCABundleAndVerifier(name string, caBundle []byte) (*caBundleAndVerifier, error) {
if len(caBundle) == 0 {
return nil, fmt.Errorf("missing content for CA bundle %q", name)
}
// Wrap with an x509 verifier
var err error
verifyOptions := defaultVerifyOptions()
verifyOptions.Roots, err = cert.NewPoolFromBytes(caBundle)
if err != nil {
return nil, fmt.Errorf("error loading CA bundle for %q: %v", name, err)
}
return &caBundleAndVerifier{
caBundle: caBundle,
verifyOptions: verifyOptions,
}, nil
}
// defaultVerifyOptions returns VerifyOptions that use the system root certificates, current time,
// and requires certificates to be valid for client auth (x509.ExtKeyUsageClientAuth)
func defaultVerifyOptions() x509.VerifyOptions {
return x509.VerifyOptions{
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
}

View File

@ -0,0 +1,236 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"context"
"crypto/tls"
"fmt"
"os"
"sync/atomic"
"time"
"github.com/fsnotify/fsnotify"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
// DynamicCertKeyPairContent provides a CertKeyContentProvider that can dynamically react to new file content
type DynamicCertKeyPairContent struct {
name string
// certFile is the name of the certificate file to read.
certFile string
// keyFile is the name of the key file to read.
keyFile string
// certKeyPair is a certKeyContent that contains the last read, non-zero length content of the key and cert
certKeyPair atomic.Value
listeners []Listener
// queue only ever has one item, but it has nice error handling backoff/retry semantics
queue workqueue.TypedRateLimitingInterface[string]
}
var _ CertKeyContentProvider = &DynamicCertKeyPairContent{}
var _ ControllerRunner = &DynamicCertKeyPairContent{}
// NewDynamicServingContentFromFiles returns a dynamic CertKeyContentProvider based on a cert and key filename
func NewDynamicServingContentFromFiles(purpose, certFile, keyFile string) (*DynamicCertKeyPairContent, error) {
if len(certFile) == 0 || len(keyFile) == 0 {
return nil, fmt.Errorf("missing filename for serving cert")
}
name := fmt.Sprintf("%s::%s::%s", purpose, certFile, keyFile)
ret := &DynamicCertKeyPairContent{
name: name,
certFile: certFile,
keyFile: keyFile,
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{Name: fmt.Sprintf("DynamicCABundle-%s", purpose)},
),
}
if err := ret.loadCertKeyPair(); err != nil {
return nil, err
}
return ret, nil
}
// AddListener adds a listener to be notified when the serving cert content changes.
func (c *DynamicCertKeyPairContent) AddListener(listener Listener) {
c.listeners = append(c.listeners, listener)
}
// loadCertKeyPair determines the next set of content for the file.
func (c *DynamicCertKeyPairContent) loadCertKeyPair() error {
cert, err := os.ReadFile(c.certFile)
if err != nil {
return err
}
key, err := os.ReadFile(c.keyFile)
if err != nil {
return err
}
if len(cert) == 0 || len(key) == 0 {
return fmt.Errorf("missing content for serving cert %q", c.Name())
}
// Ensure that the key matches the cert and both are valid
_, err = tls.X509KeyPair(cert, key)
if err != nil {
return err
}
newCertKey := &certKeyContent{
cert: cert,
key: key,
}
// check to see if we have a change. If the values are the same, do nothing.
existing, ok := c.certKeyPair.Load().(*certKeyContent)
if ok && existing != nil && existing.Equal(newCertKey) {
return nil
}
c.certKeyPair.Store(newCertKey)
klog.V(2).InfoS("Loaded a new cert/key pair", "name", c.Name())
for _, listener := range c.listeners {
listener.Enqueue()
}
return nil
}
// RunOnce runs a single sync loop
func (c *DynamicCertKeyPairContent) RunOnce(ctx context.Context) error {
return c.loadCertKeyPair()
}
// Run starts the controller and blocks until context is killed.
func (c *DynamicCertKeyPairContent) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.InfoS("Starting controller", "name", c.name)
defer klog.InfoS("Shutting down controller", "name", c.name)
// doesn't matter what workers say, only start one.
go wait.Until(c.runWorker, time.Second, ctx.Done())
// start the loop that watches the cert and key files until stopCh is closed.
go wait.Until(func() {
if err := c.watchCertKeyFile(ctx.Done()); err != nil {
klog.ErrorS(err, "Failed to watch cert and key file, will retry later")
}
}, time.Minute, ctx.Done())
<-ctx.Done()
}
func (c *DynamicCertKeyPairContent) watchCertKeyFile(stopCh <-chan struct{}) error {
// Trigger a check here to ensure the content will be checked periodically even if the following watch fails.
c.queue.Add(workItemKey)
w, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("error creating fsnotify watcher: %v", err)
}
defer w.Close()
if err := w.Add(c.certFile); err != nil {
return fmt.Errorf("error adding watch for file %s: %v", c.certFile, err)
}
if err := w.Add(c.keyFile); err != nil {
return fmt.Errorf("error adding watch for file %s: %v", c.keyFile, err)
}
// Trigger a check in case the file is updated before the watch starts.
c.queue.Add(workItemKey)
for {
select {
case e := <-w.Events:
if err := c.handleWatchEvent(e, w); err != nil {
return err
}
case err := <-w.Errors:
return fmt.Errorf("received fsnotify error: %v", err)
case <-stopCh:
return nil
}
}
}
// handleWatchEvent triggers reloading the cert and key file, and restarts a new watch if it's a Remove or Rename event.
// If one file is updated before the other, the loadCertKeyPair method will catch the mismatch and will not apply the
// change. When an event of the other file is received, it will trigger reloading the files again and the new content
// will be loaded and used.
func (c *DynamicCertKeyPairContent) handleWatchEvent(e fsnotify.Event, w *fsnotify.Watcher) error {
// This should be executed after restarting the watch (if applicable) to ensure no file event will be missing.
defer c.queue.Add(workItemKey)
if !e.Has(fsnotify.Remove) && !e.Has(fsnotify.Rename) {
return nil
}
if err := w.Remove(e.Name); err != nil {
klog.InfoS("Failed to remove file watch, it may have been deleted", "file", e.Name, "err", err)
}
if err := w.Add(e.Name); err != nil {
return fmt.Errorf("error adding watch for file %s: %v", e.Name, err)
}
return nil
}
func (c *DynamicCertKeyPairContent) runWorker() {
for c.processNextWorkItem() {
}
}
func (c *DynamicCertKeyPairContent) processNextWorkItem() bool {
dsKey, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(dsKey)
err := c.loadCertKeyPair()
if err == nil {
c.queue.Forget(dsKey)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
c.queue.AddRateLimited(dsKey)
return true
}
// Name is just an identifier
func (c *DynamicCertKeyPairContent) Name() string {
return c.name
}
// CurrentCertKeyContent provides cert and key byte content
func (c *DynamicCertKeyPairContent) CurrentCertKeyContent() ([]byte, []byte) {
certKeyContent := c.certKeyPair.Load().(*certKeyContent)
return certKeyContent.cert, certKeyContent.key
}

View File

@ -0,0 +1,49 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
// DynamicFileSNIContent provides a SNICertKeyContentProvider that can dynamically react to new file content
type DynamicFileSNIContent struct {
*DynamicCertKeyPairContent
sniNames []string
}
var _ SNICertKeyContentProvider = &DynamicFileSNIContent{}
var _ ControllerRunner = &DynamicFileSNIContent{}
// NewDynamicSNIContentFromFiles returns a dynamic SNICertKeyContentProvider based on a cert and key filename and explicit names
func NewDynamicSNIContentFromFiles(purpose, certFile, keyFile string, sniNames ...string) (*DynamicFileSNIContent, error) {
servingContent, err := NewDynamicServingContentFromFiles(purpose, certFile, keyFile)
if err != nil {
return nil, err
}
ret := &DynamicFileSNIContent{
DynamicCertKeyPairContent: servingContent,
sniNames: sniNames,
}
if err := ret.loadCertKeyPair(); err != nil {
return nil, err
}
return ret, nil
}
// SNINames returns explicitly set SNI names for the certificate. These are not dynamic.
func (c *DynamicFileSNIContent) SNINames() []string {
return c.sniNames
}

View File

@ -0,0 +1,68 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"crypto/x509"
)
// Listener is an interface to use to notify interested parties of a change.
type Listener interface {
// Enqueue should be called when an input may have changed
Enqueue()
}
// Notifier is a way to add listeners
type Notifier interface {
// AddListener is adds a listener to be notified of potential input changes.
// This is a noop on static providers.
AddListener(listener Listener)
}
// CAContentProvider provides ca bundle byte content
type CAContentProvider interface {
Notifier
// Name is just an identifier.
Name() string
// CurrentCABundleContent provides ca bundle byte content. Errors can be
// contained to the controllers initializing the value. By the time you get
// here, you should always be returning a value that won't fail.
CurrentCABundleContent() []byte
// VerifyOptions provides VerifyOptions for authenticators.
VerifyOptions() (x509.VerifyOptions, bool)
}
// CertKeyContentProvider provides a certificate and matching private key.
type CertKeyContentProvider interface {
Notifier
// Name is just an identifier.
Name() string
// CurrentCertKeyContent provides cert and key byte content.
CurrentCertKeyContent() ([]byte, []byte)
}
// SNICertKeyContentProvider provides a certificate and matching private key as
// well as optional explicit names.
type SNICertKeyContentProvider interface {
Notifier
CertKeyContentProvider
// SNINames provides names used for SNI. May return nil.
SNINames() []string
}

View File

@ -0,0 +1,91 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"crypto/tls"
"crypto/x509"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/klog/v2"
netutils "k8s.io/utils/net"
)
// BuildNamedCertificates returns a map of *tls.Certificate by name. It's
// suitable for use in tls.Config#NamedCertificates. Returns an error if any of the certs
// is invalid. Returns nil if len(certs) == 0
func (c *DynamicServingCertificateController) BuildNamedCertificates(sniCerts []sniCertKeyContent) (map[string]*tls.Certificate, error) {
nameToCertificate := map[string]*tls.Certificate{}
byNameExplicit := map[string]*tls.Certificate{}
// Iterate backwards so that earlier certs take precedence in the names map
for i := len(sniCerts) - 1; i >= 0; i-- {
cert, err := tls.X509KeyPair(sniCerts[i].cert, sniCerts[i].key)
if err != nil {
return nil, fmt.Errorf("invalid SNI cert keypair [%d/%q]: %v", i, c.sniCerts[i].Name(), err)
}
// error is not possible given above call to X509KeyPair
x509Cert, _ := x509.ParseCertificate(cert.Certificate[0])
names := sniCerts[i].sniNames
for _, name := range names {
byNameExplicit[name] = &cert
}
klog.V(2).InfoS("Loaded SNI cert", "index", i, "certName", c.sniCerts[i].Name(), "certDetail", GetHumanCertDetail(x509Cert))
if c.eventRecorder != nil {
c.eventRecorder.Eventf(&corev1.ObjectReference{Name: c.sniCerts[i].Name()}, nil, corev1.EventTypeWarning, "TLSConfigChanged", "SNICertificateReload", "loaded SNI cert [%d/%q]: %s with explicit names %v", i, c.sniCerts[i].Name(), GetHumanCertDetail(x509Cert), names)
}
if len(names) == 0 {
names = getCertificateNames(x509Cert)
for _, name := range names {
nameToCertificate[name] = &cert
}
}
}
// Explicitly set names must override
for k, v := range byNameExplicit {
nameToCertificate[k] = v
}
return nameToCertificate, nil
}
// getCertificateNames returns names for an x509.Certificate. The names are
// suitable for use in tls.Config#NamedCertificates.
func getCertificateNames(cert *x509.Certificate) []string {
var names []string
cn := cert.Subject.CommonName
cnIsIP := netutils.ParseIPSloppy(cn) != nil
cnIsValidDomain := cn == "*" || len(validation.IsDNS1123Subdomain(strings.TrimPrefix(cn, "*."))) == 0
// don't use the CN if it is a valid IP because our IP serving detection may unexpectedly use it to terminate the connection.
if !cnIsIP && cnIsValidDomain {
names = append(names, cn)
}
names = append(names, cert.DNSNames...)
// intentionally all IPs in the cert are ignored as SNI forbids passing IPs
// to select a cert. Before go 1.6 the tls happily passed IPs as SNI values.
return names
}

View File

@ -0,0 +1,120 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"crypto/tls"
"crypto/x509"
)
type staticCAContent struct {
name string
caBundle *caBundleAndVerifier
}
var _ CAContentProvider = &staticCAContent{}
// NewStaticCAContent returns a CAContentProvider that always returns the same value
func NewStaticCAContent(name string, caBundle []byte) (CAContentProvider, error) {
caBundleAndVerifier, err := newCABundleAndVerifier(name, caBundle)
if err != nil {
return nil, err
}
return &staticCAContent{
name: name,
caBundle: caBundleAndVerifier,
}, nil
}
// Name is just an identifier
func (c *staticCAContent) Name() string {
return c.name
}
func (c *staticCAContent) AddListener(Listener) {}
// CurrentCABundleContent provides ca bundle byte content
func (c *staticCAContent) CurrentCABundleContent() (cabundle []byte) {
return c.caBundle.caBundle
}
func (c *staticCAContent) VerifyOptions() (x509.VerifyOptions, bool) {
return c.caBundle.verifyOptions, true
}
type staticCertKeyContent struct {
name string
cert []byte
key []byte
}
// NewStaticCertKeyContent returns a CertKeyContentProvider that always returns the same value
func NewStaticCertKeyContent(name string, cert, key []byte) (CertKeyContentProvider, error) {
// Ensure that the key matches the cert and both are valid
_, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
return &staticCertKeyContent{
name: name,
cert: cert,
key: key,
}, nil
}
// Name is just an identifier
func (c *staticCertKeyContent) Name() string {
return c.name
}
func (c *staticCertKeyContent) AddListener(Listener) {}
// CurrentCertKeyContent provides cert and key content
func (c *staticCertKeyContent) CurrentCertKeyContent() ([]byte, []byte) {
return c.cert, c.key
}
type staticSNICertKeyContent struct {
staticCertKeyContent
sniNames []string
}
// NewStaticSNICertKeyContent returns a SNICertKeyContentProvider that always returns the same value
func NewStaticSNICertKeyContent(name string, cert, key []byte, sniNames ...string) (SNICertKeyContentProvider, error) {
// Ensure that the key matches the cert and both are valid
_, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
return &staticSNICertKeyContent{
staticCertKeyContent: staticCertKeyContent{
name: name,
cert: cert,
key: key,
},
sniNames: sniNames,
}, nil
}
func (c *staticSNICertKeyContent) SNINames() []string {
return c.sniNames
}
func (c *staticSNICertKeyContent) AddListener(Listener) {}

View File

@ -0,0 +1,287 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"sync/atomic"
"time"
corev1 "k8s.io/api/core/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/events"
"k8s.io/client-go/util/cert"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
const workItemKey = "key"
// DynamicServingCertificateController dynamically loads certificates and provides a golang tls compatible dynamic GetCertificate func.
type DynamicServingCertificateController struct {
// baseTLSConfig is the static portion of the tlsConfig for serving to clients. It is copied and the copy is mutated
// based on the dynamic cert state.
baseTLSConfig *tls.Config
// clientCA provides the very latest content of the ca bundle
clientCA CAContentProvider
// servingCert provides the very latest content of the default serving certificate
servingCert CertKeyContentProvider
// sniCerts are a list of CertKeyContentProvider with associated names used for SNI
sniCerts []SNICertKeyContentProvider
// currentlyServedContent holds the original bytes that we are serving. This is used to decide if we need to set a
// new atomic value. The types used for efficient TLSConfig preclude using the processed value.
currentlyServedContent *dynamicCertificateContent
// currentServingTLSConfig holds a *tls.Config that will be used to serve requests
currentServingTLSConfig atomic.Value
// queue only ever has one item, but it has nice error handling backoff/retry semantics
queue workqueue.TypedRateLimitingInterface[string]
eventRecorder events.EventRecorder
}
var _ Listener = &DynamicServingCertificateController{}
// NewDynamicServingCertificateController returns a controller that can be used to keep a TLSConfig up to date.
func NewDynamicServingCertificateController(
baseTLSConfig *tls.Config,
clientCA CAContentProvider,
servingCert CertKeyContentProvider,
sniCerts []SNICertKeyContentProvider,
eventRecorder events.EventRecorder,
) *DynamicServingCertificateController {
c := &DynamicServingCertificateController{
baseTLSConfig: baseTLSConfig,
clientCA: clientCA,
servingCert: servingCert,
sniCerts: sniCerts,
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{Name: "DynamicServingCertificateController"},
),
eventRecorder: eventRecorder,
}
return c
}
// GetConfigForClient is an implementation of tls.Config.GetConfigForClient
func (c *DynamicServingCertificateController) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
uncastObj := c.currentServingTLSConfig.Load()
if uncastObj == nil {
return nil, errors.New("dynamiccertificates: configuration not ready")
}
tlsConfig, ok := uncastObj.(*tls.Config)
if !ok {
return nil, errors.New("dynamiccertificates: unexpected config type")
}
tlsConfigCopy := tlsConfig.Clone()
// if the client set SNI information, just use our "normal" SNI flow
if len(clientHello.ServerName) > 0 {
return tlsConfigCopy, nil
}
// if the client didn't set SNI, then we need to inspect the requested IP so that we can choose
// a certificate from our list if we specifically handle that IP. This can happen when an IP is specifically mapped by name.
host, _, err := net.SplitHostPort(clientHello.Conn.LocalAddr().String())
if err != nil {
return tlsConfigCopy, nil
}
ipCert, ok := tlsConfigCopy.NameToCertificate[host]
if !ok {
return tlsConfigCopy, nil
}
tlsConfigCopy.Certificates = []tls.Certificate{*ipCert}
tlsConfigCopy.NameToCertificate = nil
return tlsConfigCopy, nil
}
// newTLSContent determines the next set of content for overriding the baseTLSConfig.
func (c *DynamicServingCertificateController) newTLSContent() (*dynamicCertificateContent, error) {
newContent := &dynamicCertificateContent{}
if c.clientCA != nil {
currClientCABundle := c.clientCA.CurrentCABundleContent()
// we allow removing all client ca bundles because the server is still secure when this happens. it just means
// that there isn't a hint to clients about which client-cert to used. this happens when there is no client-ca
// yet known for authentication, which can happen in aggregated apiservers and some kube-apiserver deployment modes.
newContent.clientCA = caBundleContent{caBundle: currClientCABundle}
}
if c.servingCert != nil {
currServingCert, currServingKey := c.servingCert.CurrentCertKeyContent()
if len(currServingCert) == 0 || len(currServingKey) == 0 {
return nil, fmt.Errorf("not loading an empty serving certificate from %q", c.servingCert.Name())
}
newContent.servingCert = certKeyContent{cert: currServingCert, key: currServingKey}
}
for i, sniCert := range c.sniCerts {
currCert, currKey := sniCert.CurrentCertKeyContent()
if len(currCert) == 0 || len(currKey) == 0 {
return nil, fmt.Errorf("not loading an empty SNI certificate from %d/%q", i, sniCert.Name())
}
newContent.sniCerts = append(newContent.sniCerts, sniCertKeyContent{certKeyContent: certKeyContent{cert: currCert, key: currKey}, sniNames: sniCert.SNINames()})
}
return newContent, nil
}
// syncCerts gets newTLSContent, if it has changed from the existing, the content is parsed and stored for usage in
// GetConfigForClient.
func (c *DynamicServingCertificateController) syncCerts() error {
newContent, err := c.newTLSContent()
if err != nil {
return err
}
// if the content is the same as what we currently have, we can simply skip it. This works because we are single
// threaded. If you ever make this multi-threaded, add a lock.
if newContent.Equal(c.currentlyServedContent) {
return nil
}
// make a shallow copy and override the dynamic pieces which have changed.
newTLSConfigCopy := c.baseTLSConfig.Clone()
// parse new content to add to TLSConfig
if len(newContent.clientCA.caBundle) > 0 {
newClientCAPool := x509.NewCertPool()
newClientCAs, err := cert.ParseCertsPEM(newContent.clientCA.caBundle)
if err != nil {
return fmt.Errorf("unable to load client CA file %q: %v", string(newContent.clientCA.caBundle), err)
}
for i, cert := range newClientCAs {
klog.V(2).InfoS("Loaded client CA", "index", i, "certName", c.clientCA.Name(), "certDetail", GetHumanCertDetail(cert))
if c.eventRecorder != nil {
c.eventRecorder.Eventf(&corev1.ObjectReference{Name: c.clientCA.Name()}, nil, corev1.EventTypeWarning, "TLSConfigChanged", "CACertificateReload", "loaded client CA [%d/%q]: %s", i, c.clientCA.Name(), GetHumanCertDetail(cert))
}
newClientCAPool.AddCert(cert)
}
newTLSConfigCopy.ClientCAs = newClientCAPool
}
if len(newContent.servingCert.cert) > 0 && len(newContent.servingCert.key) > 0 {
cert, err := tls.X509KeyPair(newContent.servingCert.cert, newContent.servingCert.key)
if err != nil {
return fmt.Errorf("invalid serving cert keypair: %v", err)
}
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return fmt.Errorf("invalid serving cert: %v", err)
}
klog.V(2).InfoS("Loaded serving cert", "certName", c.servingCert.Name(), "certDetail", GetHumanCertDetail(x509Cert))
if c.eventRecorder != nil {
c.eventRecorder.Eventf(&corev1.ObjectReference{Name: c.servingCert.Name()}, nil, corev1.EventTypeWarning, "TLSConfigChanged", "ServingCertificateReload", "loaded serving cert [%q]: %s", c.servingCert.Name(), GetHumanCertDetail(x509Cert))
}
newTLSConfigCopy.Certificates = []tls.Certificate{cert}
}
if len(newContent.sniCerts) > 0 {
newTLSConfigCopy.NameToCertificate, err = c.BuildNamedCertificates(newContent.sniCerts)
if err != nil {
return fmt.Errorf("unable to build named certificate map: %v", err)
}
// append all named certs. Otherwise, the go tls stack will think no SNI processing
// is necessary because there is only one cert anyway.
// Moreover, if servingCert is not set, the first SNI
// cert will become the default cert. That's what we expect anyway.
for _, sniCert := range newTLSConfigCopy.NameToCertificate {
newTLSConfigCopy.Certificates = append(newTLSConfigCopy.Certificates, *sniCert)
}
}
// store new values of content for serving.
c.currentServingTLSConfig.Store(newTLSConfigCopy)
c.currentlyServedContent = newContent // this is single threaded, so we have no locking issue
return nil
}
// RunOnce runs a single sync step to ensure that we have a valid starting configuration.
func (c *DynamicServingCertificateController) RunOnce() error {
return c.syncCerts()
}
// Run starts the kube-apiserver and blocks until stopCh is closed.
func (c *DynamicServingCertificateController) Run(workers int, stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.InfoS("Starting DynamicServingCertificateController")
defer klog.InfoS("Shutting down DynamicServingCertificateController")
// synchronously load once. We will trigger again, so ignoring any error is fine
_ = c.RunOnce()
// doesn't matter what workers say, only start one.
go wait.Until(c.runWorker, time.Second, stopCh)
// start timer that rechecks every minute, just in case. this also serves to prime the controller quickly.
go wait.Until(func() {
c.Enqueue()
}, 1*time.Minute, stopCh)
<-stopCh
}
func (c *DynamicServingCertificateController) runWorker() {
for c.processNextWorkItem() {
}
}
func (c *DynamicServingCertificateController) processNextWorkItem() bool {
dsKey, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(dsKey)
err := c.syncCerts()
if err == nil {
c.queue.Forget(dsKey)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
c.queue.AddRateLimited(dsKey)
return true
}
// Enqueue a method to allow separate control loops to cause the certificate controller to trigger and read content.
func (c *DynamicServingCertificateController) Enqueue() {
c.queue.Add(workItemKey)
}

View File

@ -0,0 +1,105 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"bytes"
"context"
"crypto/x509"
"strings"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
)
type unionCAContent []CAContentProvider
var _ CAContentProvider = &unionCAContent{}
var _ ControllerRunner = &unionCAContent{}
// NewUnionCAContentProvider returns a CAContentProvider that is a union of other CAContentProviders
func NewUnionCAContentProvider(caContentProviders ...CAContentProvider) CAContentProvider {
return unionCAContent(caContentProviders)
}
// Name is just an identifier
func (c unionCAContent) Name() string {
names := []string{}
for _, curr := range c {
names = append(names, curr.Name())
}
return strings.Join(names, ",")
}
// CurrentCABundleContent provides ca bundle byte content
func (c unionCAContent) CurrentCABundleContent() []byte {
caBundles := [][]byte{}
for _, curr := range c {
if currCABytes := curr.CurrentCABundleContent(); len(currCABytes) > 0 {
caBundles = append(caBundles, []byte(strings.TrimSpace(string(currCABytes))))
}
}
return bytes.Join(caBundles, []byte("\n"))
}
// CurrentCABundleContent provides ca bundle byte content
func (c unionCAContent) VerifyOptions() (x509.VerifyOptions, bool) {
currCABundle := c.CurrentCABundleContent()
if len(currCABundle) == 0 {
return x509.VerifyOptions{}, false
}
// TODO make more efficient. This isn't actually used in any of our mainline paths. It's called to build the TLSConfig
// TODO on file changes, but the actual authentication runs against the individual items, not the union.
ret, err := newCABundleAndVerifier(c.Name(), c.CurrentCABundleContent())
if err != nil {
// because we're made up of already vetted values, this indicates some kind of coding error
panic(err)
}
return ret.verifyOptions, true
}
// AddListener adds a listener to be notified when the CA content changes.
func (c unionCAContent) AddListener(listener Listener) {
for _, curr := range c {
curr.AddListener(listener)
}
}
// AddListener adds a listener to be notified when the CA content changes.
func (c unionCAContent) RunOnce(ctx context.Context) error {
errors := []error{}
for _, curr := range c {
if controller, ok := curr.(ControllerRunner); ok {
if err := controller.RunOnce(ctx); err != nil {
errors = append(errors, err)
}
}
}
return utilerrors.NewAggregate(errors)
}
// Run runs the controller
func (c unionCAContent) Run(ctx context.Context, workers int) {
for _, curr := range c {
if controller, ok := curr.(ControllerRunner); ok {
go controller.Run(ctx, workers)
}
}
}

View File

@ -0,0 +1,66 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiccertificates
import (
"crypto/x509"
"fmt"
"strings"
"time"
)
// GetHumanCertDetail is a convenient method for printing compact details of certificate that helps when debugging
// kube-apiserver usage of certs.
func GetHumanCertDetail(certificate *x509.Certificate) string {
humanName := certificate.Subject.CommonName
signerHumanName := certificate.Issuer.CommonName
if certificate.Subject.CommonName == certificate.Issuer.CommonName {
signerHumanName = "<self>"
}
usages := []string{}
for _, curr := range certificate.ExtKeyUsage {
if curr == x509.ExtKeyUsageClientAuth {
usages = append(usages, "client")
continue
}
if curr == x509.ExtKeyUsageServerAuth {
usages = append(usages, "serving")
continue
}
usages = append(usages, fmt.Sprintf("%d", curr))
}
validServingNames := []string{}
for _, ip := range certificate.IPAddresses {
validServingNames = append(validServingNames, ip.String())
}
validServingNames = append(validServingNames, certificate.DNSNames...)
servingString := ""
if len(validServingNames) > 0 {
servingString = fmt.Sprintf(" validServingFor=[%s]", strings.Join(validServingNames, ","))
}
groupString := ""
if len(certificate.Subject.Organization) > 0 {
groupString = fmt.Sprintf(" groups=[%s]", strings.Join(certificate.Subject.Organization, ","))
}
return fmt.Sprintf("%q [%s]%s%s issuer=%q (%v to %v (now=%v))", humanName, strings.Join(usages, ","), groupString, servingString, signerHumanName, certificate.NotBefore.UTC(), certificate.NotAfter.UTC(),
time.Now().UTC())
}