Skip to content

Commit

Permalink
Allow to disable secret auto-recreation
Browse files Browse the repository at this point in the history
Signed-off-by: Jose Luis Vazquez Gonzalez <[email protected]>
  • Loading branch information
Jose Luis Vazquez Gonzalez committed Mar 2, 2023
1 parent 6522ac6 commit ac73912
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 30 deletions.
4 changes: 4 additions & 0 deletions carvel/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ spec:
type: boolean
description: Specifies whether the Sealed Secrets controller should update the status subresource
default: true
recreate:
type: boolean
description: Specifies whether the Sealed Secrets controller should recreate removed secrets
default: false
keyrenewperiod:
type: string
description: Specifies key renewal period. Default 30 days
Expand Down
2 changes: 2 additions & 0 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func bindControllerFlags(f *controller.Flags, fs *flag.FlagSet) {

fs.BoolVar(&f.UpdateStatus, "update-status", true, "beta: if true, the controller will update the status sub-resource whenever it processes a sealed secret")

fs.BoolVar(&f.Recreate, "recreate", true, "if true the controller will listen for secret changes to recreate managed secrets on removal. Helps setting it to false on limited permission environments.")

fs.DurationVar(&f.KeyRenewPeriod, "rotate-period", defaultKeyRenewPeriod, "")
_ = fs.MarkDeprecated("rotate-period", "please use key-renew-period instead")
}
Expand Down
1 change: 1 addition & 0 deletions helm/sealed-secrets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ The command removes all the Kubernetes components associated with the chart and
| `createController` | Specifies whether the Sealed Secrets controller should be created | `true` |
| `secretName` | The name of an existing TLS secret containing the key used to encrypt secrets | `sealed-secrets-key` |
| `updateStatus` | Specifies whether the Sealed Secrets controller should update the status subresource | `true` |
| `recreate ` | Specifies whether the Sealed Secrets controller should recreate removed secrets | `false` |
| `keyrenewperiod` | Specifies key renewal period. Default 30 days | `""` |
| `rateLimit` | Number of allowed sustained request per second for verify endpoint | `""` |
| `rateLimitBurst` | Number of requests allowed to exceed the rate limit per second for verify endpoint | `""` |
Expand Down
3 changes: 3 additions & 0 deletions helm/sealed-secrets/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ spec:
{{- if .Values.updateStatus }}
- --update-status
{{- end }}
{{- if .Values.recreate }}
- --recreate
{{- end }}
{{- if .Values.keyrenewperiod }}
- --key-renew-period
- {{ .Values.keyrenewperiod | quote }}
Expand Down
6 changes: 6 additions & 0 deletions helm/sealed-secrets/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ secretName: "sealed-secrets-key"
## @param updateStatus Specifies whether the Sealed Secrets controller should update the status subresource
##
updateStatus: true
## @param recreate Specifies whether the Sealed Secrets controller should recreate removed secrets
## Setting it to false allows to optionally restore backward compatibility in low previlege
## environments when old versions of the controller did not require watch permissions on secrets
## for secret re-creation.
##
recreate: true
## @param keyrenewperiod Specifies key renewal period. Default 30 days
## e.g
## keyrenewperiod: "720h30m"
Expand Down
80 changes: 80 additions & 0 deletions integration/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,23 @@ import (

"github.com/onsi/gomega/types"
v1 "k8s.io/api/core/v1"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"

certUtil "k8s.io/client-go/util/cert"
"k8s.io/client-go/util/keyutil"

"k8s.io/client-go/informers"

ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1"
ssclient "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned"
ssinformers "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions"
"github.com/bitnami-labs/sealed-secrets/pkg/controller"
"github.com/bitnami-labs/sealed-secrets/pkg/crypto"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -620,3 +627,76 @@ var _ = Describe("controller --version", func() {
Expect(output.String()).Should(MatchRegexp("^controller version: (v[0-9]+\\.[0-9]+\\.[0-9]+|[0-9a-f]{40})(\\+dirty)?"))
})
})

