ContainerImage.Pinniped/internal/controller/supervisorconfig/secretgenerator/secretgenerator.go
Andrew Keesler e17bc31b29
Pass CSRF cookie signing key from controller to cache
This also sets the CSRF cookie Secret's OwnerReference to the Pod's grandparent
Deployment so that when the Deployment is cleaned up, then the Secret is as
well.

Obviously this controller implementation has a lot of issues, but it will at
least get us started.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
2020-12-11 11:49:27 -05:00

188 lines
5.2 KiB
Go

// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package secretgenerator provides a controller that can ensure existence of a generated secret.
package secretgenerator
import (
"context"
"crypto/rand"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
pinnipedcontroller "go.pinniped.dev/internal/controller"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/plog"
)
const (
symmetricKeySecretType = "secrets.pinniped.dev/symmetric"
symmetricKeySecretDataKey = "key"
symmetricKeySize = 32 // TODO: what should this be?
)
// generateKey is stubbed out for the purpose of testing. The default behavior is to generate a symmetric key.
//nolint:gochecknoglobals
var generateKey = generateSymmetricKey
func generateSymmetricKey() ([]byte, error) {
b := make([]byte, symmetricKeySize)
if _, err := rand.Read(b); err != nil {
return nil, err
}
return b, nil
}
type controller struct {
owner *appsv1.Deployment
client kubernetes.Interface
secrets corev1informers.SecretInformer
onCreateOrUpdate func(secret []byte)
}
// New instantiates a new controllerlib.Controller which will ensure existence of a generated secret.
func New(
owner *appsv1.Deployment,
client kubernetes.Interface,
secrets corev1informers.SecretInformer,
onCreateOrUpdate func(secret []byte),
) controllerlib.Controller {
c := controller{
owner: owner,
client: client,
secrets: secrets,
onCreateOrUpdate: onCreateOrUpdate,
}
filter := pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool {
return metav1.IsControlledBy(obj, owner)
}, nil)
return controllerlib.New(
controllerlib.Config{Name: owner.Name + "-secret-generator", Syncer: &c},
controllerlib.WithInformer(secrets, filter, controllerlib.InformerOption{}),
controllerlib.WithInitialEvent(controllerlib.Key{
Namespace: owner.Namespace,
Name: owner.Name + "-keys",
}),
)
}
// Sync implements controllerlib.Syncer.Sync().
func (c *controller) Sync(ctx controllerlib.Context) error {
secret, err := c.secrets.Lister().Secrets(ctx.Key.Namespace).Get(ctx.Key.Name)
isNotFound := k8serrors.IsNotFound(err)
if !isNotFound && err != nil {
return fmt.Errorf("failed to list secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err)
}
secretNeedsUpdate := isNotFound || !c.isValid(secret)
if !secretNeedsUpdate {
plog.Debug("secret is up to date", "secret", klog.KObj(secret))
c.onCreateOrUpdate(secret.Data[symmetricKeySecretDataKey])
return nil
}
newSecret, err := c.generateSecret(ctx.Key.Namespace, ctx.Key.Name)
if err != nil {
return fmt.Errorf("failed to generate secret: %w", err)
}
if isNotFound {
err = c.createSecret(ctx.Context, newSecret)
} else {
err = c.updateSecret(ctx.Context, &newSecret, ctx.Key.Name)
}
if err != nil {
return fmt.Errorf("failed to create/update secret %s/%s: %w", newSecret.Namespace, newSecret.Name, err)
}
c.onCreateOrUpdate(newSecret.Data[symmetricKeySecretDataKey])
return nil
}
func (c *controller) isValid(secret *corev1.Secret) bool {
if secret.Type != symmetricKeySecretType {
return false
}
data, ok := secret.Data[symmetricKeySecretDataKey]
if !ok {
return false
}
if len(data) != symmetricKeySize {
return false
}
return true
}
func (c *controller) generateSecret(namespace, name string) (*corev1.Secret, error) {
symmetricKey, err := generateKey()
if err != nil {
return nil, err
}
deploymentGVK := schema.GroupVersionKind{
Group: appsv1.SchemeGroupVersion.Group,
Version: appsv1.SchemeGroupVersion.Version,
Kind: "Deployment",
}
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(c.owner, deploymentGVK),
},
},
Type: symmetricKeySecretType,
Data: map[string][]byte{
symmetricKeySecretDataKey: symmetricKey,
},
}, nil
}
func (c *controller) createSecret(ctx context.Context, newSecret *corev1.Secret) error {
_, err := c.client.CoreV1().Secrets(newSecret.Namespace).Create(ctx, newSecret, metav1.CreateOptions{})
return err
}
func (c *controller) updateSecret(ctx context.Context, newSecret **corev1.Secret, secretName string) error {
secrets := c.client.CoreV1().Secrets((*newSecret).Namespace)
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
currentSecret, err := secrets.Get(ctx, secretName, metav1.GetOptions{})
isNotFound := k8serrors.IsNotFound(err)
if !isNotFound && err != nil {
return fmt.Errorf("failed to get secret: %w", err)
}
if isNotFound {
if err := c.createSecret(ctx, *newSecret); err != nil {
return fmt.Errorf("failed to create secret: %w", err)
}
return nil
}
if c.isValid(currentSecret) {
*newSecret = currentSecret
return nil
}
currentSecret.Type = (*newSecret).Type
currentSecret.Data = (*newSecret).Data
_, err = secrets.Update(ctx, currentSecret, metav1.UpdateOptions{})
return err
})
}