Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
ssclientset "github.com/bitnami/sealed-secrets/pkg/client/clientset/versioned"
ssv1alpha1client "github.com/bitnami/sealed-secrets/pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1"
ssinformer "github.com/bitnami/sealed-secrets/pkg/client/informers/externalversions"
"github.com/bitnami/sealed-secrets/pkg/crypto"
"github.com/bitnami/sealed-secrets/pkg/multidocyaml"
)

Expand Down Expand Up @@ -135,12 +136,50 @@ func watchKeySecrets(kinformer informers.SharedInformerFactory, registry *KeyReg
kInformer := kinformer.Core().V1().Secrets().Informer()
_, err := kInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
err := registryNewKeyWithSecret(obj.(*corev1.Secret), registry, keyOrderPriority)
secret, ok := obj.(*corev1.Secret)
if !ok {
return
}
err := registryNewKeyWithSecret(secret, registry, keyOrderPriority)
if err != nil {
slog.Error("failed to register key", "secret", secret.Name, "error", err)
return
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
secret, ok := newObj.(*corev1.Secret)
if !ok {
return
}
err := registryNewKeyWithSecret(secret, registry, keyOrderPriority)
if err != nil {
slog.Error("failed to register key", "error", err)
slog.Error("failed to register updated key", "secret", secret.Name, "error", err)
return
}
},
DeleteFunc: func(obj interface{}) {
secret, ok := obj.(*corev1.Secret)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
return
}
secret, ok = tombstone.Obj.(*corev1.Secret)
if !ok {
return
}
}
key, _, err := readKey(secret)
if err != nil {
return
}
fp, err := crypto.PublicKeyFingerprint(&key.PublicKey)
if err != nil {
return
}
registry.unregisterKey(fp)
slog.Info("unregistered deleted private key", "secretname", secret.Name)
},
})
if err != nil {
return nil, fmt.Errorf("could not add event handler to secrets informer: %w", err)
Expand Down Expand Up @@ -241,13 +280,16 @@ func watchSecrets(sinformer informers.SharedInformerFactory, ssclientset ssclien
// HasSynced returns true once this controller has completed an
// initial resource listing.
func (c *Controller) HasSynced() bool {
var synced bool
if c.sInformer == nil {
synced = c.ssInformer.HasSynced()
} else {
synced = c.ssInformer.HasSynced() && c.sInformer.HasSynced()
if !c.ssInformer.HasSynced() {
return false
}
if c.sInformer != nil && !c.sInformer.HasSynced() {
return false
}
return synced
if c.kInformer != nil && !c.kInformer.HasSynced() {
return false
}
return true
}

// LastSyncResourceVersion is the resource version observed when last
Expand Down
108 changes: 106 additions & 2 deletions pkg/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
"sync"
Expand All @@ -13,15 +14,18 @@ import (
"encoding/json"

ssv1alpha1 "github.com/bitnami/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1"
ssfake "github.com/bitnami/sealed-secrets/pkg/client/clientset/versioned/fake"
"github.com/bitnami/sealed-secrets/pkg/crypto"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"

ssfake "github.com/bitnami/sealed-secrets/pkg/client/clientset/versioned/fake"
ktesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
)

func someStr(s string) *string {
Expand Down Expand Up @@ -596,3 +600,103 @@ func TestSealedSecretTypesAreRegisteredGlobally(t *testing.T) {
}
}
}

func TestWatchKeySecretsDynamicDetection(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

clientset := fake.NewClientset()
var nameSeq int
clientset.PrependReactor("create", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) {
ca, ok := action.(ktesting.CreateAction)
if !ok {
return false, nil, nil
}
sec, ok := ca.GetObject().(*corev1.Secret)
if ok && sec.Name == "" && sec.GenerateName != "" {
nameSeq++
sec.Name = fmt.Sprintf("%s%d", sec.GenerateName, nameSeq)
}
return false, nil, nil
})
keyRegistry := NewKeyRegistry(clientset, "ns", "prefix", SealedSecretsKeyLabel, 2048)

informerFactory := informers.NewSharedInformerFactory(clientset, 0)
kInformer, err := watchKeySecrets(informerFactory, keyRegistry, "CertNotBefore")
if err != nil {
t.Fatal(err)
}

stopCh := make(chan struct{})
defer close(stopCh)
go kInformer.Run(stopCh)

if !cache.WaitForCacheSync(stopCh, kInformer.HasSynced) {
t.Fatal("timed out waiting for informer to sync")
}

// 1. Generate key and write secret
key1, cert1, err := generatePrivateKeyAndCert(2048, time.Hour, "cn1")
if err != nil {
t.Fatal(err)
}
secName1, err := writeKey(ctx, clientset, key1, []*x509.Certificate{cert1}, "ns", SealedSecretsKeyLabel, "prefix-", "", "")
if err != nil {
t.Fatal(err)
}

// Wait for informer to pick it up
var latest *rsa.PrivateKey
for i := 0; i < 20; i++ {
latest, err = keyRegistry.latestPrivateKey()
if err == nil && latest != nil && latest.Equal(key1) {
break
}
time.Sleep(50 * time.Millisecond)
}
if latest == nil || !latest.Equal(key1) {
t.Fatalf("expected keyRegistry to pick up newly added key secret %s", secName1)
}

// 2. Add second newer key via secret update/add
key2, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
cert2, err := crypto.SignKeyWithNotBefore(rand.Reader, key2, time.Now().Add(time.Minute), time.Hour, "cn2")
if err != nil {
t.Fatal(err)
}
secName2, err := writeKey(ctx, clientset, key2, []*x509.Certificate{cert2}, "ns", SealedSecretsKeyLabel, "prefix-", "", "")
if err != nil {
t.Fatal(err)
}

for i := 0; i < 20; i++ {
latest, err = keyRegistry.latestPrivateKey()
if err == nil && latest != nil && latest.Equal(key2) {
break
}
time.Sleep(50 * time.Millisecond)
}
if latest == nil || !latest.Equal(key2) {
t.Fatalf("expected keyRegistry to pick up updated key secret %s", secName2)
}

// 3. Delete key2 secret -> should unregister key2 and fallback to key1
err = clientset.CoreV1().Secrets("ns").Delete(ctx, secName2, metav1.DeleteOptions{})
if err != nil {
t.Fatal(err)
}

for i := 0; i < 20; i++ {
latest, err = keyRegistry.latestPrivateKey()
if err == nil && latest != nil && latest.Equal(key1) {
break
}
time.Sleep(50 * time.Millisecond)
}
if latest == nil || !latest.Equal(key1) {
t.Fatalf("expected keyRegistry to fallback to key1 after key2 deletion, got: %v", latest)
}
}
16 changes: 16 additions & 0 deletions pkg/controller/keyregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ func (kr *KeyRegistry) registerNewKey(keyName string, privKey *rsa.PrivateKey, c
return nil
}

func (kr *KeyRegistry) unregisterKey(fingerprint string) {
kr.mu.Lock()
defer kr.mu.Unlock()

delete(kr.keys, fingerprint)

if kr.mostRecentKey != nil && kr.mostRecentKey.fingerprint == fingerprint {
kr.mostRecentKey = nil
for _, k := range kr.keys {
if kr.mostRecentKey == nil || kr.mostRecentKey.orderingTime.Before(k.orderingTime) {
kr.mostRecentKey = k
}
}
}
}

func (kr *KeyRegistry) latestPrivateKey() (*rsa.PrivateKey, error) {
kr.mu.RLock()
defer kr.mu.RUnlock()
Expand Down
40 changes: 40 additions & 0 deletions pkg/controller/keyregistry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,43 @@ func TestConcurrentAccess(t *testing.T) {
t.Errorf("keyLen after concurrent registration: got %d, want %d", got, numKeys)
}
}

func TestUnregisterKey(t *testing.T) {
const keySize = 2048
validFor := time.Hour
cn := "my-cn"
kr := NewKeyRegistry(nil, "namespace", "prefix", "label", keySize)

key1, cert1, err := generatePrivateKeyAndCert(keySize, validFor, cn)
if err != nil {
t.Fatal(err)
}
key2, cert2, err := generatePrivateKeyAndCert(keySize, validFor, cn)
if err != nil {
t.Fatal(err)
}

t1 := time.Now()
t2 := t1.Add(time.Minute)

if err := kr.registerNewKey("k1", key1, cert1, t1); err != nil {
t.Fatal(err)
}
if err := kr.registerNewKey("k2", key2, cert2, t2); err != nil {
t.Fatal(err)
}

fp2, _ := crypto.PublicKeyFingerprint(&key2.PublicKey)
kr.unregisterKey(fp2)

if kr.keyLen() != 1 {
t.Fatalf("expected 1 key, got %d", kr.keyLen())
}
got, err := kr.latestPrivateKey()
if err != nil {
t.Fatal(err)
}
if got != key1 {
t.Errorf("got %v, want %v", got, key1)
}
}
5 changes: 5 additions & 0 deletions pkg/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"crypto/x509"
"fmt"
"io"
"log/slog"
"os"
Expand Down Expand Up @@ -108,6 +109,10 @@ func registryNewKeyWithSecret(secret *v1.Secret, keyRegistry *KeyRegistry, keyOr
key, certs, err := readKey(secret)
if err != nil {
slog.Error("Error reading key", "secret", secret.Name, "error", err)
return err
}
if len(certs) == 0 {
return fmt.Errorf("no certificates found in secret %s", secret.Name)
}

// Select ordering time based on the keyOrderPriority flag
Expand Down
Loading