var _ = Describe("new controller", func() {
var ssctl *controller.Controller
var clientset *kubernetes.Clientset
var c *corev1.CoreV1Client
var ssc ssclient.Interface
var ns string
var ssinformer ssinformers.SharedInformerFactory
var sinformer informers.SharedInformerFactory
var keyRegistry *controller.KeyRegistry
var (
ctx context.Context
cancelLog context.CancelFunc
)

Context("with default setup", func() {
BeforeEach(func() {
ctx, cancelLog = context.WithCancel(context.Background())

conf := clusterConfigOrDie()
clientset = clientSetOrDie(conf)
c = corev1.NewForConfigOrDie(conf)
ssc = ssclient.NewForConfigOrDie(conf)
ns = createNsOrDie(ctx, c, "create")
var tweakopts func(*metav1.ListOptions)
sinformer = controller.InitSecretInformerFactory(clientset, ns, tweakopts, true)
ssinformer = ssinformers.NewFilteredSharedInformerFactory(ssc, 0, ns, tweakopts)
keyRegistry = keyRegisterOrDie(ctx, clientset, ns)
})

AfterEach(func() {
deleteNsOrDie(ctx, c, ns)
cancelLog()
})

It("works as expected", func() {
var err error
Expect(sinformer).NotTo(BeNil())
ssctl, err = controller.NewController(clientset, ssc, ssinformer, sinformer, keyRegistry)
Expect(ssctl).ToNot(BeNil())
Expect(err).ToNot(HaveOccurred())
})
})

Context("without secret listener", func() {
BeforeEach(func() {
ctx, cancelLog = context.WithCancel(context.Background())

conf := clusterConfigOrDie()
clientset = clientSetOrDie(conf)
c = corev1.NewForConfigOrDie(conf)
ssc = ssclient.NewForConfigOrDie(conf)
ns = createNsOrDie(ctx, c, "create")
var tweakopts func(*metav1.ListOptions)
sinformer = controller.InitSecretInformerFactory(clientset, ns, tweakopts, false)
ssinformer = ssinformers.NewFilteredSharedInformerFactory(ssc, 0, ns, tweakopts)
keyRegistry = keyRegisterOrDie(ctx, clientset, ns)
})

AfterEach(func() {
deleteNsOrDie(ctx, c, ns)
cancelLog()
})

It("still works", func() {
var err error
Expect(sinformer).To(BeNil())
ssctl, err = controller.NewController(clientset, ssc, ssinformer, sinformer, keyRegistry)
Expect(ssctl).ToNot(BeNil())
Expect(err).ToNot(HaveOccurred())
})
})
})
25 changes: 25 additions & 0 deletions integration/integration_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bufio"
"bytes"
"context"
"crypto/rand"
"flag"
"fmt"
"io"
Expand All @@ -16,12 +17,14 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"

ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1"
"github.com/bitnami-labs/sealed-secrets/pkg/controller"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand All @@ -40,8 +43,10 @@ func clusterConfigOrDie() *rest.Config {
var err error

if *kubeconfig != "" {
fmt.Fprintf(GinkgoWriter, "config from kubectl\n")
config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
} else {
fmt.Fprintf(GinkgoWriter, "in cluster config\n")
config, err = rest.InClusterConfig()
}
if err != nil {
Expand Down Expand Up @@ -184,6 +189,26 @@ func runKubesealWith(flags []string, input runtime.Object, opts ...runAppOpt) (r
return outputObj, nil
}

func clientSetOrDie(config *rest.Config) *kubernetes.Clientset {
cs, err := kubernetes.NewForConfig(config)
if err != nil {
panic("failed to create Kubernetes clientset" + err.Error())
}
return cs
}

func keyRegisterOrDie(ctx context.Context, clientset *kubernetes.Clientset, ns string) *controller.KeyRegistry {
keyLabel := controller.SealedSecretsKeyLabel
prefix := "test-keys"
testKeySize := 4096
fmt.Fprintf(GinkgoWriter, "initiating key registry\n")
keyRegistry, err := controller.InitKeyRegistry(ctx, clientset, rand.Reader, ns, prefix, keyLabel, testKeySize)
if err != nil {
panic("failed to provision key registry" + err.Error())
}
return keyRegistry
}

func TestE2e(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "sealed-secrets integration tests")
Expand Down
54 changes: 36 additions & 18 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,34 @@ func NewController(clientset kubernetes.Interface, ssclientset ssclientset.Inter
eventBroadcaster.StartRecordingToSink(&v1.EventSinkImpl{Interface: clientset.CoreV1().Events("")})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "sealed-secrets"})

ssInformer, err := watchSealedSecrets(ssinformer, queue)
if err != nil {
return nil, err
}

var sInformer cache.SharedIndexInformer
if sinformer != nil {
sInformer, err = watchSecrets(sinformer, ssclientset, queue)
if err != nil {
return nil, err
}
}

return &Controller{
ssInformer: ssInformer,
sInformer: sInformer,
queue: queue,
sclient: clientset.CoreV1(),
ssclient: ssclientset.BitnamiV1alpha1(),
recorder: recorder,
keyRegistry: keyRegistry,
}, nil
}

