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>
This commit is contained in:
Andrew Keesler
2020-12-11 11:49:27 -05:00
parent 22c5b102ed
commit e17bc31b29
7 changed files with 190 additions and 56 deletions
@@ -9,9 +9,11 @@ import (
"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"
@@ -42,22 +44,35 @@ func generateSymmetricKey() ([]byte, error) {
}
type controller struct {
secretNamePrefix string
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(secretNamePrefix string, client kubernetes.Interface, secrets corev1informers.SecretInformer) controllerlib.Controller {
func New(
owner *appsv1.Deployment,
client kubernetes.Interface,
secrets corev1informers.SecretInformer,
onCreateOrUpdate func(secret []byte),
) controllerlib.Controller {
c := controller{
secretNamePrefix: secretNamePrefix,
owner: owner,
client: client,
secrets: secrets,
onCreateOrUpdate: onCreateOrUpdate,
}
filter := pinnipedcontroller.SimpleFilterWithSingletonQueue(isOwnee)
filter := pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool {
return metav1.IsControlledBy(obj, owner)
}, nil)
return controllerlib.New(
controllerlib.Config{Name: secretNamePrefix + "-secrets-generator", Syncer: &c},
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",
}),
)
}
@@ -72,10 +87,11 @@ func (c *controller) Sync(ctx controllerlib.Context) error {
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)
newSecret, err := c.generateSecret(ctx.Key.Namespace, ctx.Key.Name)
if err != nil {
return fmt.Errorf("failed to generate secret: %w", err)
}
@@ -83,12 +99,14 @@ func (c *controller) Sync(ctx controllerlib.Context) error {
if isNotFound {
err = c.createSecret(ctx.Context, newSecret)
} else {
err = c.updateSecret(ctx.Context, newSecret, ctx.Key.Name)
err = c.updateSecret(ctx.Context, &newSecret, ctx.Key.Name)
}
if err != nil {
return fmt.Errorf("failed to create/update secret %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err)
return fmt.Errorf("failed to create/update secret %s/%s: %w", newSecret.Namespace, newSecret.Name, err)
}
c.onCreateOrUpdate(newSecret.Data[symmetricKeySecretDataKey])
return nil
}
@@ -108,16 +126,24 @@ func (c *controller) isValid(secret *corev1.Secret) bool {
return true
}
func (c *controller) generateSecret(namespace string) (*corev1.Secret, error) {
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{
GenerateName: c.secretNamePrefix,
Namespace: namespace,
Name: name,
Namespace: namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(c.owner, deploymentGVK),
},
},
Type: symmetricKeySecretType,
Data: map[string][]byte{
@@ -131,8 +157,8 @@ func (c *controller) createSecret(ctx context.Context, newSecret *corev1.Secret)
return err
}
func (c *controller) updateSecret(ctx context.Context, newSecret *corev1.Secret, secretName string) error {
secrets := c.client.CoreV1().Secrets(newSecret.Namespace)
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)
@@ -141,26 +167,21 @@ func (c *controller) updateSecret(ctx context.Context, newSecret *corev1.Secret,
}
if isNotFound {
if err := c.createSecret(ctx, newSecret); err != nil {
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
currentSecret.Type = (*newSecret).Type
currentSecret.Data = (*newSecret).Data
_, err = secrets.Update(ctx, currentSecret, metav1.UpdateOptions{})
return err
})
}
// isOwnee returns whether the provided obj is owned by this controller.
func isOwnee(obj metav1.Object) bool {
// TODO: how do we say we are owned by our Deployment?
return true
}
@@ -11,6 +11,7 @@ import (
"time"
"github.com/stretchr/testify/require"
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"
@@ -25,9 +26,9 @@ import (
func TestController(t *testing.T) {
const (
generatedSecretNamespace = "some-namespace"
generatedSecretNamePrefix = "some-name-"
generatedSecretName = "some-name-abc123"
generatedSecretNamespace = "some-namespace"
generatedSecretName = "some-name-abc123"
otherGeneratedSecretName = "some-other-name-abc123"
)
var (
@@ -37,32 +38,60 @@ func TestController(t *testing.T) {
Resource: "secrets",
}
generatedSymmetricKey = []byte("some-neato-32-byte-generated-key")
owner = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "some-owner-name",
UID: "some-owner-uid",
},
}
ownerGVK = schema.GroupVersionKind{
Group: appsv1.SchemeGroupVersion.Group,
Version: appsv1.SchemeGroupVersion.Version,
Kind: "Deployment",
}
generatedSymmetricKey = []byte("some-neato-32-byte-generated-key")
otherGeneratedSymmetricKey = []byte("some-funio-32-byte-generated-key")
generatedSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: generatedSecretNamePrefix,
Namespace: generatedSecretNamespace,
Name: generatedSecretName,
Namespace: generatedSecretNamespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(owner, ownerGVK),
},
},
Type: "secrets.pinniped.dev/symmetric",
Data: map[string][]byte{
"key": generatedSymmetricKey,
},
}
)
generatedSecretWithName := generatedSecret.DeepCopy()
generatedSecretWithName.Name = generatedSecretName
otherGeneratedSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: generatedSecretName,
Namespace: generatedSecretNamespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(owner, ownerGVK),
},
},
Type: "secrets.pinniped.dev/symmetric",
Data: map[string][]byte{
"key": otherGeneratedSymmetricKey,
},
}
)
once := sync.Once{}
tests := []struct {
name string
storedSecret func(**corev1.Secret)
generateKey func() ([]byte, error)
apiClient func(*testing.T, *kubernetesfake.Clientset)
wantError string
wantActions []kubetesting.Action
name string
storedSecret func(**corev1.Secret)
generateKey func() ([]byte, error)
apiClient func(*testing.T, *kubernetesfake.Clientset)
wantError string
wantActions []kubetesting.Action
wantCallbackSecret []byte
}{
{
name: "when the secrets does not exist, it gets generated",
@@ -72,9 +101,11 @@ func TestController(t *testing.T) {
wantActions: []kubetesting.Action{
kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
},
wantCallbackSecret: generatedSymmetricKey,
},
{
name: "when a valid secret exists, nothing happens",
name: "when a valid secret exists, nothing happens",
wantCallbackSecret: generatedSymmetricKey,
},
{
name: "secret gets updated when the type is wrong",
@@ -83,8 +114,9 @@ func TestController(t *testing.T) {
},
wantActions: []kubetesting.Action{
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
},
wantCallbackSecret: generatedSymmetricKey,
},
{
name: "secret gets updated when the key data does not exist",
@@ -93,8 +125,9 @@ func TestController(t *testing.T) {
},
wantActions: []kubetesting.Action{
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
},
wantCallbackSecret: generatedSymmetricKey,
},
{
name: "secret gets updated when the key data is too short",
@@ -103,8 +136,9 @@ func TestController(t *testing.T) {
},
wantActions: []kubetesting.Action{
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
},
wantCallbackSecret: generatedSymmetricKey,
},
{
name: "an error is returned when creating fails",
@@ -133,7 +167,7 @@ func TestController(t *testing.T) {
},
wantActions: []kubetesting.Action{
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
},
wantError: "failed to create/update secret some-namespace/some-name-abc123: some update error",
},
@@ -168,10 +202,11 @@ func TestController(t *testing.T) {
},
wantActions: []kubetesting.Action{
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecretWithName),
kubetesting.NewUpdateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
},
wantCallbackSecret: generatedSymmetricKey,
},
{
name: "upon updating we discover that a valid secret exists",
@@ -180,12 +215,13 @@ func TestController(t *testing.T) {
},
apiClient: func(t *testing.T, client *kubernetesfake.Clientset) {
client.PrependReactor("get", "secrets", func(action kubetesting.Action) (bool, runtime.Object, error) {
return true, generatedSecretWithName, nil
return true, otherGeneratedSecret, nil
})
},
wantActions: []kubetesting.Action{
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
},
wantCallbackSecret: otherGeneratedSymmetricKey,
},
{
name: "upon updating we discover that the secret has been deleted",
@@ -204,6 +240,7 @@ func TestController(t *testing.T) {
kubetesting.NewGetAction(secretsGVR, generatedSecretNamespace, generatedSecretName),
kubetesting.NewCreateAction(secretsGVR, generatedSecretNamespace, generatedSecret),
},
wantCallbackSecret: generatedSymmetricKey,
},
{
name: "upon updating we discover that the secret has been deleted and our create fails",
@@ -257,7 +294,7 @@ func TestController(t *testing.T) {
}
informerClient := kubernetesfake.NewSimpleClientset()
storedSecret := generatedSecretWithName.DeepCopy()
storedSecret := generatedSecret.DeepCopy()
if test.storedSecret != nil {
test.storedSecret(&storedSecret)
}
@@ -269,7 +306,11 @@ func TestController(t *testing.T) {
informers := kubeinformers.NewSharedInformerFactory(informerClient, 0)
secrets := informers.Core().V1().Secrets()
c := New(generatedSecretNamePrefix, apiClient, secrets)
var callbackSecret []byte
c := New(owner, apiClient, secrets, func(secret []byte) {
require.Nil(t, callbackSecret, "callback was called twice")
callbackSecret = secret
})
// Must start informers before calling TestRunSynchronously().
informers.Start(ctx.Done())
@@ -292,6 +333,8 @@ func TestController(t *testing.T) {
test.wantActions = []kubetesting.Action{}
}
require.Equal(t, test.wantActions, apiClient.Actions())
require.Equal(t, test.wantCallbackSecret, callbackSecret)
})
}
}