func watchSealedSecrets(ssinformer ssinformer.SharedInformerFactory, queue workqueue.RateLimitingInterface) (cache.SharedIndexInformer, error) {
ssInformer := ssinformer.Bitnami().V1alpha1().
SealedSecrets().
Informer()

_, err := ssInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
Expand All @@ -114,9 +138,12 @@ func NewController(clientset kubernetes.Interface, ssclientset ssclientset.Inter
if err != nil {
return nil, fmt.Errorf("could not add event handler to sealed secrets informer: %w", err)
}
return ssInformer, nil
}

func watchSecrets(sinformer informers.SharedInformerFactory, ssclientset ssclientset.Interface, queue workqueue.RateLimitingInterface) (cache.SharedIndexInformer, error) {
sInformer := sinformer.Core().V1().Secrets().Informer()
_, err = sInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
_, err := sInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
DeleteFunc: func(obj interface{}) {
skey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
Expand Down Expand Up @@ -154,16 +181,7 @@ func NewController(clientset kubernetes.Interface, ssclientset ssclientset.Inter
if err != nil {
return nil, fmt.Errorf("could not add event handler to secrets informer: %w", err)
}

return &Controller{
ssInformer: ssInformer,
sInformer: sInformer,
queue: queue,
sclient: clientset.CoreV1(),
ssclient: ssclientset.BitnamiV1alpha1(),
recorder: recorder,
keyRegistry: keyRegistry,
}, nil
return sInformer, nil
}

// HasSynced returns true once this controller has completed an
Expand Down Expand Up @@ -192,7 +210,7 @@ func (c *Controller) Run(stopCh <-chan struct{}) {
go c.sInformer.Run(stopCh)

if !cache.WaitForCacheSync(stopCh, c.HasSynced) {
utilruntime.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
return
}

Expand Down Expand Up @@ -417,7 +435,7 @@ func (c *Controller) AttemptUnseal(content []byte) (bool, error) {
}
return true, nil
default:
return false, fmt.Errorf("Unexpected resource type: %s", s.GetObjectKind().GroupVersionKind().String())
return false, fmt.Errorf("unexpected resource type: %s", s.GetObjectKind().GroupVersionKind().String())
}
}

Expand All @@ -434,20 +452,20 @@ func (c *Controller) Rotate(content []byte) ([]byte, error) {
case *ssv1alpha1.SealedSecret:
secret, err := c.attemptUnseal(s)
if err != nil {
return nil, fmt.Errorf("Error decrypting secret. %v", err)
return nil, fmt.Errorf("error decrypting secret. %v", err)
}
latestPrivKey := c.keyRegistry.latestPrivateKey()
resealedSecret, err := ssv1alpha1.NewSealedSecret(scheme.Codecs, &latestPrivKey.PublicKey, secret)
if err != nil {
return nil, fmt.Errorf("Error creating new sealed secret. %v", err)
return nil, fmt.Errorf("error creating new sealed secret. %v", err)
}
data, err := json.Marshal(resealedSecret)
if err != nil {
return nil, fmt.Errorf("Error marshalling new secret to json. %v", err)
return nil, fmt.Errorf("error marshalling new secret to json. %v", err)
}
return data, nil
default:
return nil, fmt.Errorf("Unexpected resource type: %s", s.GetObjectKind().GroupVersionKind().String())
return nil, fmt.Errorf("unexpected resource type: %s", s.GetObjectKind().GroupVersionKind().String())
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const SealedSecretsKeyLabel = "sealedsecrets.bitnami.com/sealed-secrets-key"

var (
// ErrPrivateKeyNotRSA is returned when the private key is not a valid RSA key.
ErrPrivateKeyNotRSA = errors.New("Private key is not an RSA key")
ErrPrivateKeyNotRSA = errors.New("private key is not an RSA key")
)

func generatePrivateKeyAndCert(keySize int, validFor time.Duration, cn string) (*rsa.PrivateKey, *x509.Certificate, error) {
Expand Down
14 changes: 11 additions & 3 deletions pkg/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ type Flags struct {
RateLimitBurst int
OldGCBehavior bool
UpdateStatus bool
Recreate bool
}

func initKeyPrefix(keyPrefix string) (string, error) {
return validateKeyPrefix(keyPrefix)
}

func initKeyRegistry(ctx context.Context, client kubernetes.Interface, r io.Reader, namespace, prefix, label string, keysize int) (*KeyRegistry, error) {
func InitKeyRegistry(ctx context.Context, client kubernetes.Interface, r io.Reader, namespace, prefix, label string, keysize int) (*KeyRegistry, error) {
log.Printf("Searching for existing private keys")
secretList, err := client.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{
LabelSelector: keySelector.String(),
Expand Down Expand Up @@ -161,7 +162,7 @@ func Main(f *Flags, version string) error {
return err
}

keyRegistry, err := initKeyRegistry(ctx, clientset, rand.Reader, myNs, prefix, SealedSecretsKeyLabel, f.KeySize)
keyRegistry, err := InitKeyRegistry(ctx, clientset, rand.Reader, myNs, prefix, SealedSecretsKeyLabel, f.KeySize)
if err != nil {
return err
}
Expand Down Expand Up @@ -226,7 +227,7 @@ func Main(f *Flags, version string) error {
}
if ns != namespace {
ssinf = ssinformers.NewFilteredSharedInformerFactory(ssclientset, 0, ns, tweakopts)
sinf = informers.NewFilteredSharedInformerFactory(clientset, 0, ns, tweakopts)
sinf = InitSecretInformerFactory(clientset, ns, tweakopts, f.Recreate)
ctlr, err = NewController(clientset, ssclientset, ssinf, sinf, keyRegistry)
if err != nil {
return err
Expand Down Expand Up @@ -255,3 +256,10 @@ func Main(f *Flags, version string) error {

return server.Shutdown(context.Background())
}

func InitSecretInformerFactory(clientset *kubernetes.Clientset, ns string, tweakopts func(*metav1.ListOptions), recreate bool) informers.SharedInformerFactory {
if recreate {
return informers.NewFilteredSharedInformerFactory(clientset, 0, ns, tweakopts)
}
return nil
}
Loading

0 comments on commit ac73912

Please sign in to comment.